diff --git a/.github/workflows/motoko-token-transfer-example.yml b/.github/workflows/motoko-token-transfer-example.yml deleted file mode 100644 index ed14de84b..000000000 --- a/.github/workflows/motoko-token-transfer-example.yml +++ /dev/null @@ -1,39 +0,0 @@ -# Known failure: https://dfinity.atlassian.net/browse/EM-5 -name: motoko-token_transfer -on: - push: - branches: - - master - pull_request: - paths: - - motoko/token_transfer/** - - .github/workflows/provision-darwin.sh - - .github/workflows/provision-linux.sh - - .github/workflows/motoko-token-transfer-example.yml - - .ic-commit -concurrency: - group: ${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: true -jobs: - motoko-token_transfer-darwin: - runs-on: macos-15 - steps: - - uses: actions/checkout@50fbc622fc4ef5163becd7fab6573eac35f8462e # v1.2.0 - - name: Provision Darwin - run: bash .github/workflows/provision-darwin.sh - - name: Motoko Ledger Transfer Darwin - run: | - pushd motoko/token_transfer - bash ./demo.sh - popd - motoko-token_transfer-linux: - runs-on: ubuntu-24.04 - steps: - - uses: actions/checkout@50fbc622fc4ef5163becd7fab6573eac35f8462e # v1.2.0 - - name: Provision Linux - run: bash .github/workflows/provision-linux.sh - - name: Motoko Ledger Transfer Linux - run: | - pushd motoko/token_transfer - bash ./demo.sh - popd diff --git a/.github/workflows/motoko-token-transfer-from-example.yml b/.github/workflows/motoko-token-transfer-from-example.yml deleted file mode 100644 index 4e11b772c..000000000 --- a/.github/workflows/motoko-token-transfer-from-example.yml +++ /dev/null @@ -1,39 +0,0 @@ -# Known failure: https://dfinity.atlassian.net/browse/EM-5 -name: motoko-token_transfer_from -on: - push: - branches: - - master - pull_request: - paths: - - motoko/token_transfer_from/** - - .github/workflows/provision-darwin.sh - - .github/workflows/provision-linux.sh - - .github/workflows/motoko-token-transfer-from-example.yml - - .ic-commit -concurrency: - group: ${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: true -jobs: - motoko-token_transfer_from-darwin: - runs-on: macos-15 - steps: - - uses: actions/checkout@50fbc622fc4ef5163becd7fab6573eac35f8462e # v1.2.0 - - name: Provision Darwin - run: bash .github/workflows/provision-darwin.sh - - name: Motoko Ledger Transfer Darwin - run: | - pushd motoko/token_transfer_from - bash ./demo.sh - popd - motoko-token_transfer_from-linux: - runs-on: ubuntu-24.04 - steps: - - uses: actions/checkout@50fbc622fc4ef5163becd7fab6573eac35f8462e # v1.2.0 - - name: Provision Linux - run: bash .github/workflows/provision-linux.sh - - name: Motoko Ledger Transfer Linux - run: | - pushd motoko/token_transfer_from - bash ./demo.sh - popd diff --git a/.github/workflows/ninja_pr_checks.yml b/.github/workflows/ninja_pr_checks.yml index 8ae510de3..a5a39e7f7 100644 --- a/.github/workflows/ninja_pr_checks.yml +++ b/.github/workflows/ninja_pr_checks.yml @@ -31,7 +31,6 @@ jobs: examples=() declare -A example_paths=( - ["My Crypto Blog (Frontend)"]="hosting/my_crypto_blog" ["React (Frontend)"]="hosting/react" ["OISY Signer Demo (Frontend)"]="hosting/oisy-signer-demo" ["Motoko backend (Motoko)"]="motoko/backend_only" @@ -50,8 +49,6 @@ jobs: ["Superheroes (Motoko)"]="motoko/superheroes" ["Threshold ECDSA (Motoko)"]="motoko/threshold-ecdsa" ["Threshold Schnorr (Motoko)"]="motoko/threshold-schnorr" - ["Tokenmania (Motoko)"]="motoko/tokenmania" - ["NFT Creator (Motoko)"]="motoko/nft-creator" ["Who Am I (Motoko)"]="motoko/who_am_i" ["Rust backend (Rust)"]="rust/backend_only" ["Rust backend Wasm64 (Rust)"]="rust/backend_wasm64" @@ -74,7 +71,6 @@ jobs: ["Send HTTP Post (Rust)"]="rust/send_http_post" ["SIMD (Rust)"]="rust/simd" ["Threshold ECDSA (Rust)"]="rust/threshold-ecdsa" - ["Tokenmania (Rust)"]="rust/tokenmania" ["Unit Testable Canister (Rust)"]="rust/unit_testable_rust_canister" ["Who Am I (Rust)"]="rust/who_am_i" ["Photo Gallery (Rust)"]="rust/photo_gallery" diff --git a/.github/workflows/rust-token-transfer-example.yml b/.github/workflows/rust-token-transfer-example.yml deleted file mode 100644 index 16e3578bc..000000000 --- a/.github/workflows/rust-token-transfer-example.yml +++ /dev/null @@ -1,43 +0,0 @@ -# Known failure: https://dfinity.atlassian.net/browse/EM-5 -name: rust-token_transfer -on: - push: - branches: - - master - pull_request: - paths: - - rust/token_transfer/** - - .github/workflows/provision-darwin.sh - - .github/workflows/provision-linux.sh - - .github/workflows/rust-token-transfer-example.yml - - .ic-commit -concurrency: - group: ${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: true -jobs: - rust-token_transfer-darwin: - runs-on: macos-15 - steps: - - uses: actions/checkout@ee0669bd1cc54295c223e0bb666b733df41de1c5 # v2.7.0 - with: - submodules: recursive - - name: Provision Darwin - run: bash .github/workflows/provision-darwin.sh - - name: Rust Tokens Transfer Darwin - run: | - pushd rust/token_transfer - bash ./demo.sh - popd - rust-token_transfer-linux: - runs-on: ubuntu-24.04 - steps: - - uses: actions/checkout@ee0669bd1cc54295c223e0bb666b733df41de1c5 # v2.7.0 - with: - submodules: recursive - - name: Provision Linux - run: bash .github/workflows/provision-linux.sh - - name: Rust Tokens Transfer Linux - run: | - pushd rust/token_transfer - bash ./demo.sh - popd diff --git a/.github/workflows/rust-token-transfer-from-example.yml b/.github/workflows/rust-token-transfer-from-example.yml deleted file mode 100644 index affe3b2f0..000000000 --- a/.github/workflows/rust-token-transfer-from-example.yml +++ /dev/null @@ -1,43 +0,0 @@ -# Known failure: https://dfinity.atlassian.net/browse/EM-5 -name: rust-token_transfer_from -on: - push: - branches: - - master - pull_request: - paths: - - rust/token_transfer_from/** - - .github/workflows/provision-darwin.sh - - .github/workflows/provision-linux.sh - - .github/workflows/rust-token-transfer-from-example.yml - - .ic-commit -concurrency: - group: ${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: true -jobs: - rust-token_transfer_from-darwin: - runs-on: macos-15 - steps: - - uses: actions/checkout@ee0669bd1cc54295c223e0bb666b733df41de1c5 # v2.7.0 - with: - submodules: recursive - - name: Provision Darwin - run: bash .github/workflows/provision-darwin.sh - - name: Rust Tokens Transfer Darwin - run: | - pushd rust/token_transfer_from - bash ./demo.sh - popd - rust-token_transfer_from-linux: - runs-on: ubuntu-24.04 - steps: - - uses: actions/checkout@ee0669bd1cc54295c223e0bb666b733df41de1c5 # v2.7.0 - with: - submodules: recursive - - name: Provision Linux - run: bash .github/workflows/provision-linux.sh - - name: Rust Tokens Transfer Linux - run: | - pushd rust/token_transfer_from - bash ./demo.sh - popd diff --git a/hosting/my_crypto_blog/.devcontainer/devcontainer.json b/hosting/my_crypto_blog/.devcontainer/devcontainer.json deleted file mode 100644 index ebb0b8bcc..000000000 --- a/hosting/my_crypto_blog/.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/hosting/my_crypto_blog/BUILD.md b/hosting/my_crypto_blog/BUILD.md deleted file mode 100644 index 0105138b0..000000000 --- a/hosting/my_crypto_blog/BUILD.md +++ /dev/null @@ -1,28 +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 -``` - -The local canister URL will be shown in the terminal output. Open it in your web browser. - -## Additional examples - -Additional code examples and sample applications can be found in the [DFINITY examples repo](https://github.com/dfinity/examples). diff --git a/hosting/my_crypto_blog/README.md b/hosting/my_crypto_blog/README.md deleted file mode 100644 index f6d174a7d..000000000 --- a/hosting/my_crypto_blog/README.md +++ /dev/null @@ -1,63 +0,0 @@ -# My Crypto Blog - -[View this sample's code on GitHub](https://github.com/dfinity/examples/tree/master/hosting/my_crypto_blog) - -## Overview - -A simple blog-style web application hosted entirely onchain on ICP. Built with React, Vite, and Tailwind CSS, it demonstrates how to deploy a frontend-only application as an asset canister — no backend needed. - -The app fetches blog posts from an external API and displays them in a card layout. - -## Project structure - -The `/frontend` folder contains the web assets for the application's user interface, built with React, Vite, and Tailwind CSS. The frontend is deployed as an asset canister — no backend canister is needed. - -## 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/hosting/my_crypto_blog) - -> **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: - -```bash -git clone https://github.com/dfinity/examples -cd examples/hosting/my_crypto_blog -``` - -### Deployment - -Start the local network: - -```bash -icp network start -d -``` - -Deploy the canister: - -```bash -icp deploy -``` - -The URL for the frontend depends on the canister ID. When deployed, the URL will look like this: - -``` -http://{canister_id}.localhost:8000 -``` - -Stop the local network when done: - -```bash -icp network stop -``` diff --git a/hosting/my_crypto_blog/dfx.json b/hosting/my_crypto_blog/dfx.json deleted file mode 100644 index c661b6694..000000000 --- a/hosting/my_crypto_blog/dfx.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "canisters": { - "frontend": { - "frontend": { - "entrypoint": "frontend/index.html" - }, - "source": ["frontend/dist"], - "type": "assets" - } - } -} diff --git a/hosting/my_crypto_blog/frontend/index.css b/hosting/my_crypto_blog/frontend/index.css deleted file mode 100644 index b5c61c956..000000000 --- a/hosting/my_crypto_blog/frontend/index.css +++ /dev/null @@ -1,3 +0,0 @@ -@tailwind base; -@tailwind components; -@tailwind utilities; diff --git a/hosting/my_crypto_blog/frontend/index.html b/hosting/my_crypto_blog/frontend/index.html deleted file mode 100644 index b1885b18a..000000000 --- a/hosting/my_crypto_blog/frontend/index.html +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - My Crypto Blog - - - -
- - - diff --git a/hosting/my_crypto_blog/frontend/package.json b/hosting/my_crypto_blog/frontend/package.json deleted file mode 100644 index 14c52d283..000000000 --- a/hosting/my_crypto_blog/frontend/package.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "name": "frontend", - "private": true, - "type": "module", - "scripts": { - "prebuild": "npm i --include=dev", - "build": "vite build", - "dev": "vite" - }, - "dependencies": { - "react": "~19.2.4", - "react-dom": "~19.2.4" - }, - "devDependencies": { - "@types/react": "~19.2.14", - "@types/react-dom": "~19.2.3", - "@vitejs/plugin-react": "~5.1.4", - "autoprefixer": "~10.4.24", - "postcss": "~8.5.6", - "tailwindcss": "~3.4.19", - "vite": "~7.3.1" - } -} diff --git a/hosting/my_crypto_blog/frontend/postcss.config.js b/hosting/my_crypto_blog/frontend/postcss.config.js deleted file mode 100644 index 8c6e0c42c..000000000 --- a/hosting/my_crypto_blog/frontend/postcss.config.js +++ /dev/null @@ -1,6 +0,0 @@ -export default { - plugins: { - autoprefixer: {}, - tailwindcss: {} - } -}; diff --git a/hosting/my_crypto_blog/frontend/public/favicon.ico b/hosting/my_crypto_blog/frontend/public/favicon.ico deleted file mode 100644 index 338fbf34c..000000000 Binary files a/hosting/my_crypto_blog/frontend/public/favicon.ico and /dev/null differ diff --git a/hosting/my_crypto_blog/frontend/src/main.jsx b/hosting/my_crypto_blog/frontend/src/main.jsx deleted file mode 100644 index a94bc4815..000000000 --- a/hosting/my_crypto_blog/frontend/src/main.jsx +++ /dev/null @@ -1,69 +0,0 @@ -import React, { useState } from 'react'; -import ReactDOM from 'react-dom/client'; -import '../index.css'; - -function App() { - const [data, setData] = useState(null); - const [isLoading, setIsLoading] = useState(false); - const [error, setError] = useState(null); - - const fetchData = async () => { - setIsLoading(true); - try { - const response = await fetch('https://jsonplaceholder.typicode.com/posts'); - if (!response.ok) { - throw new Error('Network response was not ok'); - } - const result = await response.json(); - setData(result); - } catch (err) { - setError(err.message); - } finally { - setIsLoading(false); - } - }; - - return ( -
-
-

🚀 My Crypto Blog 🚀

-

- A simple web page hosted onchain on ICP. Built with React, Vite, and Tailwind CSS. -

-

-

- You can host any kind of frontend application, including React, Vue, Svelte, and more on ICP! -

-
- -
- {isLoading &&

Loading...

} - {error &&

Error: {error}

} - {data && ( -
- {data.slice(0, 3).map( - ( - post // Display only first 3 posts for brevity - ) => ( -
-

{post.title}

-

{post.body.slice(0, 100)}...

-
- ) - )} -
- )} -
-
- ); -} - -export default App; - -ReactDOM.createRoot(document.getElementById('root')).render( - - - -); diff --git a/hosting/my_crypto_blog/frontend/tailwind.config.js b/hosting/my_crypto_blog/frontend/tailwind.config.js deleted file mode 100644 index e13a6efee..000000000 --- a/hosting/my_crypto_blog/frontend/tailwind.config.js +++ /dev/null @@ -1,3 +0,0 @@ -export default { - content: ['./src/**/*.{js,ts,jsx,tsx}'] -}; diff --git a/hosting/my_crypto_blog/frontend/vite.config.js b/hosting/my_crypto_blog/frontend/vite.config.js deleted file mode 100644 index b8d0a32a7..000000000 --- a/hosting/my_crypto_blog/frontend/vite.config.js +++ /dev/null @@ -1,21 +0,0 @@ -import react from '@vitejs/plugin-react'; -import { defineConfig } from 'vite'; - -export default defineConfig({ - base: './', - plugins: [react()], - envDir: '../', - define: { - 'process.env': process.env - }, - optimizeDeps: { - esbuildOptions: { - define: { - global: 'globalThis' - } - } - }, - server: { - host: '127.0.0.1' - } -}); diff --git a/hosting/my_crypto_blog/icp.yaml b/hosting/my_crypto_blog/icp.yaml deleted file mode 100644 index 29e5edb5f..000000000 --- a/hosting/my_crypto_blog/icp.yaml +++ /dev/null @@ -1,8 +0,0 @@ -canisters: - - name: frontend - recipe: - type: "@dfinity/asset-canister@v2.1.0" - configuration: - dir: frontend/dist - build: - - npm run build --prefix frontend diff --git a/hosting/my_crypto_blog/package.json b/hosting/my_crypto_blog/package.json deleted file mode 100644 index 6b1c326b7..000000000 --- a/hosting/my_crypto_blog/package.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "name": "my_crypto_blog", - "scripts": { - "build": "npm run build --workspaces --if-present", - "prebuild": "npm run prebuild --workspaces --if-present", - "dev": "npm run dev --workspaces --if-present" - }, - "type": "module", - "workspaces": [ - "frontend" - ] -} diff --git a/motoko/encrypted-notes-dapp-vetkd/README.md b/motoko/encrypted-notes-dapp-vetkd/README.md deleted file mode 100644 index df8c05b76..000000000 --- a/motoko/encrypted-notes-dapp-vetkd/README.md +++ /dev/null @@ -1,3 +0,0 @@ -# Encrypted notes: vetKD - -This example has moved [here](https://github.com/dfinity/vetkeys/tree/main/examples/encrypted_notes_dapp_vetkd). diff --git a/motoko/nft-creator/.devcontainer/devcontainer.json b/motoko/nft-creator/.devcontainer/devcontainer.json deleted file mode 100644 index ebb0b8bcc..000000000 --- a/motoko/nft-creator/.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/nft-creator/BUILD.md b/motoko/nft-creator/BUILD.md deleted file mode 100644 index aab745625..000000000 --- a/motoko/nft-creator/BUILD.md +++ /dev/null @@ -1,113 +0,0 @@ -# Continue building locally - -Projects deployed through ICP Ninja are temporary; they will only be live for 30 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. - -To migrate your ICP Ninja project off of the web browser and develop it locally, follow these steps. - -### 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 - -``` - -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). - -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). - -### 6. Deploy to the mainnet. - -Once you have cycles, run the command: - -``` - -dfx deploy --network ic - -``` - -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). \ No newline at end of file diff --git a/motoko/nft-creator/README.md b/motoko/nft-creator/README.md deleted file mode 100644 index 0f0e1f32a..000000000 --- a/motoko/nft-creator/README.md +++ /dev/null @@ -1,31 +0,0 @@ -# NFT Creator - -> [!CAUTION] -> This example is for demonstration purposes. It does not reflect a best practices workflow for creating and minting NFTs on ICP. -> NFTs deployed using this example are only available for 20 minutes and will be deleted afterwards. They should be treated as "testnet" assets and should not be given real value. - -This example demonstrates how to create a simple NFT creator on the Internet Computer using Motoko. It allows the first authorized user claiming the collection to mint NFTs to other users. The owned NFTs can be viewed and transferred by their owners. The backend compliant with the [ICRC-7 NFT standard](https://github.com/dfinity/ICRC/blob/main/ICRCs/ICRC-7/ICRC-7.md). - -## 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: - -[![](https://icp.ninja/assets/open.svg)](https://icp.ninja/i?g=https://github.com/dfinity/examples/motoko/nft-creator) - -## Build and deploy from the command-line - -### 1. [Download and install the IC SDK.](https://internetcomputer.org/docs/building-apps/getting-started/install) - -### 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/) - -### 3. Navigate into the project's directory. - -### 4. Deploy the project to your local environment: - -``` -dfx start --background --clean && dfx deploy -``` - -## 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://internetcomputer.org/docs/building-apps/security/overview) for developing on ICP. This example may not implement all the best practices. diff --git a/motoko/nft-creator/backend/app.mo b/motoko/nft-creator/backend/app.mo deleted file mode 100644 index 1b013acda..000000000 --- a/motoko/nft-creator/backend/app.mo +++ /dev/null @@ -1,200 +0,0 @@ -// backend/app.mo -// NFT Canister implementing ICRC-7 standard with minting functionality - -// --- Standard Library Imports --- -import Principal "mo:core/Principal"; -import Runtime "mo:core/Runtime"; - -// --- Third-Party/External Imports --- -import Vec "mo:vector"; -import ICRC7 "mo:icrc7-mo"; -import ClassPlus "mo:class-plus"; - -// --- Local Imports --- -import DefaultConfig "defaultConfig"; - -// --- Actor Definition --- -shared (init_msg) persistent actor class NftCanister() : async (ICRC7.Service.Service) = this { - - // --- Initialization --- - transient let initManager = ClassPlus.ClassPlusInitializationManager( - init_msg.caller, - Principal.fromActor(this), - true, - ); - - var icrc7_migration_state = ICRC7.initialState(); - - private func get_icrc7_environment() : ICRC7.Environment { - { - add_ledger_transaction = null; - can_mint = null; - can_burn = null; - can_transfer = null; - can_update = null; - }; - }; - - transient let icrc7 = ICRC7.Init({ - manager = initManager; - initialState = icrc7_migration_state; - args = DefaultConfig.defaultConfig(init_msg.caller); - pullEnvironment = ?get_icrc7_environment; - onInitialize = null; - onStorageChange = func(new_state : ICRC7.State) { - icrc7_migration_state := new_state; - }; - }); - - // --- Query Calls --- - - public query func icrc7_symbol() : async Text { - switch (icrc7().get_ledger_info().symbol) { - case (?val) val; - case (null) ""; - }; - }; - - public query func icrc7_name() : async Text { - switch (icrc7().get_ledger_info().name) { - case (?val) val; - case (null) ""; - }; - }; - - public query func icrc7_description() : async ?Text { - icrc7().get_ledger_info().description; - }; - - public query func icrc7_logo() : async ?Text { - icrc7().get_ledger_info().logo; - }; - - public query func icrc7_max_memo_size() : async ?Nat { - ?icrc7().get_ledger_info().max_memo_size; - }; - - public query func icrc7_tx_window() : async ?Nat { - ?icrc7().get_ledger_info().tx_window; - }; - - public query func icrc7_permitted_drift() : async ?Nat { - ?icrc7().get_ledger_info().permitted_drift; - }; - - public query func icrc7_total_supply() : async Nat { - icrc7().get_stats().nft_count; - }; - - public query func icrc7_supply_cap() : async ?Nat { - icrc7().get_ledger_info().supply_cap; - }; - - public query func icrc7_max_query_batch_size() : async ?Nat { - icrc7().max_query_batch_size(); - }; - - public query func icrc7_max_update_batch_size() : async ?Nat { - icrc7().max_update_batch_size(); - }; - - public query func icrc7_default_take_value() : async ?Nat { - icrc7().default_take_value(); - }; - - public query func icrc7_max_take_value() : async ?Nat { - icrc7().max_take_value(); - }; - - public query func icrc7_atomic_batch_transfers() : async ?Bool { - icrc7().atomic_batch_transfers(); - }; - - public query func icrc7_collection_metadata() : async [(Text, ICRC7.Value)] { - let ledger_info = icrc7().collection_metadata(); - let results = Vec.new<(Text, ICRC7.Value)>(); - Vec.addFromIter(results, ledger_info.vals()); - Vec.toArray(results); - }; - - public query func icrc7_token_metadata(token_ids : [Nat]) : async [?[(Text, ICRC7.Value)]] { - icrc7().token_metadata(token_ids); - }; - - public query func icrc7_owner_of(token_ids : ICRC7.Service.OwnerOfRequest) : async ICRC7.Service.OwnerOfResponse { - switch (icrc7().get_token_owners(token_ids)) { - case (#ok(val)) val; - case (#err(err)) Runtime.trap(err); - }; - }; - - public query func icrc7_balance_of(accounts : ICRC7.Service.BalanceOfRequest) : async ICRC7.Service.BalanceOfResponse { - icrc7().balance_of(accounts); - }; - - public query func icrc7_tokens(prev : ?Nat, take : ?Nat) : async [Nat] { - icrc7().get_tokens_paginated(prev, take); - }; - - public query func icrc7_tokens_of(account : ICRC7.Account, prev : ?Nat, take : ?Nat) : async [Nat] { - icrc7().get_tokens_of_paginated(account, prev, take); - }; - - public query func icrc10_supported_standards() : async ICRC7.SupportedStandards { - [ - { name = "ICRC-7"; url = "https://github.com/dfinity/ICRC/ICRCs/ICRC-7" }, - { - name = "ICRC-10"; - url = "https://github.com/dfinity/ICRC/ICRCs/ICRC-10"; - }, - ]; - }; - - public query func collectionHasBeenClaimed() : async Bool { - hasBeenClaimed; - }; - - public query func getCollectionOwner() : async Principal { - icrc7().get_collection_owner(); - }; - - // --- Update Calls --- - - public shared (msg) func icrc7_transfer(args : [ICRC7.Service.TransferArg]) : async [?ICRC7.Service.TransferResult] { - icrc7().transfer(msg.caller, args); - }; - - var hasBeenClaimed = false; - - public shared (msg) func claimCollection() : async () { - if (hasBeenClaimed) { - return; - }; - ignore icrc7().update_ledger_info([#UpdateOwner(msg.caller)]); - hasBeenClaimed := true; - }; - - // --- Custom NFT Minting Example --- - - var nextTokenId = 0; - - public shared (msg) func mint(to : ICRC7.Account) : async [ICRC7.SetNFTResult] { - let setNftRequest : ICRC7.SetNFTItemRequest = { - token_id = nextTokenId; - metadata = #Map([("tokenUri", #Text(DefaultConfig.tokenURI))]); - owner = ?to; - override = false; - memo = null; - created_at_time = null; - }; - - switch (icrc7().set_nfts(msg.caller, [setNftRequest], true)) { - case (#ok(val)) { - nextTokenId += 1; - val; - }; - case (#err(err)) Runtime.trap(err); - }; - }; - -}; diff --git a/motoko/nft-creator/backend/defaultConfig.mo b/motoko/nft-creator/backend/defaultConfig.mo deleted file mode 100644 index 1e6933883..000000000 --- a/motoko/nft-creator/backend/defaultConfig.mo +++ /dev/null @@ -1,26 +0,0 @@ -import ICRC7 "mo:icrc7-mo"; - -module { - public let defaultConfig = func(caller : Principal) : ICRC7.InitArgs { - ?{ - symbol = ?"NBL"; - name = ?"NASA Nebulas"; - description = ?"A Collection of Nebulas Captured by NASA"; - logo = ?"https://www.nasa.gov/wp-content/themes/nasa/assets/images/nasa-logo.svg"; - supply_cap = null; - allow_transfers = null; - max_query_batch_size = ?100; - max_update_batch_size = ?100; - default_take_value = ?1000; - max_take_value = ?10000; - max_memo_size = ?512; - permitted_drift = null; - tx_window = null; - burn_account = null; //burned nfts are deleted - deployer = caller; - supported_standards = null; - }; - }; - - public let tokenURI = "https://science.nasa.gov/wp-content/uploads/2023/04/hubble-nebula-helix-nebula-display-1-jpg.webp"; -}; diff --git a/motoko/nft-creator/dfx.json b/motoko/nft-creator/dfx.json deleted file mode 100644 index b2aa16dd9..000000000 --- a/motoko/nft-creator/dfx.json +++ /dev/null @@ -1,51 +0,0 @@ -{ - "canisters": { - "internet_identity": { - "candid": "https://github.com/dfinity/internet-identity/releases/latest/download/internet_identity.did", - "remote": { - "id": { - "ic": "rdmx6-jaaaa-aaaaa-aaadq-cai" - } - }, - "type": "custom", - "specified_id": "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 } } }})" - }, - "backend": { - "main": "backend/app.mo", - "type": "motoko" - }, - "frontend": { - "dependencies": [ - "backend" - ], - "source": [ - "frontend/dist" - ], - "frontend": { - "entrypoint": "frontend/index.html" - }, - "type": "assets" - } - }, - "defaults": { - "build": { - "args": "", - "packtool": "mops sources" - } - }, - "output_env_file": ".env", - "version": 1 -} \ No newline at end of file diff --git a/motoko/nft-creator/frontend/index.html b/motoko/nft-creator/frontend/index.html deleted file mode 100644 index 87dd1c536..000000000 --- a/motoko/nft-creator/frontend/index.html +++ /dev/null @@ -1,14 +0,0 @@ - - - - - - NFT Collection Management Application - - - -
- - - - \ No newline at end of file diff --git a/motoko/nft-creator/frontend/package.json b/motoko/nft-creator/frontend/package.json deleted file mode 100644 index 4daff89f8..000000000 --- a/motoko/nft-creator/frontend/package.json +++ /dev/null @@ -1,29 +0,0 @@ -{ - "name": "frontend", - "private": true, - "version": "0.0.0", - "type": "module", - "scripts": { - "dev": "vite", - "prebuild": "npm i --include=dev && dfx generate", - "build": "vite build" - }, - "dependencies": { - "@icp-sdk/core": "~5.2.0", - "@tailwindcss/vite": "^4.1.11", - "@tanstack/react-query": "^5.83.0", - "@icp-sdk/auth": "~5.0.0", - "ic-use-internet-identity": "^0.7.0", - "lucide-react": "^0.535.0", - "react": "^19.1.1", - "react-dom": "^19.1.1", - "tailwindcss": "^4.1.11" - }, - "devDependencies": { - "@vitejs/plugin-react": "^4.7.0", - "dotenv": "^17.2.1", - "sass": "^1.89.2", - "vite": "^7.0.6", - "vite-plugin-environment": "^1.1.3" - } -} diff --git a/motoko/nft-creator/frontend/public/.ic-assets.json5 b/motoko/nft-creator/frontend/public/.ic-assets.json5 deleted file mode 100644 index c78076e88..000000000 --- a/motoko/nft-creator/frontend/public/.ic-assets.json5 +++ /dev/null @@ -1,27 +0,0 @@ -[ - { - "match": "**/*", - - // Provides a base set of security headers that will work for most dapps. - // Any headers you manually specify will override the headers provided by the policy. - // See 'dfx info security-policy' to see the policy and for advice on how to harden the headers. - // Once you improved the headers for your dapp, set the security policy to "hardened" to disable the warning. - // Options are: "hardened" | "standard" | "disabled". - "security_policy": "hardened", - - "headers": { - "Content-Security-Policy": "default-src 'self';script-src 'self' 'unsafe-eval' 'unsafe-inline';connect-src 'self' http://localhost:* https://icp0.io https://*.icp0.io https://icp-api.io;img-src 'self' data: https://science.nasa.gov;style-src * 'unsafe-inline';style-src-elem * 'unsafe-inline';font-src *;object-src 'none';base-uri 'self';frame-ancestors 'none';form-action 'self';upgrade-insecure-requests;", - // Security: The permissions policy disables all features for security reasons. If your site needs such permissions, activate them. - // To configure permissions go here https://www.permissionspolicy.com/ - // This example updated the clipboard-write permission to allow writing to clipboard - "Permissions-Policy": "accelerometer=(), ambient-light-sensor=(), autoplay=(), battery=(), camera=(), cross-origin-isolated=(), display-capture=(), document-domain=(), encrypted-media=(), execution-while-not-rendered=(), execution-while-out-of-viewport=(), fullscreen=(), geolocation=(), gyroscope=(), keyboard-map=(), magnetometer=(), microphone=(), midi=(), navigation-override=(), payment=(), picture-in-picture=(), publickey-credentials-get=(), screen-wake-lock=(), sync-xhr=(), usb=(), web-share=(), xr-spatial-tracking=(), clipboard-read=(), clipboard-write=(self), gamepad=(), speaker-selection=(), conversion-measurement=(), focus-without-user-activation=(), hid=(), idle-detection=(), interest-cohort=(), serial=(), sync-script=(), trust-token-redemption=(), window-placement=(), vertical-scroll=()", - }, - - // Uncomment to disable the warning about using the - // standard security policy, if you understand the risk - // "disable_security_policy_warning": true, - - // Uncomment to redirect all requests from .raw.icp0.io to .icp0.io - // "allow_raw_access": false - }, -] \ No newline at end of file diff --git a/motoko/nft-creator/frontend/public/favicon.ico b/motoko/nft-creator/frontend/public/favicon.ico deleted file mode 100644 index 338fbf34c..000000000 Binary files a/motoko/nft-creator/frontend/public/favicon.ico and /dev/null differ diff --git a/motoko/nft-creator/frontend/public/logo2.svg b/motoko/nft-creator/frontend/public/logo2.svg deleted file mode 100644 index 74bc67e39..000000000 --- a/motoko/nft-creator/frontend/public/logo2.svg +++ /dev/null @@ -1,37 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/motoko/nft-creator/frontend/src/App.jsx b/motoko/nft-creator/frontend/src/App.jsx deleted file mode 100644 index 64cf2cdbf..000000000 --- a/motoko/nft-creator/frontend/src/App.jsx +++ /dev/null @@ -1,98 +0,0 @@ -import { useInternetIdentity } from "ic-use-internet-identity"; -import { CollectionClaim } from "./components/CollectionClaim"; -import { MintNFT } from "./components/MintNFT"; -import { OwnedNFTs } from "./components/OwnedNFTs"; -import { AuthButton } from "./components/AuthButton"; -import { Heart } from "lucide-react"; - -function App() { - const { identity, isInitializing } = useInternetIdentity(); - - if (isInitializing) { - return ( -
-
-
- ); - } - - return ( -
-
- {/* Header */} -
-

- NFT Collection Manager -

-
- -
-
- - {identity ? ( -
- {/* Collection Management */} -
-

- Collection Management -

- -
- - {/* Minting Section */} -
-

- Mint NFT -

- -
- - {/* Owned NFTs */} -
-

- Your NFTs -

- -
-
- ) : ( -
-
-

- Welcome to NFT Collection Manager -

-

- Please authenticate with Internet Identity to - manage your NFT collection. -

-
- -
-
-
- )} - - {/* Footer */} - -
-
- ); -} - -export default App; diff --git a/motoko/nft-creator/frontend/src/components/AuthButton.jsx b/motoko/nft-creator/frontend/src/components/AuthButton.jsx deleted file mode 100644 index 487bc620b..000000000 --- a/motoko/nft-creator/frontend/src/components/AuthButton.jsx +++ /dev/null @@ -1,78 +0,0 @@ -import { useInternetIdentity } from "ic-use-internet-identity"; -import { LogIn, LogOut, User, Copy, Check } from "lucide-react"; -import { useState } from "react"; -import { useQueryClient } from "@tanstack/react-query"; - -export function AuthButton() { - const { identity, login, clear } = useInternetIdentity(); - const [copied, setCopied] = useState(false); - const queryClient = useQueryClient(); - - const copyPrincipal = async () => { - if (!identity) return; - - try { - const principal = identity.getPrincipal().toString(); - await navigator.clipboard.writeText(principal); - setCopied(true); - setTimeout(() => setCopied(false), 2000); // Reset after 2 seconds - } catch (error) { - console.error("Failed to copy principal:", error); - } - }; - - const handleLogout = async () => { - // Clear all queries before logging out - queryClient.clear(); - // Clear the identity - await clear(); - // Force a page reload to ensure clean state - window.location.reload(); - }; - - if (identity) { - return ( -
- - {copied && ( - - Copied! - - )} - -
- ); - } - - return ( - - ); -} diff --git a/motoko/nft-creator/frontend/src/components/CollectionClaim.jsx b/motoko/nft-creator/frontend/src/components/CollectionClaim.jsx deleted file mode 100644 index 33cb37218..000000000 --- a/motoko/nft-creator/frontend/src/components/CollectionClaim.jsx +++ /dev/null @@ -1,48 +0,0 @@ -import { useCollectionStatus, useClaimCollection } from '../hooks/useQueries'; -import { CheckCircle, Crown } from 'lucide-react'; - -export function CollectionClaim() { - const { data: hasBeenClaimed, isLoading: isCheckingClaim } = useCollectionStatus(); - const { mutate: claimCollection, isPending: isClaiming } = useClaimCollection(); - - if (isCheckingClaim) { - return ( -
-
- Checking collection status... -
- ); - } - - if (hasBeenClaimed) { - return ( -
- - Collection has been claimed -
- ); - } - - return ( -
-

This collection is available to claim!

- -
- ); -} diff --git a/motoko/nft-creator/frontend/src/components/MintNFT.jsx b/motoko/nft-creator/frontend/src/components/MintNFT.jsx deleted file mode 100644 index a8042763e..000000000 --- a/motoko/nft-creator/frontend/src/components/MintNFT.jsx +++ /dev/null @@ -1,99 +0,0 @@ -import { useState } from "react"; -import { useCollectionOwner, useMintNFT } from "../hooks/useQueries"; -import { useInternetIdentity } from "ic-use-internet-identity"; -import { Principal } from "@icp-sdk/core/principal"; -import { Sparkles } from "lucide-react"; -import { useToast } from "../contexts/ToastContext"; - -export function MintNFT() { - const { identity } = useInternetIdentity(); - const { data: collectionOwner, isLoading: isLoadingOwner } = - useCollectionOwner(); - const { mutate: mintNFT, isPending: isMinting } = useMintNFT(); - const { addError } = useToast(); - - const [recipient, setRecipient] = useState(""); - - const isOwner = - identity && - collectionOwner && - identity.getPrincipal().toString() === collectionOwner.toString(); - console.log("Collection Owner:", collectionOwner?.toString()); - - if (isLoadingOwner) { - return ( -
-
- Loading collection owner... -
- ); - } - - if (!collectionOwner) { - return ( -
- Collection must be claimed before minting NFTs. -
- ); - } - - if (!isOwner) { - return ( -
- Only the collection owner can mint NFTs. -
- ); - } - - const handleMint = () => { - if (!recipient) return; - - try { - const recipientPrincipal = Principal.fromText(recipient); - mintNFT({ - to: { owner: recipientPrincipal, subaccount: [] }, // Assuming no subaccount - }); - - // Reset form - setRecipient(""); - } catch (error) { - console.error("Invalid principal:", error); - addError("Invalid principal: " + (error?.message || error)); - } - }; - - return ( -
-
- - setRecipient(e.target.value)} - placeholder="Enter recipient principal" - className="w-full px-3 py-2 text-sm bg-gray-700 border border-gray-600 rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-transparent" - /> -
- - -
- ); -} diff --git a/motoko/nft-creator/frontend/src/components/NFTCard.jsx b/motoko/nft-creator/frontend/src/components/NFTCard.jsx deleted file mode 100644 index c6c7ba882..000000000 --- a/motoko/nft-creator/frontend/src/components/NFTCard.jsx +++ /dev/null @@ -1,100 +0,0 @@ -import { useState } from "react"; -import { useTransferNFT } from "../hooks/useQueries"; -import { Principal } from "@icp-sdk/core/principal"; -import { Send, Image as ImageIcon } from "lucide-react"; -import { useToast } from "../contexts/ToastContext"; - -export function NFTCard({ nft }) { - const [recipient, setRecipient] = useState(""); - const [imageError, setImageError] = useState(false); - const { mutate: transferNFT, isPending: isTransferring } = useTransferNFT(); - const { addError } = useToast(); - - // Helper function to get metadata value by key - const getMetadataValue = (key) => { - const entry = nft.metadata[0]?.find(([k]) => k === key); - if (entry && entry[1] && "Text" in entry[1]) { - return entry[1].Text; - } - return ""; - }; - - const handleTransfer = () => { - if (!recipient) return; - - try { - const recipientPrincipal = Principal.fromText(recipient); - transferNFT({ - tokenId: nft.tokenId, - to: { owner: recipientPrincipal, subaccount: [] }, // Assuming no subaccount - }); - setRecipient(""); - } catch (error) { - console.error("Invalid principal:", error); - addError("Invalid principal: " + (error?.message || error)); - } - }; - - return ( -
- {/* NFT Image */} -
- {!imageError ? ( - {getMetadataValue("name")} setImageError(true)} - /> - ) : ( -
- - Image not available -
- )} -
- - {/* NFT Details */} -
-
-

- {getMetadataValue("name")} -

-

- Token ID: {nft.tokenId.toString()} -

-

- {getMetadataValue("description")} -

-
- - {/* Transfer Form */} -
- -
- setRecipient(e.target.value)} - placeholder="Enter recipient principal" - className="flex-1 px-3 py-2 text-xs sm:text-sm bg-gray-600 border border-gray-500 rounded focus:ring-2 focus:ring-purple-500 focus:border-transparent" - /> - -
-
-
-
- ); -} diff --git a/motoko/nft-creator/frontend/src/components/OwnedNFTs.jsx b/motoko/nft-creator/frontend/src/components/OwnedNFTs.jsx deleted file mode 100644 index fb8b38597..000000000 --- a/motoko/nft-creator/frontend/src/components/OwnedNFTs.jsx +++ /dev/null @@ -1,39 +0,0 @@ -import { useOwnedNFTs } from "../hooks/useQueries"; -import { NFTCard } from "./NFTCard"; - -export function OwnedNFTs() { - const { data: ownedNFTs, isLoading, error } = useOwnedNFTs(); - - if (isLoading) { - return ( -
-
- Loading your NFTs... -
- ); - } - - if (error) { - return ( -
- Error loading NFTs: {error.message} -
- ); - } - - if (!ownedNFTs || ownedNFTs.length === 0) { - return ( -
- You don't own any NFTs yet. -
- ); - } - - return ( -
- {ownedNFTs.map((nft) => ( - - ))} -
- ); -} diff --git a/motoko/nft-creator/frontend/src/contexts/ToastContext.jsx b/motoko/nft-creator/frontend/src/contexts/ToastContext.jsx deleted file mode 100644 index 5c1accb81..000000000 --- a/motoko/nft-creator/frontend/src/contexts/ToastContext.jsx +++ /dev/null @@ -1,97 +0,0 @@ -import { createContext, useContext, useState, useCallback } from "react"; -import { X } from "lucide-react"; - -const ToastContext = createContext(undefined); - -export function useToast() { - const context = useContext(ToastContext); - if (!context) { - throw new Error("useToast must be used within a ToastProvider"); - } - return context; -} - -export function ToastProvider({ children }) { - const [toasts, setToasts] = useState([]); - - const addToast = useCallback((message, type) => { - const id = Date.now().toString(); - const newToast = { id, message, type }; - - setToasts((prev) => [newToast, ...prev]); // Latest first - - // Auto remove after 5 seconds - setTimeout(() => { - removeToast(id); - }, 5000); - }, []); - - const removeToast = useCallback((id) => { - setToasts((prev) => prev.filter((toast) => toast.id !== id)); - }, []); - - const addError = useCallback( - (message) => addToast(message, "error"), - [addToast] - ); - const addSuccess = useCallback( - (message) => addToast(message, "success"), - [addToast] - ); - const addInfo = useCallback( - (message) => addToast(message, "info"), - [addToast] - ); - - return ( - - {children} - - - ); -} - -function ToastContainer({ toasts, onRemove }) { - return ( -
- {toasts.map((toast, index) => ( -
0 ? "opacity-80" : "opacity-100"} - ${toast.type === "error" ? "bg-red-600 text-white" : ""} - ${toast.type === "success" ? "bg-green-600 text-white" : ""} - ${toast.type === "info" ? "bg-blue-600 text-white" : ""} - animate-slide-in - `} - style={{ - transform: `translateY(${index * -8}px)`, - zIndex: 50 - index, - }} - > -
- {toast.message} -
- -
- ))} -
- ); -} diff --git a/motoko/nft-creator/frontend/src/hooks/useActor.js b/motoko/nft-creator/frontend/src/hooks/useActor.js deleted file mode 100644 index 61d4b40d6..000000000 --- a/motoko/nft-creator/frontend/src/hooks/useActor.js +++ /dev/null @@ -1,56 +0,0 @@ -import { useInternetIdentity } from "ic-use-internet-identity"; -import { createActor, canisterId } from "declarations/backend"; -import { useQuery, useQueryClient } from "@tanstack/react-query"; -import { useEffect } from "react"; - -const ACTOR_QUERY_KEY = "actor"; -export function useActor() { - const { identity } = useInternetIdentity(); - const queryClient = useQueryClient(); - - const actorQuery = useQuery({ - queryKey: [ - ACTOR_QUERY_KEY, - identity?.getPrincipal().toString() || "anonymous", - ], - queryFn: async () => { - if (!canisterId) { - throw new Error("Canister ID not available"); - } - - if (!identity) { - // Create anonymous actor - return createActor(canisterId); - } - - // Create authenticated actor - return createActor(canisterId, { - agentOptions: { - identity, - }, - }); - }, - staleTime: Infinity, - enabled: true, - retry: (failureCount, error) => { - console.error("Actor creation failed:", error); - return failureCount < 2; // Retry up to 2 times - }, - }); - - // Clear all dependent queries when identity changes - useEffect(() => { - queryClient.invalidateQueries({ - predicate: (query) => { - return !query.queryKey.includes(ACTOR_QUERY_KEY); - }, - }); - }, [identity?.getPrincipal().toString(), queryClient]); - - return { - actor: actorQuery.data || null, - isFetching: actorQuery.isFetching, - isError: actorQuery.isError, - error: actorQuery.error, - }; -} diff --git a/motoko/nft-creator/frontend/src/hooks/useQueries.js b/motoko/nft-creator/frontend/src/hooks/useQueries.js deleted file mode 100644 index b73e6010d..000000000 --- a/motoko/nft-creator/frontend/src/hooks/useQueries.js +++ /dev/null @@ -1,154 +0,0 @@ -import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"; -import { useInternetIdentity } from "ic-use-internet-identity"; -import { useActor } from "./useActor"; -import { useToast } from "../contexts/ToastContext"; - -// Collection Status -export function useCollectionStatus() { - const { actor, isFetching } = useActor(); - - return useQuery({ - queryKey: ["collectionStatus"], - queryFn: async () => { - if (!actor) return false; - return actor.collectionHasBeenClaimed(); - }, - enabled: !!actor && !isFetching, - }); -} - -// Collection Owner -export function useCollectionOwner() { - const { actor, isFetching } = useActor(); - - return useQuery({ - queryKey: ["collectionOwner"], - queryFn: async () => { - if (!actor) return null; - return actor.getCollectionOwner(); - }, - enabled: !!actor && !isFetching, - }); -} - -// Claim Collection -export function useClaimCollection() { - const queryClient = useQueryClient(); - const { actor } = useActor(); - const { addError, addSuccess } = useToast(); - - return useMutation({ - mutationFn: async () => { - if (!actor) throw new Error("Actor not available"); - return actor.claimCollection(); - }, - onSuccess: () => { - queryClient.invalidateQueries({ queryKey: ["collectionStatus"] }); - queryClient.invalidateQueries({ queryKey: ["collectionOwner"] }); - addSuccess("Collection claimed successfully!"); - }, - onError: (error) => { - addError(`Failed to claim collection: ${error.message}`); - }, - }); -} - -// Mint NFT -export function useMintNFT() { - const queryClient = useQueryClient(); - const { actor } = useActor(); - const { addError, addSuccess } = useToast(); - - return useMutation({ - mutationFn: async ({ to }) => { - if (!actor) throw new Error("Actor not available"); - const result = await actor.mint(to); - if ("err" in result) { - throw new Error(`Mint failed: ${result.err}`); - } - return result; - }, - onSuccess: () => { - queryClient.invalidateQueries({ queryKey: ["ownedNFTs"] }); - addSuccess("NFT minted successfully!"); - }, - onError: (error) => { - addError(`Failed to mint NFT: ${error.message}`); - }, - }); -} - -// Owned NFTs -export function useOwnedNFTs() { - const { identity } = useInternetIdentity(); - const { actor, isFetching } = useActor(); - - return useQuery({ - queryKey: ["ownedNFTs", identity?.getPrincipal().toString()], - queryFn: async () => { - if (!actor || !identity) return []; - - const tokenIds = await actor.icrc7_tokens_of( - { owner: identity.getPrincipal(), subaccount: [] }, - [], - [] - ); - - // Get metadata for all tokens in one call - const metadataArray = await actor.icrc7_token_metadata(tokenIds); - - // Map token IDs to their corresponding metadata (same order) - const nftsWithMetadata = tokenIds.map((tokenId, index) => ({ - tokenId, - metadata: metadataArray[index] || [], - })); - - return nftsWithMetadata; - }, - enabled: !!actor && !!identity && !isFetching, - }); -} - -// Transfer NFT -export function useTransferNFT() { - const queryClient = useQueryClient(); - const { actor } = useActor(); - const { addError, addSuccess } = useToast(); - - return useMutation({ - mutationFn: async ({ tokenId, to }) => { - if (!actor) throw new Error("Actor not available"); - - const transferArg = { - token_id: tokenId, - to, - memo: [], - from_subaccount: [], - created_at_time: [], - }; - - const result = await actor.icrc7_transfer([transferArg]); - const transferResult = result[0]; - - if (!transferResult || transferResult.length === 0) { - throw new Error("Transfer failed: No result returned"); - } - - const actualResult = transferResult[0]; - if ("Err" in actualResult) { - throw new Error( - `Transfer failed: ${JSON.stringify(actualResult.Err)}` - ); - } - - return actualResult.Ok; - }, - onSuccess: () => { - queryClient.invalidateQueries({ queryKey: ["ownedNFTs"] }); - addSuccess("NFT transferred successfully!"); - }, - onError: (error) => { - addError(`Failed to transfer NFT: ${error.message}`); - }, - }); -} diff --git a/motoko/nft-creator/frontend/src/index.css b/motoko/nft-creator/frontend/src/index.css deleted file mode 100644 index bb111446b..000000000 --- a/motoko/nft-creator/frontend/src/index.css +++ /dev/null @@ -1,103 +0,0 @@ -@import "tailwindcss"; - -@layer base { - * { - box-sizing: border-box; - } - - html { - font-family: system-ui, sans-serif; - } - - body { - margin: 0; - padding: 0; - background-color: #111827; - color: #ffffff; - /* Prevent horizontal scroll on mobile */ - overflow-x: hidden; - } -} - -@layer components { - .container { - max-width: 1200px; - } -} - -/* Custom scrollbar */ -::-webkit-scrollbar { - width: 8px; -} - -::-webkit-scrollbar-track { - background: #374151; -} - -::-webkit-scrollbar-thumb { - background: #6b7280; - border-radius: 4px; -} - -::-webkit-scrollbar-thumb:hover { - background: #9ca3af; -} - -/* Loading animation */ -@keyframes spin { - to { - transform: rotate(360deg); - } -} - -.animate-spin { - animation: spin 1s linear infinite; -} - -/* Focus styles */ -input:focus, -textarea:focus, -button:focus { - outline: none; -} - -/* Smooth transitions */ -* { - transition-property: color, background-color, border-color, - text-decoration-color, fill, stroke; - transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); - transition-duration: 150ms; -} - -/* Toast animations */ -@keyframes slide-in { - from { - transform: translateX(-100%); - opacity: 0; - } - to { - transform: translateX(0); - opacity: 1; - } -} - -.animate-slide-in { - animation: slide-in 0.3s ease-out; -} - -/* Mobile-specific utilities */ -@layer utilities { - .line-clamp-2 { - display: -webkit-box; - -webkit-line-clamp: 2; - -webkit-box-orient: vertical; - overflow: hidden; - } - - /* Ensure buttons are touch-friendly on mobile */ - @media (max-width: 640px) { - button { - min-height: 44px; /* Apple's recommended minimum touch target */ - } - } -} diff --git a/motoko/nft-creator/frontend/src/main.jsx b/motoko/nft-creator/frontend/src/main.jsx deleted file mode 100644 index 0e9b402cf..000000000 --- a/motoko/nft-creator/frontend/src/main.jsx +++ /dev/null @@ -1,39 +0,0 @@ -import ReactDOM from "react-dom/client"; -import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; -import { InternetIdentityProvider } from "ic-use-internet-identity"; -import { ToastProvider } from "./contexts/ToastContext"; -import App from "./App"; -import "./index.css"; - -const queryClient = new QueryClient({ - defaultOptions: { - queries: { - retry: (failureCount, error) => { - // Don't retry on authentication errors - if ( - error?.message?.includes("Unauthorized") || - error?.message?.includes("identity") - ) { - return false; - } - return failureCount < 2; - }, - staleTime: 30 * 1000, // 30 seconds - refetchOnWindowFocus: false, - }, - }, -}); - -ReactDOM.createRoot(document.getElementById("root")).render( - - - - - - - -); diff --git a/motoko/nft-creator/frontend/vite.config.js b/motoko/nft-creator/frontend/vite.config.js deleted file mode 100644 index 0cf729504..000000000 --- a/motoko/nft-creator/frontend/vite.config.js +++ /dev/null @@ -1,53 +0,0 @@ -import react from "@vitejs/plugin-react"; -import { defineConfig } from "vite"; -import { fileURLToPath, URL } from "url"; -import environment from "vite-plugin-environment"; -import tailwindcss from "@tailwindcss/vite"; -import dotenv from "dotenv"; -import path from "path"; - -// Load from project root -dotenv.config({ path: path.resolve(__dirname, "../.env") }); - -process.env.II_URL = - process.env.DFX_NETWORK === "local" - ? `http://uqzsh-gqaaa-aaaaq-qaada-cai.localhost:4943/` - : `https://id.ai/`; - -export default defineConfig({ - base: "./", - plugins: [ - react(), - environment("all", { prefix: "CANISTER_" }), - environment("all", { prefix: "DFX_" }), - environment(["II_URL"]), - tailwindcss(), - ], - envDir: "../", - 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/motoko/nft-creator/mops.toml b/motoko/nft-creator/mops.toml deleted file mode 100644 index ff2cad21f..000000000 --- a/motoko/nft-creator/mops.toml +++ /dev/null @@ -1,20 +0,0 @@ -# Motoko dependencies (https://mops.one/) - -# NOTE: Pinned to moc 1.2.0 (latest version compatible with icrc7-mo@0.5.0). -# icrc7-mo@0.5.0 transitively depends on motoko-base@0.7.3, whose -# ExperimentalCycles.mo is incompatible with moc >= 1.3.0 (Cycles.accept -# return type changed). Once icrc7-mo migrates to mo:core, bump to moc = "1.5.1". -[toolchain] -moc = "1.2.0" - -[dependencies] -core = "2.4.0" -icrc7-mo = "0.5.0" -class-plus = "0.0.1" -vector = "0.2.0" - -[moc] -# M0236: use context dot notation (e.g. map.get(k) instead of Map.get(map, compare, k)) -# 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"] diff --git a/motoko/nft-creator/package.json b/motoko/nft-creator/package.json deleted file mode 100644 index 4a81e4d3a..000000000 --- a/motoko/nft-creator/package.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "engines": { - "node": ">=16.0.0", - "npm": ">=7.0.0" - }, - "name": "motoko-icp-ninja-template", - "scripts": { - "build": "npm run build --workspaces --if-present", - "prebuild": "npm run prebuild --workspaces --if-present", - "dev": "npm run dev --workspaces --if-present" - }, - "type": "module", - "workspaces": [ - "frontend" - ] -} \ No newline at end of file diff --git a/motoko/token_transfer/.gitignore b/motoko/token_transfer/.gitignore deleted file mode 100644 index 49c89a1c9..000000000 --- a/motoko/token_transfer/.gitignore +++ /dev/null @@ -1,25 +0,0 @@ -# Various IDEs and Editors -.vscode/ -.idea/ -**/*~ - -# Mac OSX temporary files -.DS_Store -**/.DS_Store - -# dfx temporary files -.dfx/ - -# generated files -**/declarations/ - -# rust -target/ - -# frontend code -node_modules/ -dist/ -.svelte-kit/ - -# environment variables -.env diff --git a/motoko/token_transfer/README.md b/motoko/token_transfer/README.md deleted file mode 100644 index a689a7039..000000000 --- a/motoko/token_transfer/README.md +++ /dev/null @@ -1,265 +0,0 @@ -# Token transfer - -Token transfer is a canister that can transfer ICRC-1 tokens from its account to other accounts. It is an example of a canister that uses an ICRC-1 ledger canister. Sample code is available in [Motoko](https://github.com/dfinity/examples/tree/master/motoko/token_transfer) and [Rust](https://github.com/dfinity/examples/tree/master/rust/token_transfer). - -## Architecture - -The sample code revolves around one core transfer function which takes as input the amount of tokens to transfer, the `Account` to which to transfer tokens and returns either success or an error in case e.g. the token transfer canister doesn’t have enough tokens to do the transfer. In case of success, a unique identifier of the transaction is returned. - -This sample will use the Motoko variant. - -## Prerequisites -This example requires an installation of: - -- [x] Install the [IC SDK](https://internetcomputer.org/docs/current/developer-docs/setup/install/index.mdx). - -Begin by opening a terminal window. - -## Step 1: Setup the project environment - -Start a local instance of the Internet Computer and create a new project with the commands: - -```bash -dfx start --background -dfx new --type=motoko token_transfer --no-frontend -cd token_transfer -``` - -## Step 2: Determine ICRC-1 ledger file locations - -> [!TIP] -> [Learn more about how to setup the ICRC-1 ledger locally](https://internetcomputer.org/docs/current/developer-docs/defi/icrc-1/icrc1-ledger-setup) - -Go to the [releases overview](https://dashboard.internetcomputer.org/releases) and copy the latest replica binary revision. - -The URL for the ledger Wasm module is `https://download.dfinity.systems/ic//canisters/ic-icrc1-ledger.wasm.gz`. - -The URL for the ledger .did file is `https://raw.githubusercontent.com/dfinity/ic//rs/rosetta-api/icrc1/ledger/ledger.did`. - -**OPTIONAL:** -If you want to make sure, you have the latest ICRC-1 ledger files you can run the following script. - -```sh -curl -o download_latest_icrc1_ledger.sh "https://raw.githubusercontent.com/dfinity/ic/69988ae40e4cc0db7ef758da7dd5c0606075e926/rs/rosetta-api/scripts/download_latest_icrc1_ledger.sh" -chmod +x download_latest_icrc1_ledger.sh -./download_latest_icrc1_ledger.sh -``` - -## Step 3: Configure the `dfx.json` file to use the ledger - -Replace its contents with this but adapt the URLs to be the ones you determined in step 2: - -```json -{ - "canisters": { - "token_transfer_backend": { - "main": "src/token_transfer_backend/main.mo", - "type": "motoko", - "dependencies": ["icrc1_ledger_canister"] - }, - "icrc1_ledger_canister": { - "type": "custom", - "candid": "https://raw.githubusercontent.com/dfinity/ic//rs/rosetta-api/icrc1/ledger/ledger.did", - "wasm": "https://download.dfinity.systems/ic//canisters/ic-icrc1-ledger.wasm.gz" - } - }, - "defaults": { - "build": { - "args": "", - "packtool": "mops sources" - } - }, - "output_env_file": ".env", - "version": 1 -} -``` - -If you chose to download the ICRC-1 ledger files with the script, you need to replace the Candid and Wasm file entries: - -```json -... -"candid": icrc1_ledger.did, -"wasm" : icrc1_ledger.wasm.gz, - ... -``` - -## Step 4: Use the anonymous identity as the minting account - -```bash -export MINTER=$(dfx --identity anonymous identity get-principal) -``` - -> [!TIP] -> Transfers from the minting account will create Mint transactions. Transfers to the minting account will create Burn transactions. - - -## Step 5: Record your default identity's principal to mint an initial balance to when deploying the ledger - -```bash -export DEFAULT=$(dfx identity get-principal) -``` - -## Step 6: Deploy the ICRC-1 ledger locally - -Take a moment to read the details of the call made below. Not only are you deploying an ICRC-1 ledger canister, you are also: - -- Setting the minting account to the anonymous principal you saved in a previous step (`MINTER`) -- Minting 100 tokens to the DEFAULT principal -- Setting the transfer fee to 0.0001 tokens -- Naming the token Local ICRC1 / L-ICRC1 - -```bash -dfx deploy icrc1_ledger_canister --argument "(variant { Init = -record { - token_symbol = \"ICRC1\"; - token_name = \"L-ICRC1\"; - minting_account = record { owner = principal \"${MINTER}\" }; - transfer_fee = 10_000; - metadata = vec {}; - initial_balances = vec { record { record { owner = principal \"${DEFAULT}\"; }; 10_000_000_000; }; }; - archive_options = record { - num_blocks_to_archive = 1000; - trigger_threshold = 2000; - controller_id = principal \"${MINTER}\"; - }; - } -})" -``` - -If successful, the output should be: - -```bash -Deployed canisters. -URLs: - Backend canister via Candid interface: - icrc1_ledger_canister: http://127.0.0.1:4943/?canisterId=bnz7o-iuaaa-aaaaa-qaaaa-cai&id=mxzaz-hqaaa-aaaar-qaada-cai -``` - -## Step 7: Verify that the ledger canister is healthy and working as expected by using the command - -> [!TIP] -> [Learn more about how to interact with the ICRC-1 ledger](https://internetcomputer.org/docs/current/developer-docs/defi/icrc-1/using-icrc1-ledger#icrc-1-and-icrc-1-extension-endpoints). - -````bash -dfx canister call icrc1_ledger_canister icrc1_balance_of "(record { - owner = principal \"${DEFAULT}\"; - } -)" -``` - -The output should be: - -```bash -(10_000_000_000 : nat) -```` - -## Step 8: Prepare the token transfer canister - -Replace the contents of the `src/token_transfer_backend/main.mo` file with the following: - -```motoko -import Icrc1Ledger "canister:icrc1_ledger_canister"; -import Debug "mo:core/Debug"; -import Result "mo:core/Result"; -import Error "mo:core/Error"; - -persistent actor { - - type TransferArgs = { - amount : Nat; - toAccount : Icrc1Ledger.Account; - }; - - public shared func transfer(args : TransferArgs) : async Result.Result { - Debug.print( - "Transferring " - # debug_show (args.amount) - # " tokens to account" - # debug_show (args.toAccount) - ); - - let transferArgs : Icrc1Ledger.TransferArg = { - // can be used to distinguish between transactions - memo = null; - // the amount we want to transfer - amount = args.amount; - // we want to transfer tokens from the default subaccount of the canister - from_subaccount = null; - // if not specified, the default fee for the canister is used - fee = null; - // the account we want to transfer tokens to - to = args.toAccount; - // a timestamp indicating when the transaction was created by the caller; if it is not specified by the caller then this is set to the current ICP time - created_at_time = null; - }; - - try { - // initiate the transfer - let transferResult = await Icrc1Ledger.icrc1_transfer(transferArgs); - - // check if the transfer was successfull - switch (transferResult) { - case (#Err(transferError)) { - return #err("Couldn't transfer funds:\n" # debug_show (transferError)); - }; - case (#Ok(blockIndex)) { return #ok blockIndex }; - }; - } catch (error : Error) { - // catch any errors that might occur during the transfer - return #err("Reject message: " # error.message()); - }; - }; -}; - -``` - -## Step 9: Deploy the token transfer canister - -```bash -dfx deploy token_transfer_backend -``` - -## Step 10: Transfer funds to your canister - -> [!TIP] -> Make sure that you are using the default `dfx` account that we minted tokens to in step 6 for the following steps. - -Make the following call to transfer 10 tokens to the canister: - -```bash -dfx canister call icrc1_ledger_canister icrc1_transfer "(record { - to = record { - owner = principal \"$(dfx canister id token_transfer_backend)\"; - }; - amount = 1_000_000_000; -})" -``` - -If successful, the output should be: - -```bash -(variant { Ok = 1 : nat }) -``` - -## Step 11: Transfer funds from the canister - -Now that the canister owns tokens on the ledger, you can transfer 1 token from the canister to another account, in this case back to the default account: - -```bash -dfx canister call token_transfer_backend transfer "(record { - amount = 100_000_000; - toAccount = record { - owner = principal \"$(dfx identity get-principal)\"; - }; -})" -``` - -## Security considerations and best practices - -If you base your application on this example, 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. This example may not implement all the best practices. - -For example, the following aspects are particularly relevant for this app: - -- [Inter-canister calls and rollbacks](https://internetcomputer.org/docs/current/developer-docs/security/security-best-practices/overview), since issues around inter-canister calls (here the ledger) can e.g. lead to time-of-check time-of-use or double spending security bugs. -- [Certify query responses if they are relevant for security](https://internetcomputer.org/docs/current/references/security/general-security-best-practices#certify-query-responses-if-they-are-relevant-for-security), since this is essential when e.g. displaying important financial data in the frontend that may be used by users to decide on future transactions. In this example, this is e.g. relevant for the call to `canisterBalance`. -- [Use a decentralized governance system like SNS to make a canister have a decentralized controller](https://internetcomputer.org/docs/current/developer-docs/security/security-best-practices/overview), since decentralizing control is a fundamental aspect of decentralized finance applications. diff --git a/motoko/token_transfer/demo.sh b/motoko/token_transfer/demo.sh deleted file mode 100755 index 55d3ab6a5..000000000 --- a/motoko/token_transfer/demo.sh +++ /dev/null @@ -1,47 +0,0 @@ -#!/usr/bin/env bash -dfx stop -set -e -trap 'dfx stop' EXIT - -echo "===========SETUP=========" -dfx start --background --clean -export MINTER=$(dfx --identity anonymous identity get-principal) -export DEFAULT=$(dfx identity get-principal) -dfx deploy icrc1_ledger_canister --argument "(variant { Init = -record { - token_symbol = \"ICRC1\"; - token_name = \"L-ICRC1\"; - minting_account = record { owner = principal \"${MINTER}\" }; - transfer_fee = 10_000; - metadata = vec {}; - initial_balances = vec { record { record { owner = principal \"${DEFAULT}\"; }; 10_000_000_000; }; }; - archive_options = record { - num_blocks_to_archive = 1000; - trigger_threshold = 2000; - controller_id = principal \"${MINTER}\"; - }; - } -})" -dfx canister call icrc1_ledger_canister icrc1_balance_of "(record { - owner = principal \"${DEFAULT}\"; - } -)" -echo "===========SETUP DONE=========" - -dfx deploy token_transfer_backend - -dfx canister call icrc1_ledger_canister icrc1_transfer "(record { - to = record { - owner = principal \"$(dfx canister id token_transfer_backend)\"; - }; - amount = 1_000_000_000; -})" - -dfx canister call token_transfer_backend transfer "(record { - amount = 100_000_000; - toAccount = record { - owner = principal \"$(dfx identity get-principal)\"; - }; -})" - -echo "DONE" \ No newline at end of file diff --git a/motoko/token_transfer/dfx.json b/motoko/token_transfer/dfx.json deleted file mode 100644 index 29c3d11d3..000000000 --- a/motoko/token_transfer/dfx.json +++ /dev/null @@ -1,24 +0,0 @@ -{ - "canisters": { - "token_transfer_backend": { - "main": "src/token_transfer_backend/main.mo", - "type": "motoko", - "dependencies": [ - "icrc1_ledger_canister" - ] - }, - "icrc1_ledger_canister": { - "type": "custom", - "candid": "https://raw.githubusercontent.com/dfinity/ic/d87954601e4b22972899e9957e800406a0a6b929/rs/rosetta-api/icrc1/ledger/ledger.did", - "wasm": "https://download.dfinity.systems/ic/d87954601e4b22972899e9957e800406a0a6b929/canisters/ic-icrc1-ledger.wasm.gz" - } - }, - "defaults": { - "build": { - "args": "", - "packtool": "mops sources" - } - }, - "output_env_file": ".env", - "version": 1 -} \ No newline at end of file diff --git a/motoko/token_transfer/mops.toml b/motoko/token_transfer/mops.toml deleted file mode 100644 index dc89d0891..000000000 --- a/motoko/token_transfer/mops.toml +++ /dev/null @@ -1,11 +0,0 @@ -[toolchain] -moc = "1.5.1" - -[dependencies] -core = "2.4.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"] diff --git a/motoko/token_transfer/src/token_transfer_backend/main.mo b/motoko/token_transfer/src/token_transfer_backend/main.mo deleted file mode 100644 index ec970f095..000000000 --- a/motoko/token_transfer/src/token_transfer_backend/main.mo +++ /dev/null @@ -1,52 +0,0 @@ -import Icrc1Ledger "canister:icrc1_ledger_canister"; -import Debug "mo:core/Debug"; -import Result "mo:core/Result"; -import Error "mo:core/Error"; - -persistent actor { - - type TransferArgs = { - amount : Nat; - toAccount : Icrc1Ledger.Account; - }; - - public shared func transfer(args : TransferArgs) : async Result.Result { - Debug.print( - "Transferring " - # debug_show (args.amount) - # " tokens to account" - # debug_show (args.toAccount) - ); - - let transferArgs : Icrc1Ledger.TransferArg = { - // can be used to distinguish between transactions - memo = null; - // the amount we want to transfer - amount = args.amount; - // we want to transfer tokens from the default subaccount of the canister - from_subaccount = null; - // if not specified, the default fee for the canister is used - fee = null; - // the account we want to transfer tokens to - to = args.toAccount; - // a timestamp indicating when the transaction was created by the caller; if it is not specified by the caller then this is set to the current ICP time - created_at_time = null; - }; - - try { - // initiate the transfer - let transferResult = await Icrc1Ledger.icrc1_transfer(transferArgs); - - // check if the transfer was successfull - switch (transferResult) { - case (#Err(transferError)) { - return #err("Couldn't transfer funds:\n" # debug_show (transferError)); - }; - case (#Ok(blockIndex)) { return #ok blockIndex }; - }; - } catch (error : Error) { - // catch any errors that might occur during the transfer - return #err("Reject message: " # error.message()); - }; - }; -}; diff --git a/motoko/token_transfer_from/.gitignore b/motoko/token_transfer_from/.gitignore deleted file mode 100644 index 49c89a1c9..000000000 --- a/motoko/token_transfer_from/.gitignore +++ /dev/null @@ -1,25 +0,0 @@ -# Various IDEs and Editors -.vscode/ -.idea/ -**/*~ - -# Mac OSX temporary files -.DS_Store -**/.DS_Store - -# dfx temporary files -.dfx/ - -# generated files -**/declarations/ - -# rust -target/ - -# frontend code -node_modules/ -dist/ -.svelte-kit/ - -# environment variables -.env diff --git a/motoko/token_transfer_from/README.md b/motoko/token_transfer_from/README.md deleted file mode 100644 index 23409bd77..000000000 --- a/motoko/token_transfer_from/README.md +++ /dev/null @@ -1,289 +0,0 @@ -# Token transfer_from - -`token_transfer_from_backend` is a canister that can transfer ICRC-1 tokens on behalf of accounts to other accounts. It is an example of a canister that uses an ICRC-1 ledger canister that supports the [ICRC-2](https://github.com/dfinity/ICRC-1/tree/main/standards/ICRC-2) approve and transfer from standard. Sample code is available in [Motoko](https://github.com/dfinity/examples/tree/master/motoko/token_transfer_from) and [Rust](https://github.com/dfinity/examples/tree/master/rust/token_transfer_from). - -## Architecture - -The sample code revolves around one core transfer function which takes as input the amount of tokens to transfer, the `Account` to which to transfer tokens and returns either success or an error in case e.g. the token transfer canister doesn’t have enough tokens to do the transfer or the caller has not approved the canister to spend their tokens. In case of success, a unique identifier of the transaction is returned. The example code assumes the caller of `transfer` has already approved the token transfer canister to spend their tokens. - -This sample will use the Rust variant. - -## Prerequisites -This example requires an installation of: - -- [x] Install the [IC SDK](https://internetcomputer.org/docs/current/developer-docs/setup/install/index.mdx). - -Begin by opening a terminal window. - -## Step 1: Setup the project environment - -Start a local instance of the Internet Computer and create a new project with the commands: - -```bash -dfx start --background -dfx new --type=motoko token_transfer --no-frontend -cd token_transfer -``` - -## Step 2: Determine ICRC-1 ledger file locations - -> [!TIP] -> [Learn more about how to setup the ICRC-1 ledger locally](https://internetcomputer.org/docs/current/developer-docs/defi/icrc-1/icrc1-ledger-setup) - -Go to the [releases overview](https://dashboard.internetcomputer.org/releases) and copy the latest replica binary revision. - -The URL for the ledger Wasm module is `https://download.dfinity.systems/ic//canisters/ic-icrc1-ledger.wasm.gz`. - -The URL for the ledger .did file is `https://raw.githubusercontent.com/dfinity/ic//rs/rosetta-api/icrc1/ledger/ledger.did`. - -**OPTIONAL:** -If you want to make sure, you have the latest ICRC-1 ledger files you can run the following script. - -```sh -curl -o download_latest_icrc1_ledger.sh "https://raw.githubusercontent.com/dfinity/ic/69988ae40e4cc0db7ef758da7dd5c0606075e926/rs/rosetta-api/scripts/download_latest_icrc1_ledger.sh" -chmod +x download_latest_icrc1_ledger.sh -./download_latest_icrc1_ledger.sh -``` - -## Step 3: Configure the `dfx.json` file to use the ledger - -Replace its contents with this but adapt the URLs to be the ones you determined in step 2: - -```json -{ - "canisters": { - "token_transfer_from_backend": { - "main": "src/token_transfer_from_backend/main.mo", - "type": "motoko", - "dependencies": ["icrc1_ledger_canister"] - }, - "icrc1_ledger_canister": { - "type": "custom", - "candid": "https://raw.githubusercontent.com/dfinity/ic//rs/rosetta-api/icrc1/ledger/ledger.did", - "wasm": "https://download.dfinity.systems/ic//canisters/ic-icrc1-ledger.wasm.gz" - } - }, - "defaults": { - "build": { - "args": "", - "packtool": "mops sources" - } - }, - "output_env_file": ".env", - "version": 1 -} -``` - -If you chose to download the ICRC-1 ledger files with the script, you need to replace the Candid and Wasm file entries: - -```json -... -"candid": icrc1_ledger.did, -"wasm" : icrc1_ledger.wasm.gz, - ... -``` - -## Step 4: Use the anonymous identity as the minting account - -```bash -export MINTER=$(dfx --identity anonymous identity get-principal) -``` - -> [!TIP] -> Transfers from the minting account will create Mint transactions. Transfers to the minting account will create Burn transactions. - - -## Step 5: Record your default identity's principal to mint an initial balance to when deploying the ledger - -```bash -export DEFAULT=$(dfx identity get-principal) -``` - -## Step 6: Deploy the ICRC-1 ledger locally - - -Take a moment to read the details of the call made below. Not only are you deploying an ICRC-1 ledger canister, you are also: - -- Setting the minting account to the anonymous principal (`2vxsx-fae`) -- Minting 100 tokens to the default identity -- Setting the transfer fee to 0.0001 tokens -- Naming the token Local ICRC1 / L-ICRC1 -- Enabling the ICRC-2 standard for the ledger - -```bash -dfx deploy icrc1_ledger_canister --argument "(variant { - Init = record { - token_symbol = \"ICRC1\"; - token_name = \"L-ICRC1\"; - minting_account = record { - owner = principal \"$(dfx identity --identity anonymous get-principal)\" - }; - transfer_fee = 10_000; - metadata = vec {}; - initial_balances = vec { - record { - record { - owner = principal \"$(dfx identity --identity default get-principal)\"; - }; - 10_000_000_000; - }; - }; - archive_options = record { - num_blocks_to_archive = 1000; - trigger_threshold = 2000; - controller_id = principal \"$(dfx identity --identity anonymous get-principal)\"; - }; - feature_flags = opt record { - icrc2 = true; - }; - } -})" -``` - -If successful, the output should be: - -```bash -Deployed canisters. -URLs: - Backend canister via Candid interface: - icrc1_ledger_canister: http://127.0.0.1:4943/?canisterId=bnz7o-iuaaa-aaaaa-qaaaa-cai&id=mxzaz-hqaaa-aaaar-qaada-cai -``` - -## Step 7: Verify that the ledger canister is healthy and working as expected by using the command - -> [!TIP] -> [Learn more about how to interact with the ICRC-1 ledger](https://internetcomputer.org/docs/current/developer-docs/defi/icrc-1/using-icrc1-ledger#icrc-1-and-icrc-1-extension-endpoints). - -````bash -dfx canister call icrc1_ledger_canister icrc1_balance_of "(record { - owner = principal \"${DEFAULT}\"; - } -)" -``` - -The output should be: - -```bash -(10_000_000_000 : nat) -```` - -## Step 8: Prepare the token transfer canister - -Replace the contents of the `src/token_transfer_from_backend/main.mo` file with the following: - -```motoko -import Icrc1Ledger "canister:icrc1_ledger_canister"; -import Debug "mo:core/Debug"; -import Result "mo:core/Result"; -import Error "mo:core/Error"; - -persistent actor { - - type TransferArgs = { - amount : Nat; - toAccount : Icrc1Ledger.Account; - }; - - public shared ({ caller }) func transfer(args : TransferArgs) : async Result.Result { - Debug.print( - "Transferring " - # debug_show (args.amount) - # " tokens to account" - # debug_show (args.toAccount) - ); - - let transferFromArgs : Icrc1Ledger.TransferFromArgs = { - // the account we want to transfer tokens from (in this case we assume the caller approved the canister to spend funds on their behalf) - from = { - owner = caller; - subaccount = null; - }; - // can be used to distinguish between transactions - memo = null; - // the amount we want to transfer - amount = args.amount; - // the subaccount we want to spend the tokens from (in this case we assume the default subaccount has been approved) - spender_subaccount = null; - // if not specified, the default fee for the canister is used - fee = null; - // we take the principal and subaccount from the arguments and convert them into an account identifier - to = args.toAccount; - // a timestamp indicating when the transaction was created by the caller; if it is not specified by the caller then this is set to the current ICP time - created_at_time = null; - }; - - try { - // initiate the transfer - let transferFromResult = await Icrc1Ledger.icrc2_transfer_from(transferFromArgs); - - // check if the transfer was successful - switch (transferFromResult) { - case (#Err(transferError)) { - return #err("Couldn't transfer funds:\n" # debug_show (transferError)); - }; - case (#Ok(blockIndex)) { return #ok blockIndex }; - }; - } catch (error : Error) { - // catch any errors that might occur during the transfer - return #err("Reject message: " # error.message()); - }; - }; -}; - -``` - -## Step 9: Deploy the token transfer canister - -```bash -dfx deploy token_transfer_from_backend -``` - -## Step 10: Approve the canister to transfer funds on behalf of the user - -:::info - -Make sure that you are using the default `dfx` account that we minted tokens to in step 6 for the following steps. - -::: - -Make the following call to approve the `token_transfer_from_backend` canister to transfer 100 tokens on behalf of the `default` identity: - -```bash -dfx canister call --identity default icrc1_ledger_canister icrc2_approve "( - record { - spender= record { - owner = principal \"$(dfx canister id token_transfer_from_backend)\"; - }; - amount = 10_000_000_000: nat; - } -)" -``` - -If successful, the output should be: - -```bash -(variant { Ok = 1 : nat }) -``` - -## Step 11: Let the canister transfer funds on behalf of the user - -Now that the canister has an approval for the `default` identities tokens on the ledger, the canister can transfer 1 token on behalf of the `default` identity to another account, in this case to the canisters own account. - -```bash -dfx canister call token_transfer_from_backend transfer "(record { - amount = 100_000_000; - toAccount = record { - owner = principal \"$(dfx canister id token_transfer_from_backend)\"; - }; -})" -``` - -## Security considerations and best practices - -If you base your application on this example, 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. This example may not implement all the best practices. - -For example, the following aspects are particularly relevant for this app: - -- [Inter-canister calls and rollbacks](https://internetcomputer.org/docs/current/developer-docs/security/security-best-practices/overview), since issues around inter-canister calls (here the ledger) can e.g. lead to time-of-check time-of-use or double spending security bugs. -- [Certify query responses if they are relevant for security](https://internetcomputer.org/docs/current/references/security/general-security-best-practices#certify-query-responses-if-they-are-relevant-for-security), since this is essential when e.g. displaying important financial data in the frontend that may be used by users to decide on future transactions. -- [Use a decentralized governance system like SNS to make a canister have a decentralized controller](https://internetcomputer.org/docs/current/developer-docs/security/security-best-practices/overview), since decentralizing control is a fundamental aspect of decentralized finance applications. diff --git a/motoko/token_transfer_from/demo.sh b/motoko/token_transfer_from/demo.sh deleted file mode 100755 index 84e4e0c0e..000000000 --- a/motoko/token_transfer_from/demo.sh +++ /dev/null @@ -1,59 +0,0 @@ -#!/usr/bin/env bash -dfx stop -set -e -trap 'dfx stop' EXIT - -echo "===========SETUP=========" -dfx start --background --clean -dfx deploy icrc1_ledger_canister --argument "(variant { - Init = record { - token_symbol = \"ICRC1\"; - token_name = \"L-ICRC1\"; - minting_account = record { - owner = principal \"$(dfx identity --identity anonymous get-principal)\" - }; - transfer_fee = 10_000; - metadata = vec {}; - initial_balances = vec { - record { - record { - owner = principal \"$(dfx identity --identity default get-principal)\"; - }; - 10_000_000_000; - }; - }; - archive_options = record { - num_blocks_to_archive = 1000; - trigger_threshold = 2000; - controller_id = principal \"$(dfx identity --identity anonymous get-principal)\"; - }; - feature_flags = opt record { - icrc2 = true; - }; - } -})" -dfx canister call icrc1_ledger_canister icrc1_balance_of "(record { - owner = principal \"$(dfx identity --identity default get-principal)\"; -})" -echo "===========SETUP DONE=========" - -dfx deploy token_transfer_from_backend - -# approve the token_transfer_from_backend canister to spend 100 tokens -dfx canister call --identity default icrc1_ledger_canister icrc2_approve "( - record { - spender= record { - owner = principal \"$(dfx canister id token_transfer_from_backend)\"; - }; - amount = 10_000_000_000: nat; - } -)" - -dfx canister call token_transfer_from_backend transfer "(record { - amount = 100_000_000; - toAccount = record { - owner = principal \"$(dfx canister id token_transfer_from_backend)\"; - }; -})" - -echo "DONE" \ No newline at end of file diff --git a/motoko/token_transfer_from/dfx.json b/motoko/token_transfer_from/dfx.json deleted file mode 100644 index 4855ce463..000000000 --- a/motoko/token_transfer_from/dfx.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "canisters": { - "token_transfer_from_backend": { - "main": "src/token_transfer_from_backend/main.mo", - "type": "motoko", - "dependencies": ["icrc1_ledger_canister"] - }, - "icrc1_ledger_canister": { - "type": "custom", - "candid": "https://raw.githubusercontent.com/dfinity/ic/d87954601e4b22972899e9957e800406a0a6b929/rs/rosetta-api/icrc1/ledger/ledger.did", - "wasm": "https://download.dfinity.systems/ic/d87954601e4b22972899e9957e800406a0a6b929/canisters/ic-icrc1-ledger.wasm.gz", - "specified_id": "mxzaz-hqaaa-aaaar-qaada-cai" - } - }, - "defaults": { - "build": { - "args": "", - "packtool": "mops sources" - } - }, - "output_env_file": ".env", - "version": 1 -} \ No newline at end of file diff --git a/motoko/token_transfer_from/mops.toml b/motoko/token_transfer_from/mops.toml deleted file mode 100644 index dc89d0891..000000000 --- a/motoko/token_transfer_from/mops.toml +++ /dev/null @@ -1,11 +0,0 @@ -[toolchain] -moc = "1.5.1" - -[dependencies] -core = "2.4.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"] diff --git a/motoko/token_transfer_from/src/token_transfer_from_backend/main.mo b/motoko/token_transfer_from/src/token_transfer_from_backend/main.mo deleted file mode 100644 index f5e252404..000000000 --- a/motoko/token_transfer_from/src/token_transfer_from_backend/main.mo +++ /dev/null @@ -1,57 +0,0 @@ -import Icrc1Ledger "canister:icrc1_ledger_canister"; -import Debug "mo:core/Debug"; -import Result "mo:core/Result"; -import Error "mo:core/Error"; - -persistent actor { - - type TransferArgs = { - amount : Nat; - toAccount : Icrc1Ledger.Account; - }; - - public shared ({ caller }) func transfer(args : TransferArgs) : async Result.Result { - Debug.print( - "Transferring " - # debug_show (args.amount) - # " tokens to account" - # debug_show (args.toAccount) - ); - - let transferFromArgs : Icrc1Ledger.TransferFromArgs = { - // the account we want to transfer tokens from (in this case we assume the caller approved the canister to spend funds on their behalf) - from = { - owner = caller; - subaccount = null; - }; - // can be used to distinguish between transactions - memo = null; - // the amount we want to transfer - amount = args.amount; - // the subaccount we want to spend the tokens from (in this case we assume the default subaccount has been approved) - spender_subaccount = null; - // if not specified, the default fee for the canister is used - fee = null; - // we take the principal and subaccount from the arguments and convert them into an account identifier - to = args.toAccount; - // a timestamp indicating when the transaction was created by the caller; if it is not specified by the caller then this is set to the current ICP time - created_at_time = null; - }; - - try { - // initiate the transfer - let transferFromResult = await Icrc1Ledger.icrc2_transfer_from(transferFromArgs); - - // check if the transfer was successfull - switch (transferFromResult) { - case (#Err(transferError)) { - return #err("Couldn't transfer funds:\n" # debug_show (transferError)); - }; - case (#Ok(blockIndex)) { return #ok blockIndex }; - }; - } catch (error : Error) { - // catch any errors that might occur during the transfer - return #err("Reject message: " # error.message()); - }; - }; -}; diff --git a/motoko/tokenmania/.devcontainer/devcontainer.json b/motoko/tokenmania/.devcontainer/devcontainer.json deleted file mode 100644 index ebb0b8bcc..000000000 --- a/motoko/tokenmania/.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/tokenmania/BUILD.md b/motoko/tokenmania/BUILD.md deleted file mode 100644 index 24cfcb754..000000000 --- a/motoko/tokenmania/BUILD.md +++ /dev/null @@ -1,113 +0,0 @@ -# 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. - -To migrate your ICP Ninja project off of the web browser and develop it locally, follow these steps. - -### 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 - -``` - -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). - -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). - -### 6. Deploy to the mainnet. - -Once you have cycles, run the command: - -``` - -dfx deploy --network ic - -``` - -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/tokenmania/README.md b/motoko/tokenmania/README.md deleted file mode 100644 index c9aea4ff2..000000000 --- a/motoko/tokenmania/README.md +++ /dev/null @@ -1,33 +0,0 @@ -# Tokenmania! - -Tokenmania is a simplified token minting application. When the application is ran, you will be prompted to sign in with Internet Identity. Once signed in, select the 'Mint' function. It will mint tokens based on the backend smart contract's hardcoded configuration values for things such as token name, token symbol, and total supply. The owner principal of the token will be your Internet Identity principal. - -> [!CAUTION] -> This example is for demonstration purposes. It does not reflect a best practices workflow for creating and minting tokens on ICP. -> Actual production tokens deployed on ICP use a dedicated ledger smart contract and an index smart contract. For this example's demonstration, this functionality has been simplified and the ledger functionality is included in the backend smart contract. -> Tokens deployed using this example are only available for 20 minutes and will be deleted afterwards. They should be treated as "testnet" assets and should not be given real value. -> For more information on creating tokens using a recommended production workflow, view the [create a token documentation](https://internetcomputer.org/docs/current/developer-docs/defi/tokens/create). - -## 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: - -[![](https://icp.ninja/assets/open.svg)](https://icp.ninja/i?g=https://github.com/dfinity/examples/motoko/tokenmania) - -## Build and deploy from the command-line - -### 1. [Download and install the IC SDK.](https://internetcomputer.org/docs/building-apps/getting-started/install) - -### 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/) - -### 3. Navigate into the project's directory. - -### 4. Deploy the project to your local environment: - -``` -dfx start --background --clean && dfx deploy -``` - -## 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://internetcomputer.org/docs/building-apps/security/overview) for developing on ICP. This example may not implement all the best practices. diff --git a/motoko/tokenmania/backend/app.mo b/motoko/tokenmania/backend/app.mo deleted file mode 100644 index fe99c055d..000000000 --- a/motoko/tokenmania/backend/app.mo +++ /dev/null @@ -1,697 +0,0 @@ -// Use this token for testing purposes only! -// Please visit https://github.com/dfinity/ICRC-1 to find -// the latest version of the ICP token standards. - -import Blob "mo:core/Blob"; -import List "mo:core/List"; -import VarArray "mo:core/VarArray"; -import Principal "mo:core/Principal"; -import Option "mo:core/Option"; -import Time "mo:core/Time"; -import Int "mo:core/Int"; -import Nat8 "mo:core/Nat8"; -import Nat64 "mo:core/Nat64"; - -persistent actor class Tokenmania() = this { - - // Set temporary values for the token. - // These will be overritten when the token is created. - var init : { - initial_mints : [{ - account : { owner : Principal; subaccount : ?Blob }; - amount : Nat; - }]; - minting_account : { owner : Principal; subaccount : ?Blob }; - token_name : Text; - token_symbol : Text; - decimals : Nat8; - transfer_fee : Nat; - } = { - initial_mints = []; - minting_account = { - owner = Principal.fromBlob("\04"); - subaccount = null; - }; - token_name = ""; - token_symbol = ""; - decimals = 0; - transfer_fee = 0; - }; - - var logo : Text = ""; - var created : Bool = false; - - public query func token_created() : async Bool { - created; - }; - - public shared ({ caller }) func delete_token() : async Result { - if (not created) { - return #Err("Token not created"); - }; - - if (caller != init.minting_account.owner) { - return #Err("Caller is not the token creator"); - }; - - created := false; - - // Reset token details. - init := { - initial_mints = []; - minting_account = { - owner = Principal.fromBlob("\04"); - subaccount = null; - }; - token_name = ""; - token_symbol = ""; - decimals = 0; - transfer_fee = 0; - }; - - // Override the genesis txns. - log := makeGenesisChain(); - - #Ok("Token deleted"); - }; - - public shared ({ caller }) func create_token({ - token_name : Text; - token_symbol : Text; - initial_supply : Nat; - token_logo : Text; - }) : async Result { - if (created) { - return #Err("Token already created"); - }; - - if (caller.isAnonymous()) { - return #Err("Cannot create token with anonymous principal"); - }; - - // Specify actual token details, set the caller to own some inital amount. - init := { - initial_mints = [{ - account = { - owner = caller; - subaccount = null; - }; - amount = initial_supply; - }]; - minting_account = { - owner = caller; - subaccount = null; - }; - token_name; - token_symbol; - decimals = 8; // Change this to the number of decimals you want to use. - transfer_fee = 10_000; // Change this to the fee you want to charge for transfers. - }; - - // Set the token logo. - logo := token_logo; - - // Override the genesis chain with new minter and initial mints. - log := makeGenesisChain(); - - created := true; - - #Ok("Token created"); - }; - - // From here on, we use the reference implementation of the ICRC Ledger - // canister from https://github.com/dfinity/ICRC-1/blob/main/ref/ICRC1.mo, - // except where we add the token logo to the metadata. - - public type Account = { owner : Principal; subaccount : ?Subaccount }; - public type Subaccount = Blob; - public type Tokens = Nat; - public type Memo = Blob; - public type Timestamp = Nat64; - public type Duration = Nat64; - public type TxIndex = Nat; - public type TxLog = List.List; - - public type Value = { #Nat : Nat; #Int : Int; #Blob : Blob; #Text : Text }; - - transient let maxMemoSize = 32; - transient let permittedDriftNanos : Duration = 60_000_000_000; - transient let transactionWindowNanos : Duration = 24 * 60 * 60 * 1_000_000_000; - transient let defaultSubaccount : Subaccount = Blob.fromVarArray(VarArray.repeat(0 : Nat8, 32)); - - public type Operation = { - #Approve : Approve; - #Transfer : Transfer; - #Burn : Transfer; - #Mint : Transfer; - }; - - public type CommonFields = { - memo : ?Memo; - fee : ?Tokens; - created_at_time : ?Timestamp; - }; - - public type Approve = CommonFields and { - from : Account; - spender : Account; - amount : Nat; - expires_at : ?Nat64; - }; - - public type TransferSource = { - #Init; - #Icrc1Transfer; - #Icrc2TransferFrom; - }; - - public type Transfer = CommonFields and { - spender : Account; - source : TransferSource; - to : Account; - from : Account; - amount : Tokens; - }; - - public type Allowance = { allowance : Nat; expires_at : ?Nat64 }; - - public type Transaction = { - operation : Operation; - // Effective fee for this transaction. - fee : Tokens; - timestamp : Timestamp; - }; - - public type DeduplicationError = { - #TooOld; - #Duplicate : { duplicate_of : TxIndex }; - #CreatedInFuture : { ledger_time : Timestamp }; - }; - - public type CommonError = { - #InsufficientFunds : { balance : Tokens }; - #BadFee : { expected_fee : Tokens }; - #TemporarilyUnavailable; - #GenericError : { error_code : Nat; message : Text }; - }; - - public type TransferError = DeduplicationError or CommonError or { - #BadBurn : { min_burn_amount : Tokens }; - }; - - public type ApproveError = DeduplicationError or CommonError or { - #Expired : { ledger_time : Nat64 }; - #AllowanceChanged : { current_allowance : Nat }; - }; - - public type TransferFromError = TransferError or { - #InsufficientAllowance : { allowance : Nat }; - }; - - public type Result = { #Ok : T; #Err : E }; - - // Checks whether two accounts are semantically equal. - func accountsEqual(lhs : Account, rhs : Account) : Bool { - let lhsSubaccount = lhs.subaccount.get(defaultSubaccount); - let rhsSubaccount = rhs.subaccount.get(defaultSubaccount); - - lhs.owner == rhs.owner and lhsSubaccount == rhsSubaccount; - }; - - // Computes the balance of the specified account. - func balance(account : Account, log : TxLog) : Nat { - var sum = 0; - for (tx in log.values()) { - switch (tx.operation) { - case (#Burn(args)) { - if (accountsEqual(args.from, account)) { sum -= args.amount }; - }; - case (#Mint(args)) { - if (accountsEqual(args.to, account)) { sum += args.amount }; - }; - case (#Transfer(args)) { - if (accountsEqual(args.from, account)) { - sum -= args.amount + tx.fee; - }; - if (accountsEqual(args.to, account)) { sum += args.amount }; - }; - case (#Approve(args)) { - if (accountsEqual(args.from, account)) { sum -= tx.fee }; - }; - }; - }; - sum; - }; - - // Computes the total token supply. - func totalSupply(log : TxLog) : Tokens { - var total = 0; - for (tx in log.values()) { - switch (tx.operation) { - case (#Burn(args)) { total -= args.amount }; - case (#Mint(args)) { total += args.amount }; - case (#Transfer(_)) { total -= tx.fee }; - case (#Approve(_)) { total -= tx.fee }; - }; - }; - total; - }; - - // Finds a transaction in the transaction log. - func findTransfer(transfer : Transfer, log : TxLog) : ?TxIndex { - var i = 0; - for (tx in log.values()) { - switch (tx.operation) { - case (#Burn(args)) { if (args == transfer) { return ?i } }; - case (#Mint(args)) { if (args == transfer) { return ?i } }; - case (#Transfer(args)) { if (args == transfer) { return ?i } }; - case (_) {}; - }; - i += 1; - }; - null; - }; - - // Finds an approval in the transaction log. - func findApproval(approval : Approve, log : TxLog) : ?TxIndex { - var i = 0; - for (tx in log.values()) { - switch (tx.operation) { - case (#Approve(args)) { if (args == approval) { return ?i } }; - case (_) {}; - }; - i += 1; - }; - null; - }; - - // Computes allowance of the spender for the specified account. - func allowance(account : Account, spender : Account, now : Nat64) : Allowance { - var allowance : Nat = 0; - var lastApprovalTs : ?Nat64 = null; - - for (tx in log.values()) { - // Reset expired approvals, if any. - switch (lastApprovalTs) { - case (?expires_at) { - if (expires_at < tx.timestamp) { - allowance := 0; - lastApprovalTs := null; - }; - }; - case (null) {}; - }; - // Add pending approvals. - switch (tx.operation) { - case (#Approve(args)) { - if (args.from == account and args.spender == spender) { - allowance := args.amount; - lastApprovalTs := args.expires_at; - }; - }; - case (#Transfer(args)) { - if (args.from == account and args.spender == spender) { - assert (allowance > args.amount + tx.fee); - allowance -= args.amount + tx.fee; - }; - }; - case (_) {}; - }; - }; - - switch (lastApprovalTs) { - case (?expires_at) { - if (expires_at < now) { { allowance = 0; expires_at = null } } else { - { - allowance = Int.abs(allowance); - expires_at = ?expires_at; - }; - }; - }; - case (null) { { allowance = allowance; expires_at = null } }; - }; - }; - - // Constructs the transaction log corresponding to the init argument. - func makeGenesisChain() : TxLog { - validateSubaccount(init.minting_account.subaccount); - - let now = Nat64.fromNat(Int.abs(Time.now())); - let log = List.empty(); - for ({ account; amount } in init.initial_mints.vals()) { - validateSubaccount(account.subaccount); - let tx : Transaction = { - operation = #Mint({ - spender = init.minting_account; - source = #Init; - from = init.minting_account; - to = account; - amount = amount; - fee = null; - memo = null; - created_at_time = ?now; - }); - fee = 0; - timestamp = now; - }; - log.add(tx); - }; - log; - }; - - // Traps if the specified blob is not a valid subaccount. - func validateSubaccount(s : ?Subaccount) { - let subaccount = s.get(defaultSubaccount); - assert (subaccount.size() == 32); - }; - - func validateMemo(m : ?Memo) { - switch (m) { - case (null) {}; - case (?memo) { assert (memo.size() <= maxMemoSize) }; - }; - }; - - func checkTxTime(created_at_time : ?Timestamp, now : Timestamp) : Result<(), DeduplicationError> { - let txTime : Timestamp = created_at_time.get(now); - - if ((txTime > now) and (txTime - now > permittedDriftNanos)) { - return #Err(#CreatedInFuture { ledger_time = now }); - }; - - if ((txTime < now) and (now - txTime > transactionWindowNanos + permittedDriftNanos)) { - return #Err(#TooOld); - }; - - #Ok(()); - }; - - // The list of all transactions. - transient var log : TxLog = makeGenesisChain(); - - // The stable representation of the transaction log. - // Used only during upgrades. - var persistedLog : [Transaction] = []; - - system func preupgrade() { - persistedLog := log.toArray(); - }; - - system func postupgrade() { - log := List.fromArray(persistedLog); - }; - - func recordTransaction(tx : Transaction) : TxIndex { - let idx = log.size(); - log.add(tx); - idx; - }; - - func classifyTransfer(log : TxLog, transfer : Transfer) : Result<(Operation, Tokens), TransferError> { - let minter = init.minting_account; - - if (transfer.created_at_time.isSome()) { - switch (findTransfer(transfer, log)) { - case (?txid) { return #Err(#Duplicate { duplicate_of = txid }) }; - case null {}; - }; - }; - - let result = if (accountsEqual(transfer.from, minter)) { - if (transfer.fee.get(0) != 0) { - return #Err(#BadFee { expected_fee = 0 }); - }; - (#Mint(transfer), 0); - } else if (accountsEqual(transfer.to, minter)) { - if (transfer.fee.get(0) != 0) { - return #Err(#BadFee { expected_fee = 0 }); - }; - - if (transfer.amount < init.transfer_fee) { - return #Err(#BadBurn { min_burn_amount = init.transfer_fee }); - }; - - let debitBalance = balance(transfer.from, log); - if (debitBalance < transfer.amount) { - return #Err(#InsufficientFunds { balance = debitBalance }); - }; - - (#Burn(transfer), 0); - } else { - let effectiveFee = init.transfer_fee; - if (transfer.fee.get(effectiveFee) != effectiveFee) { - return #Err(#BadFee { expected_fee = init.transfer_fee }); - }; - - let debitBalance = balance(transfer.from, log); - if (debitBalance < transfer.amount + effectiveFee) { - return #Err(#InsufficientFunds { balance = debitBalance }); - }; - - (#Transfer(transfer), effectiveFee); - }; - #Ok(result); - }; - - func applyTransfer(args : Transfer) : Result { - validateSubaccount(args.from.subaccount); - validateSubaccount(args.to.subaccount); - validateMemo(args.memo); - - let now = Nat64.fromNat(Int.abs(Time.now())); - - switch (checkTxTime(args.created_at_time, now)) { - case (#Ok(_)) {}; - case (#Err(e)) { return #Err(e) }; - }; - - switch (classifyTransfer(log, args)) { - case (#Ok((operation, effectiveFee))) { - #Ok(recordTransaction({ operation = operation; fee = effectiveFee; timestamp = now })); - }; - case (#Err(e)) { #Err(e) }; - }; - }; - - func overflowOk(x : Nat) : Nat { - x; - }; - - public shared ({ caller }) func icrc1_transfer({ - from_subaccount : ?Subaccount; - to : Account; - amount : Tokens; - fee : ?Tokens; - memo : ?Memo; - created_at_time : ?Timestamp; - }) : async Result { - let from = { - owner = caller; - subaccount = from_subaccount; - }; - applyTransfer({ - spender = from; - source = #Icrc1Transfer; - from = from; - to = to; - amount = amount; - fee = fee; - memo = memo; - created_at_time = created_at_time; - }); - }; - - public query func icrc1_balance_of(account : Account) : async Tokens { - balance(account, log); - }; - - public query func icrc1_total_supply() : async Tokens { - totalSupply(log); - }; - - public query func icrc1_minting_account() : async ?Account { - ?init.minting_account; - }; - - public query func icrc1_name() : async Text { - init.token_name; - }; - - public query func icrc1_symbol() : async Text { - init.token_symbol; - }; - - public query func icrc1_decimals() : async Nat8 { - init.decimals; - }; - - public query func icrc1_fee() : async Nat { - init.transfer_fee; - }; - - public query func icrc1_metadata() : async [(Text, Value)] { - [ - ("icrc1:name", #Text(init.token_name)), - ("icrc1:symbol", #Text(init.token_symbol)), - ("icrc1:decimals", #Nat(init.decimals.toNat())), - ("icrc1:fee", #Nat(init.transfer_fee)), - ("icrc1:logo", #Text(logo)), /* Here we add the token logo to the metadata. */ - ]; - }; - - public query func icrc1_supported_standards() : async [{ - name : Text; - url : Text; - }] { - [ - { - name = "ICRC-1"; - url = "https://github.com/dfinity/ICRC-1/tree/main/standards/ICRC-1"; - }, - { - name = "ICRC-2"; - url = "https://github.com/dfinity/ICRC-1/tree/main/standards/ICRC-2"; - }, - ]; - }; - - public shared ({ caller }) func icrc2_approve({ - from_subaccount : ?Subaccount; - spender : Account; - amount : Nat; - expires_at : ?Nat64; - expected_allowance : ?Nat; - memo : ?Memo; - fee : ?Tokens; - created_at_time : ?Timestamp; - }) : async Result { - validateSubaccount(from_subaccount); - validateMemo(memo); - - let now = Nat64.fromNat(Int.abs(Time.now())); - - switch (checkTxTime(created_at_time, now)) { - case (#Ok(_)) {}; - case (#Err(e)) { return #Err(e) }; - }; - - let approverAccount = { owner = caller; subaccount = from_subaccount }; - let approval = { - from = approverAccount; - spender = spender; - amount = amount; - expires_at = expires_at; - fee = fee; - created_at_time = created_at_time; - memo = memo; - }; - - if (created_at_time.isSome()) { - switch (findApproval(approval, log)) { - case (?txid) { return #Err(#Duplicate { duplicate_of = txid }) }; - case (null) {}; - }; - }; - - switch (expires_at) { - case (?expires_at) { - if (expires_at < now) { return #Err(#Expired { ledger_time = now }) }; - }; - case (null) {}; - }; - - let effectiveFee = init.transfer_fee; - - if (fee.get(effectiveFee) != effectiveFee) { - return #Err(#BadFee({ expected_fee = effectiveFee })); - }; - - switch (expected_allowance) { - case (?expected_allowance) { - let currentAllowance = allowance(approverAccount, spender, now); - if (currentAllowance.allowance != expected_allowance) { - return #Err(#AllowanceChanged({ current_allowance = currentAllowance.allowance })); - }; - }; - case (null) {}; - }; - - let approverBalance = balance(approverAccount, log); - if (approverBalance < init.transfer_fee) { - return #Err(#InsufficientFunds { balance = approverBalance }); - }; - - let txid = recordTransaction({ - operation = #Approve(approval); - fee = effectiveFee; - timestamp = now; - }); - - assert (balance(approverAccount, log) == overflowOk(approverBalance - effectiveFee)); - - #Ok(txid); - }; - - public shared ({ caller }) func icrc2_transfer_from({ - spender_subaccount : ?Subaccount; - from : Account; - to : Account; - amount : Tokens; - fee : ?Tokens; - memo : ?Memo; - created_at_time : ?Timestamp; - }) : async Result { - validateSubaccount(spender_subaccount); - validateSubaccount(from.subaccount); - validateSubaccount(to.subaccount); - validateMemo(memo); - - let spender = { owner = caller; subaccount = spender_subaccount }; - let transfer : Transfer = { - spender = spender; - source = #Icrc2TransferFrom; - from = from; - to = to; - amount = amount; - fee = fee; - memo = memo; - created_at_time = created_at_time; - }; - - if (caller == from.owner) { - return applyTransfer(transfer); - }; - - let now = Nat64.fromNat(Int.abs(Time.now())); - - switch (checkTxTime(created_at_time, now)) { - case (#Ok(_)) {}; - case (#Err(e)) { return #Err(e) }; - }; - - let (operation, effectiveFee) = switch (classifyTransfer(log, transfer)) { - case (#Ok(result)) { result }; - case (#Err(err)) { return #Err(err) }; - }; - - let preTransferAllowance = allowance(from, spender, now); - if (preTransferAllowance.allowance < amount + effectiveFee) { - return #Err(#InsufficientAllowance { allowance = preTransferAllowance.allowance }); - }; - - let txid = recordTransaction({ - operation = operation; - fee = effectiveFee; - timestamp = now; - }); - - let postTransferAllowance = allowance(from, spender, now); - assert (postTransferAllowance.allowance == overflowOk(preTransferAllowance.allowance - (amount + effectiveFee))); - - #Ok(txid); - }; - - public query func icrc2_allowance({ account : Account; spender : Account }) : async Allowance { - allowance(account, spender, Nat64.fromNat(Int.abs(Time.now()))); - }; -}; diff --git a/motoko/tokenmania/dfx.json b/motoko/tokenmania/dfx.json deleted file mode 100644 index 837aea145..000000000 --- a/motoko/tokenmania/dfx.json +++ /dev/null @@ -1,46 +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" - }, - "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/tokenmania/frontend/index.css b/motoko/tokenmania/frontend/index.css deleted file mode 100644 index b5c61c956..000000000 --- a/motoko/tokenmania/frontend/index.css +++ /dev/null @@ -1,3 +0,0 @@ -@tailwind base; -@tailwind components; -@tailwind utilities; diff --git a/motoko/tokenmania/frontend/index.html b/motoko/tokenmania/frontend/index.html deleted file mode 100644 index 2fb96d095..000000000 --- a/motoko/tokenmania/frontend/index.html +++ /dev/null @@ -1,14 +0,0 @@ - - - - - - Tokenmania - - - - -
- - - diff --git a/motoko/tokenmania/frontend/package.json b/motoko/tokenmania/frontend/package.json deleted file mode 100644 index d0365f7ec..000000000 --- a/motoko/tokenmania/frontend/package.json +++ /dev/null @@ -1,26 +0,0 @@ -{ - "name": "frontend", - "private": true, - "type": "module", - "scripts": { - "prebuild": "npm i --include=dev && dfx generate backend", - "build": "vite build", - "dev": "vite" - }, - "dependencies": { - "@icp-sdk/auth": "~5.0.0", - "@icp-sdk/core": "~5.2.0", - "react": "18.3.1", - "react-dom": "18.3.1" - }, - "devDependencies": { - "@types/react": "18.3.12", - "@types/react-dom": "18.3.1", - "@vitejs/plugin-react": "4.3.3", - "autoprefixer": "^10.4.20", - "postcss": "8.4.48", - "tailwindcss": "3.4.14", - "vite": "5.4.11", - "vite-plugin-environment": "1.1.3" - } -} diff --git a/motoko/tokenmania/frontend/postcss.config.js b/motoko/tokenmania/frontend/postcss.config.js deleted file mode 100644 index 8c6e0c42c..000000000 --- a/motoko/tokenmania/frontend/postcss.config.js +++ /dev/null @@ -1,6 +0,0 @@ -export default { - plugins: { - autoprefixer: {}, - tailwindcss: {} - } -}; diff --git a/motoko/tokenmania/frontend/public/favicon.ico b/motoko/tokenmania/frontend/public/favicon.ico deleted file mode 100644 index 338fbf34c..000000000 Binary files a/motoko/tokenmania/frontend/public/favicon.ico and /dev/null differ diff --git a/motoko/tokenmania/frontend/src/App.jsx b/motoko/tokenmania/frontend/src/App.jsx deleted file mode 100644 index e73dea8f1..000000000 --- a/motoko/tokenmania/frontend/src/App.jsx +++ /dev/null @@ -1,83 +0,0 @@ -import React, { useState, useEffect } from 'react'; -import ApproveSpender from './TokenApprove'; -import AuthWarning from './AuthWarning'; -import BalanceChecker from './BalanceChecker'; -import Header from './Header'; -import TransferFrom from './TokenTransfer'; -import TokenInfo from './TokenInfo'; -import TokenSender from './TokenSender'; -import CreateToken from './CreateToken'; - -const App = () => { - const [isAuthenticated, setIsAuthenticated] = useState(false); - const [totalSupply, setTotalSupply] = useState(''); - const [actor, setActor] = useState(); - const [tokenCreated, setTokenCreated] = useState(false); - const [decimals, setDecimals] = useState(0n); - - const updateSupply = async () => { - try { - const supply = await actor.icrc1_total_supply(); - const decimals = BigInt(await actor.icrc1_decimals()); - setTotalSupply(`${Number(supply) / Number(10n ** decimals)}`); - setDecimals(decimals); - } catch (error) { - console.error('Error fetching total supply:', error); - } - }; - - const checkTokenCreated = async () => { - try { - const result = await actor.token_created(); - setTokenCreated(result); - } catch (error) { - console.error('Error fetching token created status:', error); - } - }; - - useEffect(() => { - if (isAuthenticated || tokenCreated) { - updateSupply(); - } - }, [isAuthenticated, tokenCreated]); - - useEffect(() => { - if (actor) { - checkTokenCreated(); - } - }, [actor]); - - return ( -
-
- {tokenCreated ? ( -
- -
- {isAuthenticated ? ( -
- - - - -
- ) : ( - - )} -
-
- ) : ( -
{isAuthenticated ? : }
- )} -
- ); -}; - -export default App; diff --git a/motoko/tokenmania/frontend/src/AuthWarning.jsx b/motoko/tokenmania/frontend/src/AuthWarning.jsx deleted file mode 100644 index df729fe3c..000000000 --- a/motoko/tokenmania/frontend/src/AuthWarning.jsx +++ /dev/null @@ -1,34 +0,0 @@ -import React from 'react'; - -const FullPageAuthWarning = ({ showIdentity }) => { - return ( -
-
-
-
-
- - - -

Authentication Required

-
-

Please sign in to access token management features.

-
-
-
-
- ); -}; - -export default FullPageAuthWarning; diff --git a/motoko/tokenmania/frontend/src/BalanceChecker.jsx b/motoko/tokenmania/frontend/src/BalanceChecker.jsx deleted file mode 100644 index 5cc2c5fa3..000000000 --- a/motoko/tokenmania/frontend/src/BalanceChecker.jsx +++ /dev/null @@ -1,81 +0,0 @@ -import React, { useState } from 'react'; -import { Principal } from '@icp-sdk/core/principal'; -import { backend } from 'declarations/backend'; - -const BalanceChecker = ({ decimals }) => { - const [principal, setPrincipal] = useState(''); - const [subaccount, setSubaccount] = useState(''); - const [balance, setBalance] = useState(null); - const [error, setError] = useState(''); - - const handleCheckBalance = async (e) => { - e.preventDefault(); - setBalance(null); - setError(''); - - try { - const owner = Principal.fromText(principal); - const subaccountArray = subaccount - ? [new Uint8Array(subaccount.split(',').map((num) => parseInt(num.trim(), 10)))] - : []; - - const result = await backend.icrc1_balance_of({ - owner: owner, - subaccount: subaccountArray - }); - - const supplyScaler = (s) => { - return Number(s) / Number(10n ** decimals); - }; - setBalance(supplyScaler(result).toString()); - } catch (err) { - console.error('Error checking balance:', err); - setError('Failed to check balance. Please ensure the principal is valid.'); - } - }; - - const inputFields = [ - { - name: 'principal', - value: principal, - setter: setPrincipal, - placeholder: 'Principal ID', - type: 'text', - required: true - }, - { - name: 'subaccount', - value: subaccount, - setter: setSubaccount, - placeholder: 'Subaccount (optional)', - type: 'text', - required: false - } - ]; - - return ( -
-

Check Balance

-
- {inputFields.map(({ name, value, setter, placeholder, type, required }) => ( - setter(e.target.value)} - placeholder={placeholder} - required={required} - className="w-full rounded-md border px-3 py-2" - /> - ))} - -
- {balance !== null &&
Balance: {balance}
} - {error &&
{error}
}{' '} -
- ); -}; - -export default BalanceChecker; diff --git a/motoko/tokenmania/frontend/src/CardDisplay.jsx b/motoko/tokenmania/frontend/src/CardDisplay.jsx deleted file mode 100644 index d59f81e09..000000000 --- a/motoko/tokenmania/frontend/src/CardDisplay.jsx +++ /dev/null @@ -1,36 +0,0 @@ -import React from 'react'; - -const Card = ({ icon, title, value }) => ( -
-
- {icon} - {title} -
-
{Object.values(value)}
-
-); - -const CardDisplay = ({ cards, loading }) => { - if (loading) { - return ( -
-
-
- ); - } - - return ( -
-

- Token Information -

-
- {cards.map((card, index) => ( - - ))} -
-
- ); -}; - -export default CardDisplay; diff --git a/motoko/tokenmania/frontend/src/CreateToken.jsx b/motoko/tokenmania/frontend/src/CreateToken.jsx deleted file mode 100644 index 47df34d40..000000000 --- a/motoko/tokenmania/frontend/src/CreateToken.jsx +++ /dev/null @@ -1,110 +0,0 @@ -import React from 'react'; - -const CreateToken = ({ actor, setTokenCreated }) => { - const [tokenName, setTokenName] = React.useState(); - const [tokenSymbol, setTokenSymbol] = React.useState(); - const [tokenSupply, setTokenSupply] = React.useState(); - const [tokenLogo, setTokenLogo] = React.useState(); - - const createToken = async (e) => { - e.preventDefault(); - if (!tokenName || !tokenSymbol || !tokenSupply || !tokenLogo) { - alert('Please fill in all fields'); - return; - } - const result = await actor.create_token({ - token_name: tokenName, - token_symbol: tokenSymbol, - initial_supply: tokenSupply * Number(10 ** 8), - token_logo: tokenLogo - }); - - if ('Ok' in result) { - setTokenCreated(true); - } else if ('Err' in result) { - console.error('Failed to create token:', result.Err); - } - }; - - const handleImageChange = (e) => { - const file = e.target.files[0]; - - if (!file) { - return; - } - // Check file size - if (file.size > 1024 * 1024) { - alert('File is too large. Please select a file under 1MB.'); - return; - } - const reader = new FileReader(); - reader.onload = (e) => { - setTokenLogo(e.target.result); - }; - reader.readAsDataURL(file); - }; - - return ( -
-

Create a new token

-
-
- - setTokenName(e.target.value)} - /> -
-
- - setTokenSymbol(e.target.value)} - /> -
-
- - setTokenSupply(e.target.value)} - /> -
-
- - - {tokenLogo && Token logo preview} -
-

The principal signed in will be set as the token minter.

- -
-
- ); -}; - -export default CreateToken; diff --git a/motoko/tokenmania/frontend/src/Header.jsx b/motoko/tokenmania/frontend/src/Header.jsx deleted file mode 100644 index 5e7cd4676..000000000 --- a/motoko/tokenmania/frontend/src/Header.jsx +++ /dev/null @@ -1,52 +0,0 @@ -import React from 'react'; -import InternetIdentity from './InternetIdentity'; -import { canisterId } from 'declarations/backend'; - -const Header = ({ actor, setActor, isAuthenticated, setIsAuthenticated, tokenCreated, setTokenCreated }) => { - const handleDeleteToken = async () => { - try { - const result = await actor.delete_token(); - if ('Ok' in result) { - setTokenCreated(false); - } else if ('Err' in result) { - console.error('Failed to delete token:', result.Err); - alert('Failed to delete token: ' + result.Err); - } - } catch (error) { - console.error('Error deleting token:', error); - } - }; - - return ( -
-
-

Tokenmania

-
- - {isAuthenticated && tokenCreated && ( -
- - -
- )} -
-
-
- ); -}; - -export default Header; diff --git a/motoko/tokenmania/frontend/src/InternetIdentity.jsx b/motoko/tokenmania/frontend/src/InternetIdentity.jsx deleted file mode 100644 index 5e6793b40..000000000 --- a/motoko/tokenmania/frontend/src/InternetIdentity.jsx +++ /dev/null @@ -1,72 +0,0 @@ -import React, { useEffect, useState } from 'react'; -import { AuthClient } from '@icp-sdk/auth/client'; -import { createActor, canisterId } from 'declarations/backend'; - -const network = process.env.DFX_NETWORK; -const identityProvider = - network === 'ic' - ? 'https://id.ai/' // Mainnet - : 'http://uqzsh-gqaaa-aaaaq-qaada-cai.localhost:4943'; // Local - -const InternetIdentity = ({ setActor, isAuthenticated, setIsAuthenticated }) => { - const [authClient, setAuthClient] = useState(); - const [principal, setPrincipal] = useState(); - useEffect(() => { - updateActor(); - }, []); - - async function updateActor() { - const authClient = await AuthClient.create(); - const identity = authClient.getIdentity(); - const actor = createActor(canisterId, { - agentOptions: { - identity - } - }); - const isAuthenticated = await authClient.isAuthenticated(); - - setActor(actor); - setAuthClient(authClient); - setIsAuthenticated(isAuthenticated); - setPrincipal(identity.getPrincipal().toString()); - } - - async function login() { - await authClient.login({ - identityProvider, - onSuccess: updateActor - }); - } - - async function logout() { - await authClient.logout(); - updateActor(); - } - - return ( -
- {isAuthenticated ? ( - <> -

- {principal} -

- - - ) : ( - - )} -
- ); -}; - -export default InternetIdentity; diff --git a/motoko/tokenmania/frontend/src/StatusMessage.jsx b/motoko/tokenmania/frontend/src/StatusMessage.jsx deleted file mode 100644 index f76c8f4ed..000000000 --- a/motoko/tokenmania/frontend/src/StatusMessage.jsx +++ /dev/null @@ -1,24 +0,0 @@ -import React from 'react'; - -const StatusMessage = ({ message, isSuccess }) => { - if (!message) return null; - - return ( -
- - {isSuccess ? : } - - {message} -
- ); -}; - -export default StatusMessage; diff --git a/motoko/tokenmania/frontend/src/TokenApprove.jsx b/motoko/tokenmania/frontend/src/TokenApprove.jsx deleted file mode 100644 index 175cf6561..000000000 --- a/motoko/tokenmania/frontend/src/TokenApprove.jsx +++ /dev/null @@ -1,96 +0,0 @@ -import React, { useState } from 'react'; -import { Principal } from '@icp-sdk/core/principal'; -import StatusMessage from './StatusMessage'; - -const ApproveSpender = ({ actor, decimals }) => { - const [spenderAddress, setSpenderAddress] = useState(''); - const [amount, setAmount] = useState(''); - const [fromSubaccount, setFromSubaccount] = useState(''); - const [status, setStatus] = useState({ message: '', isSuccess: null }); - - const handleApprove = async (e) => { - e.preventDefault(); - try { - const result = await actor.icrc2_approve({ - spender: { owner: Principal.fromText(spenderAddress), subaccount: [] }, - amount: amount * Number(10 ** Number(decimals)), - from_subaccount: fromSubaccount ? [fromSubaccount] : [], - expires_at: [], - expected_allowance: [], - memo: [], - fee: [], - created_at_time: [] - }); - if ('Ok' in result) { - setStatus({ message: 'Approval successful', isSuccess: true }); - } else if ('Err' in result) { - setStatus({ - message: `Approval failed: ${Object.keys(result.Err)[0]}`, - isSuccess: false - }); - } - } catch (error) { - console.error('Approval failed:', error); - setStatus({ - message: 'Approval failed failed: Unexpected error', - isSuccess: false - }); - } - }; - - const inputFields = [ - { - name: 'fromSubaccount', - value: fromSubaccount, - setter: setFromSubaccount, - placeholder: 'From Subaccount (optional)', - type: 'text', - required: false - }, - { - name: 'spenderAddress', - value: spenderAddress, - setter: setSpenderAddress, - placeholder: 'Spender Address', - type: 'text', - required: true - }, - { - name: 'amount', - value: amount, - setter: setAmount, - placeholder: 'Approved Amount', - type: 'number', - required: true, - min: '0', - step: '0.000001' - } - ]; - - return ( -
-

Approve Spender

-
- {inputFields.map(({ name, value, setter, placeholder, type, required, min, step }) => ( - setter(e.target.value)} - placeholder={placeholder} - required={required} - min={min} - step={step} - className="w-full rounded-md border px-3 py-2" - /> - ))} - -
- -
- ); -}; - -export default ApproveSpender; diff --git a/motoko/tokenmania/frontend/src/TokenInfo.jsx b/motoko/tokenmania/frontend/src/TokenInfo.jsx deleted file mode 100644 index 98968e5c2..000000000 --- a/motoko/tokenmania/frontend/src/TokenInfo.jsx +++ /dev/null @@ -1,56 +0,0 @@ -import React, { useState, useEffect } from 'react'; -import { backend, canisterId } from 'declarations/backend'; -import CardDisplay from './CardDisplay'; - -const TokenInfo = ({ totalSupply }) => { - const [tokenInfo, setTokenInfo] = useState({ - name: '', - symbol: '', - loading: true - }); - - useEffect(() => { - const fetchTokenInfo = async () => { - try { - const metadata = await backend.icrc1_metadata(); - const newTokenInfo = metadata.reduce((acc, [key, value]) => { - const parsedKey = key.split(':')[1].trim(); - if (parsedKey === 'name' || parsedKey === 'symbol') { - acc[parsedKey] = value.Text; - } - return acc; - }, {}); - - setTokenInfo((prevState) => ({ - ...prevState, - ...newTokenInfo, - loading: false - })); - } catch (error) { - console.error('Error fetching token info:', error); - setTokenInfo((prevState) => ({ ...prevState, loading: false })); - } - }; - - fetchTokenInfo(); - }, []); - - if (tokenInfo.loading) { - return ( -
-
-
- ); - } - - const cardInfo = [ - { icon: '💰', title: 'Name', value: tokenInfo.name }, - { icon: '🏷️', title: 'Symbol', value: tokenInfo.symbol }, - { icon: '📊', title: 'Total Supply', value: totalSupply }, - { icon: '💳', title: 'Token Address (ICRC-2)', value: canisterId } - ]; - - return ; -}; - -export default TokenInfo; diff --git a/motoko/tokenmania/frontend/src/TokenSender.jsx b/motoko/tokenmania/frontend/src/TokenSender.jsx deleted file mode 100644 index 762d21df3..000000000 --- a/motoko/tokenmania/frontend/src/TokenSender.jsx +++ /dev/null @@ -1,105 +0,0 @@ -import React, { useState } from 'react'; -import { Principal } from '@icp-sdk/core/principal'; -import StatusMessage from './StatusMessage'; - -const TokenSender = ({ actor, updateSupply, decimals }) => { - const [fromSubaccount, setFromSubaccount] = useState(''); - const [address, setAddress] = useState(''); - const [amount, setAmount] = useState(); - const [status, setStatus] = useState({ message: '', isSuccess: null }); - - const handleSendTransaction = async (e) => { - e.preventDefault(); - try { - const result = await actor.icrc1_transfer({ - to: { - owner: Principal.fromText(address), - subaccount: [] - }, - fee: [], - memo: [], - from_subaccount: fromSubaccount ? [fromSubaccount] : [], - created_at_time: [], - amount: amount * Number(10 ** Number(decimals)) - }); - if ('Ok' in result) { - setStatus({ message: 'Transfer successful', isSuccess: true }); - updateSupply(); - } else if ('Err' in result) { - if ('InsufficientFunds' in result.Err) { - setStatus({ - message: `Transfer failed: Insufficient funds. Available balance: ${result.Err.InsufficientFunds.balance}`, - isSuccess: false - }); - } else { - setStatus({ - message: `Transfer failed: ${Object.keys(result.Err)[0]}`, - isSuccess: false - }); - } - } - } catch (error) { - console.error('Transfer failed:', error); - setStatus({ - message: 'Transfer failed: Unexpected error', - isSuccess: false - }); - } - }; - - const inputFields = [ - { - name: 'fromSubaccount', - value: fromSubaccount, - setter: setFromSubaccount, - placeholder: 'From Subaccount (optional)', - type: 'text', - required: false - }, - { - name: 'address', - value: address, - setter: setAddress, - placeholder: 'Recipient Address', - type: 'text', - required: true - }, - { - name: 'amount', - value: amount, - setter: setAmount, - placeholder: 'Amount', - type: 'number', - required: true, - min: '0', - step: '0.000001' - } - ]; - - return ( -
-

Send/Mint Tokens

-
- {inputFields.map(({ name, value, setter, placeholder, type, required, min, step }) => ( - setter(e.target.value)} - placeholder={placeholder} - required={required} - min={min} - step={step} - className="w-full rounded-md border px-3 py-2" - /> - ))} - -
- -
- ); -}; - -export default TokenSender; diff --git a/motoko/tokenmania/frontend/src/TokenTransfer.jsx b/motoko/tokenmania/frontend/src/TokenTransfer.jsx deleted file mode 100644 index 912347b69..000000000 --- a/motoko/tokenmania/frontend/src/TokenTransfer.jsx +++ /dev/null @@ -1,104 +0,0 @@ -import React, { useState } from 'react'; -import { Principal } from '@icp-sdk/core/principal'; -import StatusMessage from './StatusMessage'; - -const TransferFrom = ({ actor, decimals }) => { - const [fromAddress, setFromAddress] = useState(''); - const [toAddress, setToAddress] = useState(''); - const [amount, setAmount] = useState(); - const [spenderSubaccount, setSpenderSubaccount] = useState(''); - const [status, setStatus] = useState({ message: '', isSuccess: null }); - - const handleTransferFrom = async (e) => { - e.preventDefault(); - try { - const result = await actor.icrc2_transfer_from({ - from: { owner: Principal.fromText(fromAddress), subaccount: [] }, - to: { owner: Principal.fromText(toAddress), subaccount: [] }, - amount: amount * Number(10 ** Number(decimals)), - spender_subaccount: spenderSubaccount ? [spenderSubaccount] : [], - fee: [], - memo: [], - created_at_time: [] - }); - if ('Ok' in result) { - setStatus({ message: 'Transfer successful', isSuccess: true }); - } else if ('Err' in result) { - setStatus({ - message: `Transfer failed: ${Object.keys(result.Err)[0]}`, - isSuccess: false - }); - } - } catch (error) { - console.error('Transfer failed:', error); - setStatus({ - message: 'Transfer failed: Unexpected error', - isSuccess: false - }); - } - }; - - const inputFields = [ - { - name: 'fromAddress', - value: fromAddress, - setter: setFromAddress, - placeholder: 'From Address', - type: 'text', - required: true - }, - { - name: 'toAddress', - value: toAddress, - setter: setToAddress, - placeholder: 'To Address', - type: 'text', - required: true - }, - { - name: 'amount', - value: amount, - setter: setAmount, - placeholder: 'Amount', - type: 'number', - required: true, - min: '0', - step: '0.000001' - }, - { - name: 'spenderSubaccount', - value: spenderSubaccount, - setter: setSpenderSubaccount, - placeholder: 'Spender Subaccount (optional)', - type: 'text', - required: false - } - ]; - - return ( -
-

Transfer From

-
- {inputFields.map(({ name, value, setter, placeholder, type, required, min, step }) => ( - setter(e.target.value)} - placeholder={placeholder} - required={required} - min={min} - step={step} - className="w-full rounded-md border px-3 py-2" - /> - ))} - -
- -
- ); -}; - -export default TransferFrom; diff --git a/motoko/tokenmania/frontend/src/main.jsx b/motoko/tokenmania/frontend/src/main.jsx deleted file mode 100644 index 70e834f85..000000000 --- a/motoko/tokenmania/frontend/src/main.jsx +++ /dev/null @@ -1,10 +0,0 @@ -import React from 'react'; -import ReactDOM from 'react-dom/client'; -import App from './App'; -import '../index.css'; - -ReactDOM.createRoot(document.getElementById('root')).render( - - - -); diff --git a/motoko/tokenmania/frontend/tailwind.config.js b/motoko/tokenmania/frontend/tailwind.config.js deleted file mode 100644 index c1d69a222..000000000 --- a/motoko/tokenmania/frontend/tailwind.config.js +++ /dev/null @@ -1,19 +0,0 @@ -export default { - content: ['./src/**/*.{js,ts,jsx,tsx}'], - theme: { - extend: { - colors: { - infinite: '#3b00b9', - 'dark-infinite': '#1e005d', - razzmatazz: '#ed1e79', - flamingo: '#f15a24', - 'sea-buckthron': '#fbb03b', - 'picton-blue': '#29abe2', - meteorite: '#522785' - }, - fontFamily: { - body: ['Circular Std', 'sans'] - } - } - } -}; diff --git a/motoko/tokenmania/frontend/vite.config.js b/motoko/tokenmania/frontend/vite.config.js deleted file mode 100644 index f9e04a9a9..000000000 --- a/motoko/tokenmania/frontend/vite.config.js +++ /dev/null @@ -1,37 +0,0 @@ -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/motoko/tokenmania/mops.toml b/motoko/tokenmania/mops.toml deleted file mode 100644 index 2de72e881..000000000 --- a/motoko/tokenmania/mops.toml +++ /dev/null @@ -1,13 +0,0 @@ -# Motoko dependencies (https://mops.one/) - -[toolchain] -moc = "1.5.1" - -[dependencies] -core = "2.4.0" - -[moc] -# M0236: use context dot notation (e.g. map.get(k) instead of Map.get(map, compare, k)) -# 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", "-W", "M0237", "-W", "M0223"] diff --git a/motoko/tokenmania/package.json b/motoko/tokenmania/package.json deleted file mode 100644 index b50e79683..000000000 --- a/motoko/tokenmania/package.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "name": "example", - "scripts": { - "build": "npm run build --workspaces --if-present", - "prebuild": "npm run prebuild --workspaces --if-present", - "dev": "npm run dev --workspaces --if-present" - }, - "type": "module", - "workspaces": [ - "frontend" - ] -} diff --git a/motoko/vetkeys/README.md b/motoko/vetkeys/README.md deleted file mode 100644 index 822311636..000000000 --- a/motoko/vetkeys/README.md +++ /dev/null @@ -1,12 +0,0 @@ -# VetKeys Examples (Motoko) - -The VetKeys examples (including Motoko backends) are located in [`rust/vetkeys/`](../../rust/vetkeys/). - -Each example that supports a Motoko backend has a `motoko/` subdirectory alongside its `rust/` backend: - -- [Basic BLS Signing](../../rust/vetkeys/basic_bls_signing/) — Motoko + Rust -- [Basic IBE](../../rust/vetkeys/basic_ibe/) — Motoko + Rust -- [Encrypted Notes](../../rust/vetkeys/encrypted_notes_dapp_vetkd/) — Motoko + Rust -- [Password Manager](../../rust/vetkeys/password_manager/) — Motoko + Rust -- [Password Manager with Metadata](../../rust/vetkeys/password_manager_with_metadata/) — Motoko + Rust -- [Basic Timelock IBE](../../rust/vetkeys/basic_timelock_ibe/) — Rust only diff --git a/rust/basic_dogecoin/README.md b/rust/basic_dogecoin/README.md deleted file mode 100644 index aedc79db0..000000000 --- a/rust/basic_dogecoin/README.md +++ /dev/null @@ -1,3 +0,0 @@ -# Basic Dogecoin - -This example is available in the [Dogecoin canister](https://github.com/dfinity/dogecoin-canister) repository under [`examples/basic_dogecoin`](https://github.com/dfinity/dogecoin-canister/tree/master/examples/basic_dogecoin). diff --git a/rust/basic_solana/README.md b/rust/basic_solana/README.md deleted file mode 100644 index 0817185c4..000000000 --- a/rust/basic_solana/README.md +++ /dev/null @@ -1,3 +0,0 @@ -# Basic Solana - -This example is available in the [SOL RPC canister](https://github.com/dfinity/sol-rpc-canister) repository under [`examples/basic_solana`](https://github.com/dfinity/sol-rpc-canister/tree/main/examples/basic_solana). diff --git a/rust/encrypted-notes-dapp-vetkd/README.md b/rust/encrypted-notes-dapp-vetkd/README.md deleted file mode 100644 index dcf59f482..000000000 --- a/rust/encrypted-notes-dapp-vetkd/README.md +++ /dev/null @@ -1,3 +0,0 @@ -# Encrypted notes: vetKD - -This example has moved [here](https://github.com/dfinity/vetkeys/tree/main/examples/encrypted_notes_dapp_vetkd). \ No newline at end of file diff --git a/rust/token_transfer/.gitignore b/rust/token_transfer/.gitignore deleted file mode 100644 index 49c89a1c9..000000000 --- a/rust/token_transfer/.gitignore +++ /dev/null @@ -1,25 +0,0 @@ -# Various IDEs and Editors -.vscode/ -.idea/ -**/*~ - -# Mac OSX temporary files -.DS_Store -**/.DS_Store - -# dfx temporary files -.dfx/ - -# generated files -**/declarations/ - -# rust -target/ - -# frontend code -node_modules/ -dist/ -.svelte-kit/ - -# environment variables -.env diff --git a/rust/token_transfer/Cargo.lock b/rust/token_transfer/Cargo.lock deleted file mode 100644 index 7658e1385..000000000 --- a/rust/token_transfer/Cargo.lock +++ /dev/null @@ -1,861 +0,0 @@ -# This file is automatically @generated by Cargo. -# It is not intended for manual editing. -version = 4 - -[[package]] -name = "anyhow" -version = "1.0.98" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e16d2d3311acee920a9eb8d33b8cbc1787ce4a264e85f964c2404b969bdcd487" - -[[package]] -name = "arrayvec" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23b62fc65de8e4e7f52534fb52b0f3ed04746ae267519eef2a83941e8085068b" - -[[package]] -name = "async-trait" -version = "0.1.89" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.101", -] - -[[package]] -name = "autocfg" -version = "1.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" - -[[package]] -name = "base32" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23ce669cd6c8588f79e15cf450314f9638f967fc5770ff1c7c1deb0925ea7cfa" - -[[package]] -name = "binread" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "16598dfc8e6578e9b597d9910ba2e73618385dc9f4b1d43dd92c349d6be6418f" -dependencies = [ - "binread_derive", - "lazy_static", - "rustversion", -] - -[[package]] -name = "binread_derive" -version = "2.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d9672209df1714ee804b1f4d4f68c8eb2a90b1f7a07acf472f88ce198ef1fed" -dependencies = [ - "either", - "proc-macro2", - "quote", - "syn 1.0.109", -] - -[[package]] -name = "block-buffer" -version = "0.10.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" -dependencies = [ - "generic-array", -] - -[[package]] -name = "byteorder" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" - -[[package]] -name = "candid" -version = "0.10.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eaac522d18020d5fbc8320ecb12a9b13b2137ae31133da2d42fa256a825507c4" -dependencies = [ - "anyhow", - "binread", - "byteorder", - "candid_derive", - "hex", - "ic_principal", - "leb128", - "num-bigint", - "num-traits", - "paste", - "pretty", - "serde", - "serde_bytes", - "stacker", - "thiserror 1.0.69", -] - -[[package]] -name = "candid_derive" -version = "0.10.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a1b4fddbd462182050989068d53604a91a3d0f117c3c8316c6818023df00add" -dependencies = [ - "lazy_static", - "proc-macro2", - "quote", - "syn 2.0.101", -] - -[[package]] -name = "cc" -version = "1.2.23" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f4ac86a9e5bc1e2b3449ab9d7d3a6a405e3d1bb28d7b9be8614f55846ae3766" -dependencies = [ - "shlex", -] - -[[package]] -name = "cfg-if" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" - -[[package]] -name = "cpufeatures" -version = "0.2.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" -dependencies = [ - "libc", -] - -[[package]] -name = "crc32fast" -version = "1.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a97769d94ddab943e4510d138150169a2758b5ef3eb191a9ee688de3e23ef7b3" -dependencies = [ - "cfg-if", -] - -[[package]] -name = "crypto-common" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" -dependencies = [ - "generic-array", - "typenum", -] - -[[package]] -name = "darling" -version = "0.20.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc7f46116c46ff9ab3eb1597a45688b6715c6e628b5c133e288e709a29bcb4ee" -dependencies = [ - "darling_core", - "darling_macro", -] - -[[package]] -name = "darling_core" -version = "0.20.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d00b9596d185e565c2207a0b01f8bd1a135483d02d9b7b0a54b11da8d53412e" -dependencies = [ - "fnv", - "ident_case", - "proc-macro2", - "quote", - "strsim", - "syn 2.0.101", -] - -[[package]] -name = "darling_macro" -version = "0.20.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead" -dependencies = [ - "darling_core", - "quote", - "syn 2.0.101", -] - -[[package]] -name = "data-encoding" -version = "2.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a2330da5de22e8a3cb63252ce2abb30116bf5265e89c0e01bc17015ce30a476" - -[[package]] -name = "deranged" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c9e6a11ca8224451684bc0d7d5a7adbf8f2fd6887261a1cfc3c0432f9d4068e" -dependencies = [ - "powerfmt", -] - -[[package]] -name = "digest" -version = "0.10.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" -dependencies = [ - "block-buffer", - "crypto-common", -] - -[[package]] -name = "either" -version = "1.15.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" - -[[package]] -name = "fnv" -version = "1.0.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" - -[[package]] -name = "generic-array" -version = "0.14.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" -dependencies = [ - "typenum", - "version_check", -] - -[[package]] -name = "heck" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" - -[[package]] -name = "hex" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" -dependencies = [ - "serde", -] - -[[package]] -name = "ic-cdk" -version = "0.18.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4efb278f5d3ef033b3eed7f01f1096eaf67701896aa5ef69f5eddf5a84833dc0" -dependencies = [ - "candid", - "ic-cdk-executor", - "ic-cdk-macros", - "ic-error-types", - "ic-management-canister-types", - "ic0", - "serde", - "serde_bytes", - "slotmap", - "thiserror 2.0.12", -] - -[[package]] -name = "ic-cdk-executor" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "99f4ee8930fd2e491177e2eb7fff53ee1c407c13b9582bdc7d6920cf83109a2d" -dependencies = [ - "ic0", - "slotmap", -] - -[[package]] -name = "ic-cdk-macros" -version = "0.18.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7eb14c5d691cc9d72bb95459b4761e3a4b3444b85a63d17555d5ddd782969a1e" -dependencies = [ - "candid", - "darling", - "proc-macro2", - "quote", - "syn 2.0.101", -] - -[[package]] -name = "ic-error-types" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbeeb3d91aa179d6496d7293becdacedfc413c825cac79fd54ea1906f003ee55" -dependencies = [ - "serde", - "strum", - "strum_macros", -] - -[[package]] -name = "ic-management-canister-types" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea7e5b8a0f7c3b320d9450ac950547db4f24a31601b5d398f9680b64427455d2" -dependencies = [ - "candid", - "serde", - "serde_bytes", -] - -[[package]] -name = "ic-stable-structures" -version = "0.6.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0f5684f577e0146738cd11afed789109c4f51ba963c75823c48c1501dc53278" -dependencies = [ - "ic_principal", -] - -[[package]] -name = "ic0" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8877193e1921b5fd16accb0305eb46016868cd1935b05c05eca0ec007b943272" - -[[package]] -name = "ic_principal" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1762deb6f7c8d8c2bdee4b6c5a47b60195b74e9b5280faa5ba29692f8e17429c" -dependencies = [ - "crc32fast", - "data-encoding", - "serde", - "sha2", - "thiserror 1.0.69", -] - -[[package]] -name = "icrc-cbor" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90569d2894d9536c5416943556ac6339df249f06611b3c41029196b39e0dd119" -dependencies = [ - "candid", - "minicbor", - "num-bigint", - "num-traits", -] - -[[package]] -name = "icrc-ledger-client" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6662452c60a8636cd8f1293246de3fdc4652ff2a37af6c7e2522fb567efe67b2" -dependencies = [ - "async-trait", - "candid", - "icrc-ledger-types", -] - -[[package]] -name = "icrc-ledger-client-cdk" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e72bf46503a9ff15378e8d5560e51679f11d6548ecac7939ac00bf05d37f7ff8" -dependencies = [ - "async-trait", - "candid", - "ic-cdk", - "icrc-ledger-client", -] - -[[package]] -name = "icrc-ledger-types" -version = "0.1.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87c31beeee0e5ab964861a3d5ea2b5ed7b688b2b22400367a832b1fcf0db1fa4" -dependencies = [ - "base32", - "candid", - "crc32fast", - "hex", - "ic-stable-structures", - "icrc-cbor", - "itertools", - "minicbor", - "num-bigint", - "num-traits", - "serde", - "serde_bytes", - "sha2", - "strum", - "strum_macros", - "time", -] - -[[package]] -name = "ident_case" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" - -[[package]] -name = "itertools" -version = "0.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569" -dependencies = [ - "either", -] - -[[package]] -name = "itoa" -version = "1.0.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" - -[[package]] -name = "lazy_static" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" - -[[package]] -name = "leb128" -version = "0.2.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "884e2677b40cc8c339eaefcb701c32ef1fd2493d71118dc0ca4b6a736c93bd67" - -[[package]] -name = "libc" -version = "0.2.172" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d750af042f7ef4f724306de029d18836c26c1765a54a6a3f094cbd23a7267ffa" - -[[package]] -name = "libm" -version = "0.2.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f9fbbcab51052fe104eb5e5d351cf728d30a5be1fe14d9be8a3b097481fb97de" - -[[package]] -name = "minicbor" -version = "0.19.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7005aaf257a59ff4de471a9d5538ec868a21586534fff7f85dd97d4043a6139" -dependencies = [ - "minicbor-derive", -] - -[[package]] -name = "minicbor-derive" -version = "0.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1154809406efdb7982841adb6311b3d095b46f78342dd646736122fe6b19e267" -dependencies = [ - "proc-macro2", - "quote", - "syn 1.0.109", -] - -[[package]] -name = "num-bigint" -version = "0.4.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" -dependencies = [ - "num-integer", - "num-traits", - "serde", -] - -[[package]] -name = "num-conv" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" - -[[package]] -name = "num-integer" -version = "0.1.46" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" -dependencies = [ - "num-traits", -] - -[[package]] -name = "num-traits" -version = "0.2.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" -dependencies = [ - "autocfg", - "libm", -] - -[[package]] -name = "paste" -version = "1.0.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" - -[[package]] -name = "powerfmt" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" - -[[package]] -name = "pretty" -version = "0.12.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac98773b7109bc75f475ab5a134c9b64b87e59d776d31098d8f346922396a477" -dependencies = [ - "arrayvec", - "typed-arena", - "unicode-width", -] - -[[package]] -name = "proc-macro2" -version = "1.0.95" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778" -dependencies = [ - "unicode-ident", -] - -[[package]] -name = "psm" -version = "0.1.26" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e944464ec8536cd1beb0bbfd96987eb5e3b72f2ecdafdc5c769a37f1fa2ae1f" -dependencies = [ - "cc", -] - -[[package]] -name = "quote" -version = "1.0.40" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" -dependencies = [ - "proc-macro2", -] - -[[package]] -name = "rustversion" -version = "1.0.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eded382c5f5f786b989652c49544c4877d9f015cc22e145a5ea8ea66c2921cd2" - -[[package]] -name = "serde" -version = "1.0.219" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" -dependencies = [ - "serde_derive", -] - -[[package]] -name = "serde_bytes" -version = "0.11.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8437fd221bde2d4ca316d61b90e337e9e702b3820b87d63caa9ba6c02bd06d96" -dependencies = [ - "serde", -] - -[[package]] -name = "serde_derive" -version = "1.0.219" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.101", -] - -[[package]] -name = "sha2" -version = "0.10.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" -dependencies = [ - "cfg-if", - "cpufeatures", - "digest", -] - -[[package]] -name = "shlex" -version = "1.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" - -[[package]] -name = "slotmap" -version = "1.0.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dbff4acf519f630b3a3ddcfaea6c06b42174d9a44bc70c620e9ed1649d58b82a" -dependencies = [ - "version_check", -] - -[[package]] -name = "stacker" -version = "0.1.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cddb07e32ddb770749da91081d8d0ac3a16f1a569a18b20348cd371f5dead06b" -dependencies = [ - "cc", - "cfg-if", - "libc", - "psm", - "windows-sys", -] - -[[package]] -name = "strsim" -version = "0.11.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" - -[[package]] -name = "strum" -version = "0.26.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fec0f0aef304996cf250b31b5a10dee7980c85da9d759361292b8bca5a18f06" -dependencies = [ - "strum_macros", -] - -[[package]] -name = "strum_macros" -version = "0.26.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c6bee85a5a24955dc440386795aa378cd9cf82acd5f764469152d2270e581be" -dependencies = [ - "heck", - "proc-macro2", - "quote", - "rustversion", - "syn 2.0.101", -] - -[[package]] -name = "syn" -version = "1.0.109" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" -dependencies = [ - "proc-macro2", - "quote", - "unicode-ident", -] - -[[package]] -name = "syn" -version = "2.0.101" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ce2b7fc941b3a24138a0a7cf8e858bfc6a992e7978a068a5c760deb0ed43caf" -dependencies = [ - "proc-macro2", - "quote", - "unicode-ident", -] - -[[package]] -name = "thiserror" -version = "1.0.69" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" -dependencies = [ - "thiserror-impl 1.0.69", -] - -[[package]] -name = "thiserror" -version = "2.0.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "567b8a2dae586314f7be2a752ec7474332959c6460e02bde30d702a66d488708" -dependencies = [ - "thiserror-impl 2.0.12", -] - -[[package]] -name = "thiserror-impl" -version = "1.0.69" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.101", -] - -[[package]] -name = "thiserror-impl" -version = "2.0.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f7cf42b4507d8ea322120659672cf1b9dbb93f8f2d4ecfd6e51350ff5b17a1d" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.101", -] - -[[package]] -name = "time" -version = "0.3.41" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a7619e19bc266e0f9c5e6686659d394bc57973859340060a69221e57dbc0c40" -dependencies = [ - "deranged", - "itoa", - "num-conv", - "powerfmt", - "serde", - "time-core", - "time-macros", -] - -[[package]] -name = "time-core" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c9e9a38711f559d9e3ce1cdb06dd7c5b8ea546bc90052da6d06bb76da74bb07c" - -[[package]] -name = "time-macros" -version = "0.2.22" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3526739392ec93fd8b359c8e98514cb3e8e021beb4e5f597b00a0221f8ed8a49" -dependencies = [ - "num-conv", - "time-core", -] - -[[package]] -name = "token_transfer_backend" -version = "0.1.0" -dependencies = [ - "candid", - "ic-cdk", - "icrc-ledger-client-cdk", - "icrc-ledger-types", - "serde", -] - -[[package]] -name = "typed-arena" -version = "2.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6af6ae20167a9ece4bcb41af5b80f8a1f1df981f6391189ce00fd257af04126a" - -[[package]] -name = "typenum" -version = "1.18.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1dccffe3ce07af9386bfd29e80c0ab1a8205a2fc34e4bcd40364df902cfa8f3f" - -[[package]] -name = "unicode-ident" -version = "1.0.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" - -[[package]] -name = "unicode-width" -version = "0.1.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" - -[[package]] -name = "version_check" -version = "0.9.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" - -[[package]] -name = "windows-sys" -version = "0.59.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" -dependencies = [ - "windows-targets", -] - -[[package]] -name = "windows-targets" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" -dependencies = [ - "windows_aarch64_gnullvm", - "windows_aarch64_msvc", - "windows_i686_gnu", - "windows_i686_gnullvm", - "windows_i686_msvc", - "windows_x86_64_gnu", - "windows_x86_64_gnullvm", - "windows_x86_64_msvc", -] - -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" - -[[package]] -name = "windows_aarch64_msvc" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" - -[[package]] -name = "windows_i686_gnu" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" - -[[package]] -name = "windows_i686_gnullvm" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" - -[[package]] -name = "windows_i686_msvc" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" - -[[package]] -name = "windows_x86_64_gnu" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" - -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" - -[[package]] -name = "windows_x86_64_msvc" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" diff --git a/rust/token_transfer/Cargo.toml b/rust/token_transfer/Cargo.toml deleted file mode 100644 index bbd8593f3..000000000 --- a/rust/token_transfer/Cargo.toml +++ /dev/null @@ -1,5 +0,0 @@ -[workspace] -members = [ - "src/token_transfer_backend" -] -resolver = "2" diff --git a/rust/token_transfer/README.md b/rust/token_transfer/README.md deleted file mode 100644 index ef8795fb4..000000000 --- a/rust/token_transfer/README.md +++ /dev/null @@ -1,322 +0,0 @@ -# Token transfer - - -Token transfer is a canister that can transfer ICRC-1 tokens from its account to other accounts. It is an example of a canister that uses an ICRC-1 ledger canister. Sample code is available in [Motoko](https://github.com/dfinity/examples/tree/master/motoko/token_transfer) and [Rust](https://github.com/dfinity/examples/tree/master/rust/token_transfer). - -## Architecture - -The sample code revolves around one core transfer function which takes as input the amount of tokens to transfer, the `Account` to which to transfer tokens and returns either success or an error in case e.g. the token transfer canister doesn’t have enough tokens to do the transfer. In case of success, a unique identifier of the transaction is returned. - -This sample will use the Rust variant. - -## Prerequisites - -This example requires an installation of: - -- [x] Install the [IC SDK](https://internetcomputer.org/docs/current/developer-docs/getting-started/install). -- [x] Clone the example dapp project: `git clone https://github.com/dfinity/examples` - -## Step 1: Create a new `dfx` project and navigate into the project's directory - -```bash -dfx new --type=rust token_transfer --no-frontend -cd token_transfer -``` - -## Step 2: Determine ICRC-1 ledger file locations - -:::info - -You can read more about how to [setup the ICRC-1 ledger locally](https://internetcomputer.org/docs/current/developer-docs/defi/icrc-1/icrc1-ledger-setup). - -::: - -Go to the [releases overview](https://dashboard.internetcomputer.org/releases) and copy the latest replica binary revision. - -The URL for the ledger Wasm module is `https://download.dfinity.systems/ic//canisters/ic-icrc1-ledger.wasm.gz`. - -The URL for the ledger .did file is `https://raw.githubusercontent.com/dfinity/ic//rs/rosetta-api/icrc1/ledger/ledger.did`. - -**OPTIONAL:** -If you want to make sure, you have the latest ICRC-1 ledger files you can run the following script. - -```sh -curl -o download_latest_icrc1_ledger.sh "https://raw.githubusercontent.com/dfinity/ic/69988ae40e4cc0db7ef758da7dd5c0606075e926/rs/rosetta-api/scripts/download_latest_icrc1_ledger.sh" -chmod +x download_latest_icrc1_ledger.sh -./download_latest_icrc1_ledger.sh -``` - -## Step 3: Configure the `dfx.json` file to use the ledger - -Replace its contents with this but adapt the URLs to be the ones you determined in step 2. Note that we are deploying the ICRC-1 ledger to the same canister id the ckBTC ledger uses on mainnet. This will make it easier to interact with it later. - -```json -{ - "canisters": { - "token_transfer_backend": { - "candid": "src/token_transfer_backend/token_transfer_backend.did", - "package": "token_transfer_backend", - "type": "rust", - "dependencies": ["icrc1_ledger_canister"] - }, - "icrc1_ledger_canister": { - "type": "custom", - "candid": "https://raw.githubusercontent.com/dfinity/ic//rs/rosetta-api/icrc1/ledger/ledger.did", - "wasm": "https://download.dfinity.systems/ic//canisters/ic-icrc1-ledger.wasm.gz", - "specified_id": "mxzaz-hqaaa-aaaar-qaada-cai" - } - }, - "defaults": { - "build": { - "args": "", - "packtool": "" - } - }, - "output_env_file": ".env", - "version": 1 -} -``` - -If you chose to download the ICRC-1 ledger files with the script, you need to replace the Candid and Wasm file entries: - -```json -... -"candid": icrc1_ledger.did, -"wasm" : icrc1_ledger.wasm.gz, - ... -``` - -## Step 4: Start a local instance of the Internet Computer - -```bash -dfx start --background --clean -``` - -## Step 5: Create a new identity that will work as a minting account - -```bash -dfx identity new minter --storage-mode plaintext -dfx identity use minter -export MINTER=$(dfx identity get-principal) -``` - -:::info - -Transfers from the minting account will create Mint transactions. Transfers to the minting account will create Burn transactions. - -::: - -## Step 6: Switch back to your default identity - -Record its principal to mint an initial balance to when deploying the ledger: - -```bash -dfx identity use default -export DEFAULT=$(dfx identity get-principal) -``` - -## Step 7: Deploy the ICRC-1 ledger locally - -Take a moment to read the details of the call made below. Not only are you deploying an ICRC-1 ledger canister, you are also: - -- Setting the minting account to the principal you saved in a previous step (`MINTER`) -- Minting 100 tokens to the DEFAULT principal -- Setting the transfer fee to 0.0001 tokens -- Naming the token Local ICRC1 / L-ICRC1 - -```bash -dfx deploy icrc1_ledger_canister --argument "(variant { Init = -record { - token_symbol = \"ICRC1\"; - token_name = \"L-ICRC1\"; - minting_account = record { owner = principal \"${MINTER}\" }; - transfer_fee = 10_000; - metadata = vec {}; - initial_balances = vec { record { record { owner = principal \"${DEFAULT}\"; }; 10_000_000_000; }; }; - archive_options = record { - num_blocks_to_archive = 1000; - trigger_threshold = 2000; - controller_id = principal \"${MINTER}\"; - }; - } -})" -``` - -If successful, the output should be: - -```bash -Deployed canisters. -URLs: - Backend canister via Candid interface: - icrc1_ledger_canister: http://127.0.0.1:4943/?canisterId=bnz7o-iuaaa-aaaaa-qaaaa-cai&id=mxzaz-hqaaa-aaaar-qaada-cai -``` - -## Step 8: Verify that the ledger canister is healthy and working as expected - -> [!TIP] -> You can find more information on how to [interact with the ICRC-1 ledger](https://internetcomputer.org/docs/current/developer-docs/defi/icrc-1/using-icrc1-ledger#icrc-1-and-icrc-1-extension-endpoints) - -````bash -dfx canister call icrc1_ledger_canister icrc1_balance_of "(record { - owner = principal \"${DEFAULT}\"; - } -)" -``` - -The output should be: - -```bash -(10_000_000_000 : nat) -```` - -## Step 9: Prepare the token transfer canister - -Replace the contents of the `src/token_transfer_backend/Cargo.toml` file with the following: - -```toml -[package] -name = "token_transfer_backend" -version = "0.1.0" -edition = "2021" - -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html - -[lib] -crate-type = ["cdylib"] - -[dependencies] -candid = "0.10" -ic-cdk = "0.12" -ic-cdk-timers = "0.6" # Feel free to remove this dependency if you don't need timers -icrc-ledger-types = "0.1.12" -serde = "1.0.197" -serde_derive = "1.0.197" -``` - -Replace the contents of the `src/token_transfer_backend/src/lib.rs` file with the following: - -```rust -use candid::{CandidType, Deserialize, Principal}; -use icrc_ledger_types::icrc1::account::Account; -use icrc_ledger_types::icrc1::transfer::{BlockIndex, NumTokens, TransferArg, TransferError}; -use serde::Serialize; - -#[derive(CandidType, Deserialize, Serialize)] -pub struct TransferArgs { - amount: NumTokens, - to_account: Account, -} - -#[ic_cdk::update] -async fn transfer(args: TransferArgs) -> Result { - ic_cdk::println!( - "Transferring {} tokens to account {}", - &args.amount, - &args.to_account, - ); - - let transfer_args: TransferArg = TransferArg { - // can be used to distinguish between transactions - memo: None, - // the amount we want to transfer - amount: args.amount, - // we want to transfer tokens from the default subaccount of the canister - from_subaccount: None, - // if not specified, the default fee for the canister is used - fee: None, - // the account we want to transfer tokens to - to: args.to_account, - // a timestamp indicating when the transaction was created by the caller; if it is not specified by the caller then this is set to the current ICP time - created_at_time: None, - }; - - // 1. Asynchronously call another canister function using `ic_cdk::call`. - ic_cdk::call::<(TransferArg,), (Result,)>( - // 2. Convert a textual representation of a Principal into an actual `Principal` object. The principal is the one we specified in `dfx.json`. - // `expect` will panic if the conversion fails, ensuring the code does not proceed with an invalid principal. - Principal::from_text("mxzaz-hqaaa-aaaar-qaada-cai") - .expect("Could not decode the principal."), - // 3. Specify the method name on the target canister to be called, in this case, "icrc1_transfer". - "icrc1_transfer", - // 4. Provide the arguments for the call in a tuple, here `transfer_args` is encapsulated as a single-element tuple. - (transfer_args,), - ) - .await // 5. Await the completion of the asynchronous call, pausing the execution until the future is resolved. - // 6. Apply `map_err` to transform any network or system errors encountered during the call into a more readable string format. - // The `?` operator is then used to propagate errors: if the result is an `Err`, it returns from the function with that error, - // otherwise, it unwraps the `Ok` value, allowing the chain to continue. - .map_err(|e| format!("failed to call ledger: {:?}", e))? - // 7. Access the first element of the tuple, which is the `Result`, for further processing. - .0 - // 8. Use `map_err` again to transform any specific ledger transfer errors into a readable string format, facilitating error handling and debugging. - .map_err(|e| format!("ledger transfer error {:?}", e)) -} - -// Enable Candid export (see https://internetcomputer.org/docs/current/developer-docs/backend/rust/generating-candid) -ic_cdk::export_candid!(); - -``` - -Replace the contents of the `src/token_transfer_backend/token_transfer_backend.did` file with the following: - -> [!TIP] -> The `token_transfer_backend.did` file is a Candid file that describes the service interface of the canister. It was generated from the Rust code using the `candid-extractor` tool. You can read more about the [necessary steps](https://internetcomputer.org/docs/current/developer-docs/backend/rust/generating-candid). - -```did -type Account = record { owner : principal; subaccount : opt vec nat8 }; -type Result = variant { Ok : nat; Err : text }; -type TransferArgs = record { to_account : Account; amount : nat }; -service : { transfer : (TransferArgs) -> (Result) } - -``` - -## Step 10: Deploy the token transfer canister - -```bash -dfx deploy token_transfer_backend -``` - -## Step 11: Transfer funds to your canister - -> [!WARNING] -> Make sure that you are using the default `dfx` account that we minted tokens to in step 7 for the following steps. - -Make the following call to transfer 10 tokens to the canister: - -```bash -dfx canister call icrc1_ledger_canister icrc1_transfer "(record { - to = record { - owner = principal \"$(dfx canister id token_transfer_backend)\"; - }; - amount = 1_000_000_000; -})" -``` - -If successful, the output should be: - -```bash -(variant { Ok = 1 : nat }) -``` - -## Step 12: Transfer funds from the canister - -Now that the canister owns tokens on the ledger, you can transfer 1 token from the canister to another account, in this case back to the default account: - -```bash -dfx canister call token_transfer_backend transfer "(record { - amount = 100_000_000; - to_account = record { - owner = principal \"$(dfx identity get-principal)\"; - }; -})" -``` - -## Security considerations and best practices - -If you base your application on this example, 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. This example may not implement all the best practices. - -For example, the following aspects are particularly relevant for this app: - -- [Inter-canister calls and rollbacks](https://internetcomputer.org/docs/current/developer-docs/security/security-best-practices/overview), since issues around inter-canister calls (here the ledger) can e.g. lead to time-of-check time-of-use or double spending security bugs. -- [Certify query responses if they are relevant for security](https://internetcomputer.org/docs/current/references/security/general-security-best-practices#certify-query-responses-if-they-are-relevant-for-security), since this is essential when e.g. displaying important financial data in the frontend that may be used by users to decide on future transactions. In this example, this is e.g. relevant for the call to `canisterBalance`. -- [Use a decentralized governance system like SNS to make a canister have a decentralized controller](https://internetcomputer.org/docs/current/developer-docs/security/security-best-practices/overview), since decentralizing control is a fundamental aspect of decentralized finance applications. diff --git a/rust/token_transfer/demo.sh b/rust/token_transfer/demo.sh deleted file mode 100755 index 1e7a87619..000000000 --- a/rust/token_transfer/demo.sh +++ /dev/null @@ -1,48 +0,0 @@ -#!/usr/bin/env bash -dfx stop -# set -e -# trap 'dfx stop' EXIT - -echo "===========SETUP=========" -dfx start --background --clean -dfx identity new alice_token_transfer --storage-mode plaintext --force -export MINTER=$(dfx --identity anonymous identity get-principal) -export DEFAULT=$(dfx identity get-principal) -dfx deploy icrc1_ledger_canister --argument "(variant { Init = -record { - token_symbol = \"ICRC1\"; - token_name = \"L-ICRC1\"; - minting_account = record { owner = principal \"${MINTER}\" }; - transfer_fee = 10_000; - metadata = vec {}; - initial_balances = vec { record { record { owner = principal \"${DEFAULT}\"; }; 10_000_000_000; }; }; - archive_options = record { - num_blocks_to_archive = 1000; - trigger_threshold = 2000; - controller_id = principal \"${MINTER}\"; - }; - } -})" -dfx canister call icrc1_ledger_canister icrc1_balance_of "(record { - owner = principal \"${DEFAULT}\"; - } -)" -echo "===========SETUP DONE=========" - -dfx deploy token_transfer_backend - -dfx canister call icrc1_ledger_canister icrc1_transfer "(record { - to = record { - owner = principal \"$(dfx canister id token_transfer_backend)\"; - }; - amount = 1_000_000_000; -})" - -dfx canister call token_transfer_backend transfer "(record { - amount = 100_000_000; - to_account = record { - owner = principal \"$(dfx identity get-principal)\"; - }; -})" - -echo "DONE" \ No newline at end of file diff --git a/rust/token_transfer/dfx.json b/rust/token_transfer/dfx.json deleted file mode 100644 index a22ae0e5a..000000000 --- a/rust/token_transfer/dfx.json +++ /dev/null @@ -1,26 +0,0 @@ -{ - "canisters": { - "token_transfer_backend": { - "candid": "src/token_transfer_backend/token_transfer_backend.did", - "package": "token_transfer_backend", - "type": "rust", - "dependencies": [ - "icrc1_ledger_canister" - ] - }, - "icrc1_ledger_canister": { - "type": "custom", - "candid": "https://raw.githubusercontent.com/dfinity/ic/d87954601e4b22972899e9957e800406a0a6b929/rs/rosetta-api/icrc1/ledger/ledger.did", - "wasm": "https://download.dfinity.systems/ic/d87954601e4b22972899e9957e800406a0a6b929/canisters/ic-icrc1-ledger.wasm.gz", - "specified_id": "mxzaz-hqaaa-aaaar-qaada-cai" - } - }, - "defaults": { - "build": { - "args": "", - "packtool": "" - } - }, - "output_env_file": ".env", - "version": 1 -} \ No newline at end of file diff --git a/rust/token_transfer/src/token_transfer_backend/Cargo.toml b/rust/token_transfer/src/token_transfer_backend/Cargo.toml deleted file mode 100644 index 62ca011bf..000000000 --- a/rust/token_transfer/src/token_transfer_backend/Cargo.toml +++ /dev/null @@ -1,16 +0,0 @@ -[package] -name = "token_transfer_backend" -version = "0.1.0" -edition = "2021" - -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html - -[lib] -crate-type = ["cdylib"] - -[dependencies] -candid = "0.10.14" -ic-cdk = "0.18.3" -serde = "1.0.219" -icrc-ledger-types = "0.1.12" -icrc-ledger-client-cdk = "0.1.3" diff --git a/rust/token_transfer/src/token_transfer_backend/src/lib.rs b/rust/token_transfer/src/token_transfer_backend/src/lib.rs deleted file mode 100644 index aef1b531d..000000000 --- a/rust/token_transfer/src/token_transfer_backend/src/lib.rs +++ /dev/null @@ -1,58 +0,0 @@ -use candid::{CandidType, Deserialize, Principal}; -use icrc_ledger_client_cdk::{CdkRuntime, ICRC1Client}; -use icrc_ledger_types::icrc1::account::Account; -use icrc_ledger_types::icrc1::transfer::{BlockIndex, NumTokens, TransferArg}; -use serde::Serialize; - -#[derive(CandidType, Deserialize, Serialize)] -pub struct TransferArgs { - amount: NumTokens, - to_account: Account, -} - -#[ic_cdk::update] -async fn transfer(args: TransferArgs) -> Result { - ic_cdk::println!( - "Transferring {} tokens to account {}", - &args.amount, - &args.to_account, - ); - - let transfer_args: TransferArg = TransferArg { - // can be used to distinguish between transactions - memo: None, - // the amount we want to transfer - amount: args.amount, - // we want to transfer tokens from the default subaccount of the canister - from_subaccount: None, - // if not specified, the default fee for the canister is used - fee: None, - // the account we want to transfer tokens to - to: args.to_account, - // a timestamp indicating when the transaction was created by the caller; if it is not specified by the caller then this is set to the current ICP time - created_at_time: None, - }; - - // Convert a textual representation of a Principal into an actual `Principal` object. The principal is the one we specified in `dfx.json`. - // `expect` will panic if the conversion fails, ensuring the code does not proceed with an invalid principal. - let ledger_canister_id = Principal::from_text("mxzaz-hqaaa-aaaar-qaada-cai") - .expect("Could not decode the principal."); - - let client = ICRC1Client { - runtime: CdkRuntime, - ledger_canister_id, - }; - - client - .transfer(transfer_args) - .await - // Apply `map_err` to transform any network or system errors encountered during the call into a more readable string format. - // The `?` operator is then used to propagate errors: if the result is an `Err`, it returns from the function with that error, - // otherwise, it unwraps the `Ok` value, allowing the chain to continue. - .map_err(|e| format!("failed to call ledger: {:?}", e))? - // Use `map_err` again to handle any specific ledger transfer errors, converting them into a string format for easier debugging. - .map_err(|e| format!("ledger transfer error {:?}", e)) -} - -// Enable Candid export (see https://internetcomputer.org/docs/current/developer-docs/backend/rust/generating-candid) -ic_cdk::export_candid!(); diff --git a/rust/token_transfer/src/token_transfer_backend/token_transfer_backend.did b/rust/token_transfer/src/token_transfer_backend/token_transfer_backend.did deleted file mode 100644 index f981bd8d6..000000000 --- a/rust/token_transfer/src/token_transfer_backend/token_transfer_backend.did +++ /dev/null @@ -1,4 +0,0 @@ -type Account = record { owner : principal; subaccount : opt vec nat8 }; -type Result = variant { Ok : nat; Err : text }; -type TransferArgs = record { to_account : Account; amount : nat }; -service : { transfer : (TransferArgs) -> (Result) } diff --git a/rust/token_transfer_from/.gitignore b/rust/token_transfer_from/.gitignore deleted file mode 100644 index 49c89a1c9..000000000 --- a/rust/token_transfer_from/.gitignore +++ /dev/null @@ -1,25 +0,0 @@ -# Various IDEs and Editors -.vscode/ -.idea/ -**/*~ - -# Mac OSX temporary files -.DS_Store -**/.DS_Store - -# dfx temporary files -.dfx/ - -# generated files -**/declarations/ - -# rust -target/ - -# frontend code -node_modules/ -dist/ -.svelte-kit/ - -# environment variables -.env diff --git a/rust/token_transfer_from/Cargo.lock b/rust/token_transfer_from/Cargo.lock deleted file mode 100644 index 67b499c42..000000000 --- a/rust/token_transfer_from/Cargo.lock +++ /dev/null @@ -1,861 +0,0 @@ -# This file is automatically @generated by Cargo. -# It is not intended for manual editing. -version = 4 - -[[package]] -name = "anyhow" -version = "1.0.98" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e16d2d3311acee920a9eb8d33b8cbc1787ce4a264e85f964c2404b969bdcd487" - -[[package]] -name = "arrayvec" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23b62fc65de8e4e7f52534fb52b0f3ed04746ae267519eef2a83941e8085068b" - -[[package]] -name = "async-trait" -version = "0.1.89" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.101", -] - -[[package]] -name = "autocfg" -version = "1.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" - -[[package]] -name = "base32" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23ce669cd6c8588f79e15cf450314f9638f967fc5770ff1c7c1deb0925ea7cfa" - -[[package]] -name = "binread" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "16598dfc8e6578e9b597d9910ba2e73618385dc9f4b1d43dd92c349d6be6418f" -dependencies = [ - "binread_derive", - "lazy_static", - "rustversion", -] - -[[package]] -name = "binread_derive" -version = "2.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d9672209df1714ee804b1f4d4f68c8eb2a90b1f7a07acf472f88ce198ef1fed" -dependencies = [ - "either", - "proc-macro2", - "quote", - "syn 1.0.109", -] - -[[package]] -name = "block-buffer" -version = "0.10.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" -dependencies = [ - "generic-array", -] - -[[package]] -name = "byteorder" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" - -[[package]] -name = "candid" -version = "0.10.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eaac522d18020d5fbc8320ecb12a9b13b2137ae31133da2d42fa256a825507c4" -dependencies = [ - "anyhow", - "binread", - "byteorder", - "candid_derive", - "hex", - "ic_principal", - "leb128", - "num-bigint", - "num-traits", - "paste", - "pretty", - "serde", - "serde_bytes", - "stacker", - "thiserror 1.0.69", -] - -[[package]] -name = "candid_derive" -version = "0.10.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a1b4fddbd462182050989068d53604a91a3d0f117c3c8316c6818023df00add" -dependencies = [ - "lazy_static", - "proc-macro2", - "quote", - "syn 2.0.101", -] - -[[package]] -name = "cc" -version = "1.2.23" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f4ac86a9e5bc1e2b3449ab9d7d3a6a405e3d1bb28d7b9be8614f55846ae3766" -dependencies = [ - "shlex", -] - -[[package]] -name = "cfg-if" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" - -[[package]] -name = "cpufeatures" -version = "0.2.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" -dependencies = [ - "libc", -] - -[[package]] -name = "crc32fast" -version = "1.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a97769d94ddab943e4510d138150169a2758b5ef3eb191a9ee688de3e23ef7b3" -dependencies = [ - "cfg-if", -] - -[[package]] -name = "crypto-common" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" -dependencies = [ - "generic-array", - "typenum", -] - -[[package]] -name = "darling" -version = "0.20.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc7f46116c46ff9ab3eb1597a45688b6715c6e628b5c133e288e709a29bcb4ee" -dependencies = [ - "darling_core", - "darling_macro", -] - -[[package]] -name = "darling_core" -version = "0.20.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d00b9596d185e565c2207a0b01f8bd1a135483d02d9b7b0a54b11da8d53412e" -dependencies = [ - "fnv", - "ident_case", - "proc-macro2", - "quote", - "strsim", - "syn 2.0.101", -] - -[[package]] -name = "darling_macro" -version = "0.20.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead" -dependencies = [ - "darling_core", - "quote", - "syn 2.0.101", -] - -[[package]] -name = "data-encoding" -version = "2.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a2330da5de22e8a3cb63252ce2abb30116bf5265e89c0e01bc17015ce30a476" - -[[package]] -name = "deranged" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c9e6a11ca8224451684bc0d7d5a7adbf8f2fd6887261a1cfc3c0432f9d4068e" -dependencies = [ - "powerfmt", -] - -[[package]] -name = "digest" -version = "0.10.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" -dependencies = [ - "block-buffer", - "crypto-common", -] - -[[package]] -name = "either" -version = "1.15.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" - -[[package]] -name = "fnv" -version = "1.0.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" - -[[package]] -name = "generic-array" -version = "0.14.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" -dependencies = [ - "typenum", - "version_check", -] - -[[package]] -name = "heck" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" - -[[package]] -name = "hex" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" -dependencies = [ - "serde", -] - -[[package]] -name = "ic-cdk" -version = "0.18.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4efb278f5d3ef033b3eed7f01f1096eaf67701896aa5ef69f5eddf5a84833dc0" -dependencies = [ - "candid", - "ic-cdk-executor", - "ic-cdk-macros", - "ic-error-types", - "ic-management-canister-types", - "ic0", - "serde", - "serde_bytes", - "slotmap", - "thiserror 2.0.12", -] - -[[package]] -name = "ic-cdk-executor" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "99f4ee8930fd2e491177e2eb7fff53ee1c407c13b9582bdc7d6920cf83109a2d" -dependencies = [ - "ic0", - "slotmap", -] - -[[package]] -name = "ic-cdk-macros" -version = "0.18.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7eb14c5d691cc9d72bb95459b4761e3a4b3444b85a63d17555d5ddd782969a1e" -dependencies = [ - "candid", - "darling", - "proc-macro2", - "quote", - "syn 2.0.101", -] - -[[package]] -name = "ic-error-types" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbeeb3d91aa179d6496d7293becdacedfc413c825cac79fd54ea1906f003ee55" -dependencies = [ - "serde", - "strum", - "strum_macros", -] - -[[package]] -name = "ic-management-canister-types" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea7e5b8a0f7c3b320d9450ac950547db4f24a31601b5d398f9680b64427455d2" -dependencies = [ - "candid", - "serde", - "serde_bytes", -] - -[[package]] -name = "ic-stable-structures" -version = "0.6.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0f5684f577e0146738cd11afed789109c4f51ba963c75823c48c1501dc53278" -dependencies = [ - "ic_principal", -] - -[[package]] -name = "ic0" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8877193e1921b5fd16accb0305eb46016868cd1935b05c05eca0ec007b943272" - -[[package]] -name = "ic_principal" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1762deb6f7c8d8c2bdee4b6c5a47b60195b74e9b5280faa5ba29692f8e17429c" -dependencies = [ - "crc32fast", - "data-encoding", - "serde", - "sha2", - "thiserror 1.0.69", -] - -[[package]] -name = "icrc-cbor" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90569d2894d9536c5416943556ac6339df249f06611b3c41029196b39e0dd119" -dependencies = [ - "candid", - "minicbor", - "num-bigint", - "num-traits", -] - -[[package]] -name = "icrc-ledger-client" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6662452c60a8636cd8f1293246de3fdc4652ff2a37af6c7e2522fb567efe67b2" -dependencies = [ - "async-trait", - "candid", - "icrc-ledger-types", -] - -[[package]] -name = "icrc-ledger-client-cdk" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e72bf46503a9ff15378e8d5560e51679f11d6548ecac7939ac00bf05d37f7ff8" -dependencies = [ - "async-trait", - "candid", - "ic-cdk", - "icrc-ledger-client", -] - -[[package]] -name = "icrc-ledger-types" -version = "0.1.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aafb78e620b2cc2b000cd745c0504dfb23a828acc3dd6f1baef208cd6c471e32" -dependencies = [ - "base32", - "candid", - "crc32fast", - "hex", - "ic-stable-structures", - "icrc-cbor", - "itertools", - "minicbor", - "num-bigint", - "num-traits", - "serde", - "serde_bytes", - "sha2", - "strum", - "strum_macros", - "time", -] - -[[package]] -name = "ident_case" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" - -[[package]] -name = "itertools" -version = "0.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569" -dependencies = [ - "either", -] - -[[package]] -name = "itoa" -version = "1.0.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" - -[[package]] -name = "lazy_static" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" - -[[package]] -name = "leb128" -version = "0.2.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "884e2677b40cc8c339eaefcb701c32ef1fd2493d71118dc0ca4b6a736c93bd67" - -[[package]] -name = "libc" -version = "0.2.172" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d750af042f7ef4f724306de029d18836c26c1765a54a6a3f094cbd23a7267ffa" - -[[package]] -name = "libm" -version = "0.2.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f9fbbcab51052fe104eb5e5d351cf728d30a5be1fe14d9be8a3b097481fb97de" - -[[package]] -name = "minicbor" -version = "0.19.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7005aaf257a59ff4de471a9d5538ec868a21586534fff7f85dd97d4043a6139" -dependencies = [ - "minicbor-derive", -] - -[[package]] -name = "minicbor-derive" -version = "0.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1154809406efdb7982841adb6311b3d095b46f78342dd646736122fe6b19e267" -dependencies = [ - "proc-macro2", - "quote", - "syn 1.0.109", -] - -[[package]] -name = "num-bigint" -version = "0.4.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" -dependencies = [ - "num-integer", - "num-traits", - "serde", -] - -[[package]] -name = "num-conv" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" - -[[package]] -name = "num-integer" -version = "0.1.46" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" -dependencies = [ - "num-traits", -] - -[[package]] -name = "num-traits" -version = "0.2.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" -dependencies = [ - "autocfg", - "libm", -] - -[[package]] -name = "paste" -version = "1.0.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" - -[[package]] -name = "powerfmt" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" - -[[package]] -name = "pretty" -version = "0.12.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac98773b7109bc75f475ab5a134c9b64b87e59d776d31098d8f346922396a477" -dependencies = [ - "arrayvec", - "typed-arena", - "unicode-width", -] - -[[package]] -name = "proc-macro2" -version = "1.0.95" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778" -dependencies = [ - "unicode-ident", -] - -[[package]] -name = "psm" -version = "0.1.26" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e944464ec8536cd1beb0bbfd96987eb5e3b72f2ecdafdc5c769a37f1fa2ae1f" -dependencies = [ - "cc", -] - -[[package]] -name = "quote" -version = "1.0.40" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" -dependencies = [ - "proc-macro2", -] - -[[package]] -name = "rustversion" -version = "1.0.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eded382c5f5f786b989652c49544c4877d9f015cc22e145a5ea8ea66c2921cd2" - -[[package]] -name = "serde" -version = "1.0.219" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" -dependencies = [ - "serde_derive", -] - -[[package]] -name = "serde_bytes" -version = "0.11.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8437fd221bde2d4ca316d61b90e337e9e702b3820b87d63caa9ba6c02bd06d96" -dependencies = [ - "serde", -] - -[[package]] -name = "serde_derive" -version = "1.0.219" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.101", -] - -[[package]] -name = "sha2" -version = "0.10.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" -dependencies = [ - "cfg-if", - "cpufeatures", - "digest", -] - -[[package]] -name = "shlex" -version = "1.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" - -[[package]] -name = "slotmap" -version = "1.0.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dbff4acf519f630b3a3ddcfaea6c06b42174d9a44bc70c620e9ed1649d58b82a" -dependencies = [ - "version_check", -] - -[[package]] -name = "stacker" -version = "0.1.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cddb07e32ddb770749da91081d8d0ac3a16f1a569a18b20348cd371f5dead06b" -dependencies = [ - "cc", - "cfg-if", - "libc", - "psm", - "windows-sys", -] - -[[package]] -name = "strsim" -version = "0.11.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" - -[[package]] -name = "strum" -version = "0.26.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fec0f0aef304996cf250b31b5a10dee7980c85da9d759361292b8bca5a18f06" -dependencies = [ - "strum_macros", -] - -[[package]] -name = "strum_macros" -version = "0.26.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c6bee85a5a24955dc440386795aa378cd9cf82acd5f764469152d2270e581be" -dependencies = [ - "heck", - "proc-macro2", - "quote", - "rustversion", - "syn 2.0.101", -] - -[[package]] -name = "syn" -version = "1.0.109" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" -dependencies = [ - "proc-macro2", - "quote", - "unicode-ident", -] - -[[package]] -name = "syn" -version = "2.0.101" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ce2b7fc941b3a24138a0a7cf8e858bfc6a992e7978a068a5c760deb0ed43caf" -dependencies = [ - "proc-macro2", - "quote", - "unicode-ident", -] - -[[package]] -name = "thiserror" -version = "1.0.69" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" -dependencies = [ - "thiserror-impl 1.0.69", -] - -[[package]] -name = "thiserror" -version = "2.0.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "567b8a2dae586314f7be2a752ec7474332959c6460e02bde30d702a66d488708" -dependencies = [ - "thiserror-impl 2.0.12", -] - -[[package]] -name = "thiserror-impl" -version = "1.0.69" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.101", -] - -[[package]] -name = "thiserror-impl" -version = "2.0.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f7cf42b4507d8ea322120659672cf1b9dbb93f8f2d4ecfd6e51350ff5b17a1d" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.101", -] - -[[package]] -name = "time" -version = "0.3.41" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a7619e19bc266e0f9c5e6686659d394bc57973859340060a69221e57dbc0c40" -dependencies = [ - "deranged", - "itoa", - "num-conv", - "powerfmt", - "serde", - "time-core", - "time-macros", -] - -[[package]] -name = "time-core" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c9e9a38711f559d9e3ce1cdb06dd7c5b8ea546bc90052da6d06bb76da74bb07c" - -[[package]] -name = "time-macros" -version = "0.2.22" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3526739392ec93fd8b359c8e98514cb3e8e021beb4e5f597b00a0221f8ed8a49" -dependencies = [ - "num-conv", - "time-core", -] - -[[package]] -name = "token_transfer_from_backend" -version = "0.1.0" -dependencies = [ - "candid", - "ic-cdk", - "icrc-ledger-client-cdk", - "icrc-ledger-types", - "serde", -] - -[[package]] -name = "typed-arena" -version = "2.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6af6ae20167a9ece4bcb41af5b80f8a1f1df981f6391189ce00fd257af04126a" - -[[package]] -name = "typenum" -version = "1.18.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1dccffe3ce07af9386bfd29e80c0ab1a8205a2fc34e4bcd40364df902cfa8f3f" - -[[package]] -name = "unicode-ident" -version = "1.0.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" - -[[package]] -name = "unicode-width" -version = "0.1.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" - -[[package]] -name = "version_check" -version = "0.9.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" - -[[package]] -name = "windows-sys" -version = "0.59.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" -dependencies = [ - "windows-targets", -] - -[[package]] -name = "windows-targets" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" -dependencies = [ - "windows_aarch64_gnullvm", - "windows_aarch64_msvc", - "windows_i686_gnu", - "windows_i686_gnullvm", - "windows_i686_msvc", - "windows_x86_64_gnu", - "windows_x86_64_gnullvm", - "windows_x86_64_msvc", -] - -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" - -[[package]] -name = "windows_aarch64_msvc" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" - -[[package]] -name = "windows_i686_gnu" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" - -[[package]] -name = "windows_i686_gnullvm" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" - -[[package]] -name = "windows_i686_msvc" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" - -[[package]] -name = "windows_x86_64_gnu" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" - -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" - -[[package]] -name = "windows_x86_64_msvc" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" diff --git a/rust/token_transfer_from/Cargo.toml b/rust/token_transfer_from/Cargo.toml deleted file mode 100644 index bf17363cc..000000000 --- a/rust/token_transfer_from/Cargo.toml +++ /dev/null @@ -1,5 +0,0 @@ -[workspace] -members = [ - "src/token_transfer_from_backend" -] -resolver = "2" diff --git a/rust/token_transfer_from/README.md b/rust/token_transfer_from/README.md deleted file mode 100644 index af39abe06..000000000 --- a/rust/token_transfer_from/README.md +++ /dev/null @@ -1,315 +0,0 @@ -# Token transfer_from - -`token_transfer_from_backend` is a canister that can transfer ICRC-1 tokens on behalf of accounts to other accounts. It is an example of a canister that uses an ICRC-1 ledger canister that supports the [ICRC-2](https://github.com/dfinity/ICRC-1/tree/main/standards/ICRC-2) approve and transfer from standard. Sample code is available in [Motoko](https://github.com/dfinity/examples/tree/master/motoko/token_transfer_from) and [Rust](https://github.com/dfinity/examples/tree/master/rust/token_transfer_from). - -## Architecture - -The sample code revolves around one core transfer function which takes as input the amount of tokens to transfer, the `Account` to which to transfer tokens and returns either success or an error in case e.g. the token transfer canister doesn’t have enough tokens to do the transfer or the caller has not approved the canister to spend their tokens. In case of success, a unique identifier of the transaction is returned. The example code assumes the caller of `transfer` has already approved the token transfer canister to spend their tokens. - -This sample will use the Rust variant. - -## Prerequisites - -This example requires an installation of: - -- [x] Install the [IC SDK](https://internetcomputer.org/docs/current/developer-docs/getting-started/install). -- [x] Clone the example dapp project: `git clone https://github.com/dfinity/examples` - -## Step 1: Create a new `dfx` project and navigate into the project's directory - -```bash -dfx new --type=rust token_transfer_from --no-frontend -cd token_transfer_from -``` - -## Step 2: Determine ICRC-1 ledger file locations - -:::info - -You can read more about how to [setup the ICRC-1 ledger locally](https://internetcomputer.org/docs/current/developer-docs/defi/icrc-1/icrc1-ledger-setup). - -::: - -Go to the [releases overview](https://dashboard.internetcomputer.org/releases) and copy the latest replica binary revision. - -The URL for the ledger Wasm module is `https://download.dfinity.systems/ic//canisters/ic-icrc1-ledger.wasm.gz`. - -The URL for the ledger .did file is `https://raw.githubusercontent.com/dfinity/ic//rs/rosetta-api/icrc1/ledger/ledger.did`. - -**OPTIONAL:** -If you want to make sure, you have the latest ICRC-1 ledger files you can run the following script. - -```sh -curl -o download_latest_icrc1_ledger.sh "https://raw.githubusercontent.com/dfinity/ic/69988ae40e4cc0db7ef758da7dd5c0606075e926/rs/rosetta-api/scripts/download_latest_icrc1_ledger.sh" -chmod +x download_latest_icrc1_ledger.sh -./download_latest_icrc1_ledger.sh -``` - -## Step 3: Configure the `dfx.json` file to use the ledger - -Replace its contents with this but adapt the URLs to be the ones you determined in step 2. Note that we are deploying the ICRC-1 ledger to the same canister id the ckBTC ledger uses on mainnet. This will make it easier to interact with it later. - -```json -{ - "canisters": { - "token_transfer_from_backend": { - "candid": "src/token_transfer_from_backend/token_transfer_from_backend.did", - "package": "token_transfer_from_backend", - "type": "rust" - }, - "icrc1_ledger_canister": { - "type": "custom", - "candid": "https://raw.githubusercontent.com/dfinity/ic//rs/rosetta-api/icrc1/ledger/ledger.did", - "wasm": "https://download.dfinity.systems/ic//canisters/ic-icrc1-ledger.wasm.gz", - "specified_id": "mxzaz-hqaaa-aaaar-qaada-cai" - } - }, - "defaults": { - "build": { - "args": "", - "packtool": "" - } - }, - "output_env_file": ".env", - "version": 1 -} -``` - -If you chose to download the ICRC-1 ledger files with the script, you need to replace the Candid and Wasm file entries: - -``` -... -"candid": icrc1_ledger.did, -"wasm" : icrc1_ledger.wasm.gz, -... -``` - -## Step 4: Start a local replica - -```bash -dfx start --background --clean -``` - -## Step 5: Deploy the ICRC-1 ledger locally - -> [!TIP] -> Transfers from the `minting_account` will create Mint transactions. Transfers to the minting account will create Burn transactions. - -Take a moment to read the details of the call made below. Not only are you deploying an ICRC-1 ledger canister, you are also: - -- Setting the minting account to the anonymous principal (`2vxsx-fae`) -- Minting 100 tokens to the default identity -- Setting the transfer fee to 0.0001 tokens -- Naming the token Local ICRC1 / L-ICRC1 -- Enabling the ICRC-2 standard for the ledger - -```bash -dfx deploy icrc1_ledger_canister --argument "(variant { - Init = record { - token_symbol = \"ICRC1\"; - token_name = \"L-ICRC1\"; - minting_account = record { - owner = principal \"$(dfx identity --identity anonymous get-principal)\" - }; - transfer_fee = 10_000; - metadata = vec {}; - initial_balances = vec { - record { - record { - owner = principal \"$(dfx identity --identity default get-principal)\"; - }; - 10_000_000_000; - }; - }; - archive_options = record { - num_blocks_to_archive = 1000; - trigger_threshold = 2000; - controller_id = principal \"$(dfx identity --identity anonymous get-principal)\"; - }; - feature_flags = opt record { - icrc2 = true; - }; - } -})" -``` - -If successful, the output should be: - -```bash -Deployed canisters. -URLs: - Backend canister via Candid interface: - icrc1_ledger_canister: http://127.0.0.1:4943/?canisterId=bnz7o-iuaaa-aaaaa-qaaaa-cai&id=mxzaz-hqaaa-aaaar-qaada-cai -``` - -## Step 6: Verify that the ledger canister is healthy and working as expected - -> [!TIP] -> You can find more information on how to [interact with the ICRC-1 ledger](https://internetcomputer.org/docs/current/developer-docs/defi/icrc-1/using-icrc1-ledger#icrc-1-and-icrc-1-extension-endpoints) - -````bash -dfx canister call icrc1_ledger_canister icrc1_balance_of "(record { - owner = principal \"$(dfx identity --identity default get-principal)\"; -})" -``` - -The output should be: - -```bash -(10_000_000_000 : nat) -```` - -## Step 7: Prepare the token transfer canister - -Replace the contents of the `src/token_transfer_from_backend/Cargo.toml` file with the following: - -```toml -[package] -name = "token_transfer_from_backend" -version = "0.1.0" -edition = "2021" - -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html - -[lib] -crate-type = ["cdylib"] - -[dependencies] -candid = "0.10" -ic-cdk = "0.12" -ic-cdk-timers = "0.6" # Feel free to remove this dependency if you don't need timers -icrc-ledger-types = "0.1.12" -serde = "1.0.197" -``` - -Replace the contents of the `src/token_transfer_from_backend/src/lib.rs` file with the following: - -```rust -use candid::{CandidType, Deserialize, Principal}; -use icrc_ledger_types::icrc1::account::Account; -use icrc_ledger_types::icrc1::transfer::{BlockIndex, NumTokens}; -use icrc_ledger_types::icrc2::transfer_from::{TransferFromArgs, TransferFromError}; -use serde::Serialize; - -#[derive(CandidType, Deserialize, Serialize)] -pub struct TransferArgs { - amount: NumTokens, - to_account: Account, -} - -#[ic_cdk::update] -async fn transfer(args: TransferArgs) -> Result { - ic_cdk::println!( - "Transferring {} tokens to account {}", - &args.amount, - &args.to_account, - ); - - let transfer_from_args = TransferFromArgs { - // the account we want to transfer tokens from (in this case we assume the caller approved the canister to spend funds on their behalf) - from: Account::from(ic_cdk::caller()), - // can be used to distinguish between transactions - memo: None, - // the amount we want to transfer - amount: args.amount, - // the subaccount we want to spend the tokens from (in this case we assume the default subaccount has been approved) - spender_subaccount: None, - // if not specified, the default fee for the canister is used - fee: None, - // the account we want to transfer tokens to - to: args.to_account, - // a timestamp indicating when the transaction was created by the caller; if it is not specified by the caller then this is set to the current ICP time - created_at_time: None, - }; - - // 1. Asynchronously call another canister function using `ic_cdk::call`. - ic_cdk::call::<(TransferFromArgs,), (Result,)>( - // 2. Convert a textual representation of a Principal into an actual `Principal` object. The principal is the one we specified in `dfx.json`. - // `expect` will panic if the conversion fails, ensuring the code does not proceed with an invalid principal. - Principal::from_text("mxzaz-hqaaa-aaaar-qaada-cai") - .expect("Could not decode the principal."), - // 3. Specify the method name on the target canister to be called, in this case, "icrc1_transfer". - "icrc2_transfer_from", - // 4. Provide the arguments for the call in a tuple, here `transfer_args` is encapsulated as a single-element tuple. - (transfer_from_args,), - ) - .await // 5. Await the completion of the asynchronous call, pausing the execution until the future is resolved. - // 6. Apply `map_err` to transform any network or system errors encountered during the call into a more readable string format. - // The `?` operator is then used to propagate errors: if the result is an `Err`, it returns from the function with that error, - // otherwise, it unwraps the `Ok` value, allowing the chain to continue. - .map_err(|e| format!("failed to call ledger: {:?}", e))? - // 7. Access the first element of the tuple, which is the `Result`, for further processing. - .0 - // 8. Use `map_err` again to transform any specific ledger transfer errors into a readable string format, facilitating error handling and debugging. - .map_err(|e| format!("ledger transfer error {:?}", e)) -} - -// Enable Candid export (see https://internetcomputer.org/docs/current/developer-docs/backend/rust/generating-candid) -ic_cdk::export_candid!(); - -``` - -Replace the contents of the `src/token_transfer_from_backend/token_transfer_from_backend.did` file with the following: - -> [!TIP] -> The `token_transfer_from.did` file is a Candid file that describes the service interface of the canister. It was generated from the Rust code using the `candid-extractor` tool. You can read more about the [necessary steps](https://internetcomputer.org/docs/current/developer-docs/backend/rust/generating-candid). - -```did -type Account = record { owner : principal; subaccount : opt blob }; -type Result = variant { Ok : nat; Err : text }; -type TransferArgs = record { to_account : Account; amount : nat }; -service : { transfer : (TransferArgs) -> (Result) } -``` - -## Step 8: Deploy the token transfer canister - -```bash -dfx deploy token_transfer_from_backend -``` - -## Step 9: Approve the canister to transfer funds on behalf of the user - -> [!TIP] -> Make sure that you are using the default `dfx` account that we minted tokens to in step 5 for the following steps. - -Make the following call to approve the `token_transfer_from_backend` canister to transfer 100 tokens on behalf of the `default` identity: - -```bash -dfx canister call --identity default icrc1_ledger_canister icrc2_approve "( - record { - spender= record { - owner = principal \"$(dfx canister id token_transfer_from_backend)\"; - }; - amount = 10_000_000_000: nat; - } -)" -``` - -If successful, the output should be: - -```bash -(variant { Ok = 1 : nat }) -``` - -## Step 10: Let the canister transfer funds on behalf of the user - -Now that the canister has an approval for the `default` identities tokens on the ledger, the canister can transfer 1 token on behalf of the `default` identity to another account, in this case to the canisters own account. - -```bash -dfx canister call token_transfer_from_backend transfer "(record { - amount = 100_000_000; - to_account = record { - owner = principal \"$(dfx canister id token_transfer_from_backend)\"; - }; -})" -``` - -## Security considerations and best practices - -If you base your application on this example, 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. This example may not implement all the best practices. - -For example, the following aspects are particularly relevant for this app: - -- [Inter-canister calls and rollbacks](https://internetcomputer.org/docs/current/developer-docs/security/security-best-practices/overview), since issues around inter-canister calls (here the ledger) can e.g. lead to time-of-check time-of-use or double spending security bugs. -- [Certify query responses if they are relevant for security](https://internetcomputer.org/docs/current/references/security/general-security-best-practices#certify-query-responses-if-they-are-relevant-for-security), since this is essential when e.g. displaying important financial data in the frontend that may be used by users to decide on future transactions. -- [Use a decentralized governance system like SNS to make a canister have a decentralized controller](https://internetcomputer.org/docs/current/developer-docs/security/security-best-practices/overview), since decentralizing control is a fundamental aspect of decentralized finance applications. diff --git a/rust/token_transfer_from/demo.sh b/rust/token_transfer_from/demo.sh deleted file mode 100755 index e7c43bf43..000000000 --- a/rust/token_transfer_from/demo.sh +++ /dev/null @@ -1,59 +0,0 @@ -#!/usr/bin/env bash -dfx stop -set -e -trap 'dfx stop' EXIT - -echo "===========SETUP=========" -dfx start --background --clean -dfx deploy icrc1_ledger_canister --argument "(variant { - Init = record { - token_symbol = \"ICRC1\"; - token_name = \"L-ICRC1\"; - minting_account = record { - owner = principal \"$(dfx identity --identity anonymous get-principal)\" - }; - transfer_fee = 10_000; - metadata = vec {}; - initial_balances = vec { - record { - record { - owner = principal \"$(dfx identity --identity default get-principal)\"; - }; - 10_000_000_000; - }; - }; - archive_options = record { - num_blocks_to_archive = 1000; - trigger_threshold = 2000; - controller_id = principal \"$(dfx identity --identity anonymous get-principal)\"; - }; - feature_flags = opt record { - icrc2 = true; - }; - } -})" -dfx canister call icrc1_ledger_canister icrc1_balance_of "(record { - owner = principal \"$(dfx identity --identity default get-principal)\"; -})" -echo "===========SETUP DONE=========" - -dfx deploy token_transfer_from_backend - -# approve the token_transfer_from_backend canister to spend 100 tokens -dfx canister call --identity default icrc1_ledger_canister icrc2_approve "( - record { - spender= record { - owner = principal \"$(dfx canister id token_transfer_from_backend)\"; - }; - amount = 10_000_000_000: nat; - } -)" - -dfx canister call token_transfer_from_backend transfer "(record { - amount = 100_000_000; - to_account = record { - owner = principal \"$(dfx canister id token_transfer_from_backend)\"; - }; -})" - -echo "DONE" \ No newline at end of file diff --git a/rust/token_transfer_from/dfx.json b/rust/token_transfer_from/dfx.json deleted file mode 100644 index 6cb1ecbf4..000000000 --- a/rust/token_transfer_from/dfx.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "canisters": { - "token_transfer_from_backend": { - "candid": "src/token_transfer_from_backend/token_transfer_from_backend.did", - "package": "token_transfer_from_backend", - "type": "rust" - }, - "icrc1_ledger_canister": { - "type": "custom", - "candid": "https://raw.githubusercontent.com/dfinity/ic/d87954601e4b22972899e9957e800406a0a6b929/rs/rosetta-api/icrc1/ledger/ledger.did", - "wasm": "https://download.dfinity.systems/ic/d87954601e4b22972899e9957e800406a0a6b929/canisters/ic-icrc1-ledger.wasm.gz", - "specified_id": "mxzaz-hqaaa-aaaar-qaada-cai" - } - }, - "defaults": { - "build": { - "args": "", - "packtool": "" - } - }, - "output_env_file": ".env", - "version": 1 -} \ No newline at end of file diff --git a/rust/token_transfer_from/src/token_transfer_from_backend/Cargo.toml b/rust/token_transfer_from/src/token_transfer_from_backend/Cargo.toml deleted file mode 100644 index 8dc74ac7e..000000000 --- a/rust/token_transfer_from/src/token_transfer_from_backend/Cargo.toml +++ /dev/null @@ -1,16 +0,0 @@ -[package] -name = "token_transfer_from_backend" -version = "0.1.0" -edition = "2021" - -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html - -[lib] -crate-type = ["cdylib"] - -[dependencies] -candid = "0.10.14" -ic-cdk = "0.18.3" -serde = "1.0.219" -icrc-ledger-types = "0.1.12" -icrc-ledger-client-cdk = "0.1.3" diff --git a/rust/token_transfer_from/src/token_transfer_from_backend/src/lib.rs b/rust/token_transfer_from/src/token_transfer_from_backend/src/lib.rs deleted file mode 100644 index 2d3009808..000000000 --- a/rust/token_transfer_from/src/token_transfer_from_backend/src/lib.rs +++ /dev/null @@ -1,62 +0,0 @@ -use candid::{CandidType, Deserialize, Principal}; -use ic_cdk::api::msg_caller; -use icrc_ledger_client_cdk::{CdkRuntime, ICRC1Client}; -use icrc_ledger_types::icrc1::account::Account; -use icrc_ledger_types::icrc1::transfer::{BlockIndex, NumTokens}; -use icrc_ledger_types::icrc2::transfer_from::TransferFromArgs; -use serde::Serialize; - -#[derive(CandidType, Deserialize, Serialize)] -pub struct TransferArgs { - amount: NumTokens, - to_account: Account, -} - -#[ic_cdk::update] -async fn transfer(args: TransferArgs) -> Result { - ic_cdk::println!( - "Transferring {} tokens to account {}", - &args.amount, - &args.to_account, - ); - - let transfer_from_args = TransferFromArgs { - // the account we want to transfer tokens from (in this case we assume the caller approved the canister to spend funds on their behalf) - from: Account::from(msg_caller()), - // can be used to distinguish between transactions - memo: None, - // the amount we want to transfer - amount: args.amount, - // the subaccount we want to spend the tokens from (in this case we assume the default subaccount has been approved) - spender_subaccount: None, - // if not specified, the default fee for the canister is used - fee: None, - // the account we want to transfer tokens to - to: args.to_account, - // a timestamp indicating when the transaction was created by the caller; if it is not specified by the caller then this is set to the current ICP time - created_at_time: None, - }; - - // Convert a textual representation of a Principal into an actual `Principal` object. The principal is the one we specified in `dfx.json`. - // `expect` will panic if the conversion fails, ensuring the code does not proceed with an invalid principal. - let ledger_canister_id = Principal::from_text("mxzaz-hqaaa-aaaar-qaada-cai") - .expect("Could not decode the principal."); - - let client = ICRC1Client { - runtime: CdkRuntime, - ledger_canister_id, - }; - - client - .transfer_from(transfer_from_args) - .await - // Apply `map_err` to transform any network or system errors encountered during the call into a more readable string format. - // The `?` operator is then used to propagate errors: if the result is an `Err`, it returns from the function with that error, - // otherwise, it unwraps the `Ok` value, allowing the chain to continue. - .map_err(|e| format!("failed to call ledger: {:?}", e))? - // Use `map_err` again to handle any specific ledger transfer errors, converting them into a string format for easier debugging. - .map_err(|e| format!("ledger transfer error {:?}", e)) -} - -// Enable Candid export (see https://internetcomputer.org/docs/current/developer-docs/backend/rust/generating-candid) -ic_cdk::export_candid!(); diff --git a/rust/token_transfer_from/src/token_transfer_from_backend/token_transfer_from_backend.did b/rust/token_transfer_from/src/token_transfer_from_backend/token_transfer_from_backend.did deleted file mode 100644 index c4f9924d2..000000000 --- a/rust/token_transfer_from/src/token_transfer_from_backend/token_transfer_from_backend.did +++ /dev/null @@ -1,4 +0,0 @@ -type Account = record { owner : principal; subaccount : opt blob }; -type Result = variant { Ok : nat; Err : text }; -type TransferArgs = record { to_account : Account; amount : nat }; -service : { transfer : (TransferArgs) -> (Result) } diff --git a/rust/tokenmania/.devcontainer/devcontainer.json b/rust/tokenmania/.devcontainer/devcontainer.json deleted file mode 100644 index ebb0b8bcc..000000000 --- a/rust/tokenmania/.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/tokenmania/BUILD.md b/rust/tokenmania/BUILD.md deleted file mode 100644 index 24cfcb754..000000000 --- a/rust/tokenmania/BUILD.md +++ /dev/null @@ -1,113 +0,0 @@ -# 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. - -To migrate your ICP Ninja project off of the web browser and develop it locally, follow these steps. - -### 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 - -``` - -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). - -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). - -### 6. Deploy to the mainnet. - -Once you have cycles, run the command: - -``` - -dfx deploy --network ic - -``` - -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/tokenmania/Cargo.lock b/rust/tokenmania/Cargo.lock deleted file mode 100644 index b84e142ed..000000000 --- a/rust/tokenmania/Cargo.lock +++ /dev/null @@ -1,902 +0,0 @@ -# This file is automatically @generated by Cargo. -# It is not intended for manual editing. -version = 4 - -[[package]] -name = "anyhow" -version = "1.0.100" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61" - -[[package]] -name = "ar_archive_writer" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0c269894b6fe5e9d7ada0cf69b5bf847ff35bc25fc271f08e1d080fce80339a" -dependencies = [ - "object", -] - -[[package]] -name = "arrayvec" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23b62fc65de8e4e7f52534fb52b0f3ed04746ae267519eef2a83941e8085068b" - -[[package]] -name = "autocfg" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" - -[[package]] -name = "backend" -version = "0.1.0" -dependencies = [ - "candid", - "ic-cdk", - "ic-cdk-timers", - "ic-stable-structures", - "icrc-ledger-types", - "serde", -] - -[[package]] -name = "base32" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23ce669cd6c8588f79e15cf450314f9638f967fc5770ff1c7c1deb0925ea7cfa" - -[[package]] -name = "binread" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "16598dfc8e6578e9b597d9910ba2e73618385dc9f4b1d43dd92c349d6be6418f" -dependencies = [ - "binread_derive", - "lazy_static", - "rustversion", -] - -[[package]] -name = "binread_derive" -version = "2.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d9672209df1714ee804b1f4d4f68c8eb2a90b1f7a07acf472f88ce198ef1fed" -dependencies = [ - "either", - "proc-macro2", - "quote", - "syn 1.0.109", -] - -[[package]] -name = "block-buffer" -version = "0.10.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" -dependencies = [ - "generic-array", -] - -[[package]] -name = "byteorder" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" - -[[package]] -name = "candid" -version = "0.10.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8037a01ec09d6c06883a38bad4f47b8d06158ad360b841e0ae5707c9884dfaf6" -dependencies = [ - "anyhow", - "binread", - "byteorder", - "candid_derive", - "hex", - "ic_principal", - "leb128", - "num-bigint", - "num-traits", - "paste", - "pretty", - "serde", - "serde_bytes", - "stacker", - "thiserror", -] - -[[package]] -name = "candid_derive" -version = "0.10.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fb45f4d5eff3805598ee633dd80f8afb306c023249d34b5b7dfdc2080ea1df2e" -dependencies = [ - "lazy_static", - "proc-macro2", - "quote", - "syn 2.0.111", -] - -[[package]] -name = "cc" -version = "1.2.48" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c481bdbf0ed3b892f6f806287d72acd515b352a4ec27a208489b8c1bc839633a" -dependencies = [ - "find-msvc-tools", - "shlex", -] - -[[package]] -name = "cfg-if" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" - -[[package]] -name = "cpufeatures" -version = "0.2.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" -dependencies = [ - "libc", -] - -[[package]] -name = "crc32fast" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" -dependencies = [ - "cfg-if", -] - -[[package]] -name = "crypto-common" -version = "0.1.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" -dependencies = [ - "generic-array", - "typenum", -] - -[[package]] -name = "data-encoding" -version = "2.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a2330da5de22e8a3cb63252ce2abb30116bf5265e89c0e01bc17015ce30a476" - -[[package]] -name = "deranged" -version = "0.5.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ececcb659e7ba858fb4f10388c250a7252eb0a27373f1a72b8748afdd248e587" -dependencies = [ - "powerfmt", -] - -[[package]] -name = "digest" -version = "0.10.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" -dependencies = [ - "block-buffer", - "crypto-common", -] - -[[package]] -name = "either" -version = "1.15.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" - -[[package]] -name = "find-msvc-tools" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a3076410a55c90011c298b04d0cfa770b00fa04e1e3c97d3f6c9de105a03844" - -[[package]] -name = "futures" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876" -dependencies = [ - "futures-channel", - "futures-core", - "futures-executor", - "futures-io", - "futures-sink", - "futures-task", - "futures-util", -] - -[[package]] -name = "futures-channel" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" -dependencies = [ - "futures-core", - "futures-sink", -] - -[[package]] -name = "futures-core" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" - -[[package]] -name = "futures-executor" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f" -dependencies = [ - "futures-core", - "futures-task", - "futures-util", -] - -[[package]] -name = "futures-io" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" - -[[package]] -name = "futures-macro" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.111", -] - -[[package]] -name = "futures-sink" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" - -[[package]] -name = "futures-task" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" - -[[package]] -name = "futures-util" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" -dependencies = [ - "futures-channel", - "futures-core", - "futures-io", - "futures-macro", - "futures-sink", - "futures-task", - "memchr", - "pin-project-lite", - "pin-utils", - "slab", -] - -[[package]] -name = "generic-array" -version = "0.14.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" -dependencies = [ - "typenum", - "version_check", -] - -[[package]] -name = "heck" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" - -[[package]] -name = "hex" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" -dependencies = [ - "serde", -] - -[[package]] -name = "ic-cdk" -version = "0.16.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a50afd39a4c2022ca6cbdf15dbd653b33eeaae9010748b74006bbd717aa7c51a" -dependencies = [ - "candid", - "ic-cdk-executor", - "ic-cdk-macros", - "ic0", - "serde", - "serde_bytes", -] - -[[package]] -name = "ic-cdk-executor" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "903057edd3d4ff4b3fe44a64eaee1ceb73f579ba29e3ded372b63d291d7c16c2" - -[[package]] -name = "ic-cdk-macros" -version = "0.16.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d4d857135deef20cc7ea8f3869a30cd9cfeb1392b3a81043790b2cd82adc3e0" -dependencies = [ - "candid", - "proc-macro2", - "quote", - "serde", - "serde_tokenstream", - "syn 2.0.111", -] - -[[package]] -name = "ic-cdk-timers" -version = "0.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a298faf67b21a8c4b1dddf60f15ae4c9e981bfcfd61456f7e0a1ae187269738" -dependencies = [ - "futures", - "ic-cdk", - "ic0", - "serde", - "serde_bytes", - "slotmap", -] - -[[package]] -name = "ic-stable-structures" -version = "0.6.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4d30d4cf17aff1024e13133897048bcba580e063c9000571ab766ca37e2996f4" -dependencies = [ - "ic_principal", -] - -[[package]] -name = "ic0" -version = "0.23.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8de254dd67bbd58073e23dc1c8553ba12fa1dc610a19de94ad2bbcd0460c067f" - -[[package]] -name = "ic_principal" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1762deb6f7c8d8c2bdee4b6c5a47b60195b74e9b5280faa5ba29692f8e17429c" -dependencies = [ - "crc32fast", - "data-encoding", - "serde", - "sha2", - "thiserror", -] - -[[package]] -name = "icrc-cbor" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90569d2894d9536c5416943556ac6339df249f06611b3c41029196b39e0dd119" -dependencies = [ - "candid", - "minicbor", - "num-bigint", - "num-traits", -] - -[[package]] -name = "icrc-ledger-types" -version = "0.1.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aafb78e620b2cc2b000cd745c0504dfb23a828acc3dd6f1baef208cd6c471e32" -dependencies = [ - "base32", - "candid", - "crc32fast", - "hex", - "ic-stable-structures", - "icrc-cbor", - "itertools", - "minicbor", - "num-bigint", - "num-traits", - "serde", - "serde_bytes", - "sha2", - "strum", - "strum_macros", - "time", -] - -[[package]] -name = "itertools" -version = "0.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569" -dependencies = [ - "either", -] - -[[package]] -name = "itoa" -version = "1.0.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" - -[[package]] -name = "lazy_static" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" - -[[package]] -name = "leb128" -version = "0.2.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "884e2677b40cc8c339eaefcb701c32ef1fd2493d71118dc0ca4b6a736c93bd67" - -[[package]] -name = "libc" -version = "0.2.178" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37c93d8daa9d8a012fd8ab92f088405fb202ea0b6ab73ee2482ae66af4f42091" - -[[package]] -name = "libm" -version = "0.2.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f9fbbcab51052fe104eb5e5d351cf728d30a5be1fe14d9be8a3b097481fb97de" - -[[package]] -name = "memchr" -version = "2.7.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" - -[[package]] -name = "minicbor" -version = "0.19.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7005aaf257a59ff4de471a9d5538ec868a21586534fff7f85dd97d4043a6139" -dependencies = [ - "minicbor-derive", -] - -[[package]] -name = "minicbor-derive" -version = "0.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1154809406efdb7982841adb6311b3d095b46f78342dd646736122fe6b19e267" -dependencies = [ - "proc-macro2", - "quote", - "syn 1.0.109", -] - -[[package]] -name = "num-bigint" -version = "0.4.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" -dependencies = [ - "num-integer", - "num-traits", - "serde", -] - -[[package]] -name = "num-conv" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" - -[[package]] -name = "num-integer" -version = "0.1.46" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" -dependencies = [ - "num-traits", -] - -[[package]] -name = "num-traits" -version = "0.2.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" -dependencies = [ - "autocfg", - "libm", -] - -[[package]] -name = "object" -version = "0.32.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a6a622008b6e321afc04970976f62ee297fdbaa6f95318ca343e3eebb9648441" -dependencies = [ - "memchr", -] - -[[package]] -name = "paste" -version = "1.0.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" - -[[package]] -name = "pin-project-lite" -version = "0.2.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" - -[[package]] -name = "pin-utils" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" - -[[package]] -name = "powerfmt" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" - -[[package]] -name = "pretty" -version = "0.12.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d22152487193190344590e4f30e219cf3fe140d9e7a3fdb683d82aa2c5f4156" -dependencies = [ - "arrayvec", - "typed-arena", - "unicode-width", -] - -[[package]] -name = "proc-macro2" -version = "1.0.103" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ee95bc4ef87b8d5ba32e8b7714ccc834865276eab0aed5c9958d00ec45f49e8" -dependencies = [ - "unicode-ident", -] - -[[package]] -name = "psm" -version = "0.1.28" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d11f2fedc3b7dafdc2851bc52f277377c5473d378859be234bc7ebb593144d01" -dependencies = [ - "ar_archive_writer", - "cc", -] - -[[package]] -name = "quote" -version = "1.0.42" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a338cc41d27e6cc6dce6cefc13a0729dfbb81c262b1f519331575dd80ef3067f" -dependencies = [ - "proc-macro2", -] - -[[package]] -name = "rustversion" -version = "1.0.22" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" - -[[package]] -name = "serde" -version = "1.0.228" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" -dependencies = [ - "serde_core", - "serde_derive", -] - -[[package]] -name = "serde_bytes" -version = "0.11.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a5d440709e79d88e51ac01c4b72fc6cb7314017bb7da9eeff678aa94c10e3ea8" -dependencies = [ - "serde", - "serde_core", -] - -[[package]] -name = "serde_core" -version = "1.0.228" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" -dependencies = [ - "serde_derive", -] - -[[package]] -name = "serde_derive" -version = "1.0.228" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.111", -] - -[[package]] -name = "serde_tokenstream" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "64060d864397305347a78851c51588fd283767e7e7589829e8121d65512340f1" -dependencies = [ - "proc-macro2", - "quote", - "serde", - "syn 2.0.111", -] - -[[package]] -name = "sha2" -version = "0.10.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" -dependencies = [ - "cfg-if", - "cpufeatures", - "digest", -] - -[[package]] -name = "shlex" -version = "1.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" - -[[package]] -name = "slab" -version = "0.4.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a2ae44ef20feb57a68b23d846850f861394c2e02dc425a50098ae8c90267589" - -[[package]] -name = "slotmap" -version = "1.0.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dbff4acf519f630b3a3ddcfaea6c06b42174d9a44bc70c620e9ed1649d58b82a" -dependencies = [ - "version_check", -] - -[[package]] -name = "stacker" -version = "0.1.22" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e1f8b29fb42aafcea4edeeb6b2f2d7ecd0d969c48b4cf0d2e64aafc471dd6e59" -dependencies = [ - "cc", - "cfg-if", - "libc", - "psm", - "windows-sys", -] - -[[package]] -name = "strum" -version = "0.26.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fec0f0aef304996cf250b31b5a10dee7980c85da9d759361292b8bca5a18f06" -dependencies = [ - "strum_macros", -] - -[[package]] -name = "strum_macros" -version = "0.26.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c6bee85a5a24955dc440386795aa378cd9cf82acd5f764469152d2270e581be" -dependencies = [ - "heck", - "proc-macro2", - "quote", - "rustversion", - "syn 2.0.111", -] - -[[package]] -name = "syn" -version = "1.0.109" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" -dependencies = [ - "proc-macro2", - "quote", - "unicode-ident", -] - -[[package]] -name = "syn" -version = "2.0.111" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "390cc9a294ab71bdb1aa2e99d13be9c753cd2d7bd6560c77118597410c4d2e87" -dependencies = [ - "proc-macro2", - "quote", - "unicode-ident", -] - -[[package]] -name = "thiserror" -version = "1.0.69" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" -dependencies = [ - "thiserror-impl", -] - -[[package]] -name = "thiserror-impl" -version = "1.0.69" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.111", -] - -[[package]] -name = "time" -version = "0.3.44" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91e7d9e3bb61134e77bde20dd4825b97c010155709965fedf0f49bb138e52a9d" -dependencies = [ - "deranged", - "itoa", - "num-conv", - "powerfmt", - "serde", - "time-core", - "time-macros", -] - -[[package]] -name = "time-core" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "40868e7c1d2f0b8d73e4a8c7f0ff63af4f6d19be117e90bd73eb1d62cf831c6b" - -[[package]] -name = "time-macros" -version = "0.2.24" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30cfb0125f12d9c277f35663a0a33f8c30190f4e4574868a330595412d34ebf3" -dependencies = [ - "num-conv", - "time-core", -] - -[[package]] -name = "typed-arena" -version = "2.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6af6ae20167a9ece4bcb41af5b80f8a1f1df981f6391189ce00fd257af04126a" - -[[package]] -name = "typenum" -version = "1.19.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" - -[[package]] -name = "unicode-ident" -version = "1.0.22" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" - -[[package]] -name = "unicode-width" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254" - -[[package]] -name = "version_check" -version = "0.9.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" - -[[package]] -name = "windows-sys" -version = "0.59.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" -dependencies = [ - "windows-targets", -] - -[[package]] -name = "windows-targets" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" -dependencies = [ - "windows_aarch64_gnullvm", - "windows_aarch64_msvc", - "windows_i686_gnu", - "windows_i686_gnullvm", - "windows_i686_msvc", - "windows_x86_64_gnu", - "windows_x86_64_gnullvm", - "windows_x86_64_msvc", -] - -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" - -[[package]] -name = "windows_aarch64_msvc" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" - -[[package]] -name = "windows_i686_gnu" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" - -[[package]] -name = "windows_i686_gnullvm" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" - -[[package]] -name = "windows_i686_msvc" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" - -[[package]] -name = "windows_x86_64_gnu" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" - -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" - -[[package]] -name = "windows_x86_64_msvc" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" diff --git a/rust/tokenmania/Cargo.toml b/rust/tokenmania/Cargo.toml deleted file mode 100644 index d1e49e317..000000000 --- a/rust/tokenmania/Cargo.toml +++ /dev/null @@ -1,3 +0,0 @@ -[workspace] -members = ["backend"] -resolver = "2" diff --git a/rust/tokenmania/README.md b/rust/tokenmania/README.md deleted file mode 100644 index b9a009d14..000000000 --- a/rust/tokenmania/README.md +++ /dev/null @@ -1,31 +0,0 @@ -# Tokenmania! - -Tokenmania is a simplified token minting application. When the application is ran, it will automatically mint tokens based on the backend smart contract's hardcoded configuration values for things such as token name, token symbol, and total supply. - -> [!CAUTION] -> This sample application is not production-ready code. Actual tokens deployed on ICP will require a ledger and an index smart contract. For this example's demonstration, this functionality has been simplified and the ledger functionality has been included in the backend. Tokens deployed using this example are only available for 20 minutes and will be deleted afterwards. They should be treated as "testnet" assets and should not be given real value. -> For more information on creating tokens using a recommended production workflow, view the [create a token documentation](https://internetcomputer.org/docs/current/developer-docs/defi/tokens/create). - -## 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: - -[![](https://icp.ninja/assets/open.svg)](https://icp.ninja/i?g=https://github.com/dfinity/examples/rust/tokenmania) - -## Build and deploy from the command-line - -### 1. [Download and install the IC SDK.](https://internetcomputer.org/docs/building-apps/getting-started/install) - -### 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/) - -### 3. Navigate into the project's directory. - -### 4. Deploy the project to your local environment: - -``` -dfx start --background --clean && dfx deploy -``` - -## 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://internetcomputer.org/docs/building-apps/security/overview) for developing on ICP. This example may not implement all the best practices. diff --git a/rust/tokenmania/backend/Cargo.toml b/rust/tokenmania/backend/Cargo.toml deleted file mode 100644 index 75c280962..000000000 --- a/rust/tokenmania/backend/Cargo.toml +++ /dev/null @@ -1,18 +0,0 @@ -[package] -name = "backend" -version = "0.1.0" -edition = "2021" - -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html - -[lib] -crate-type = ["cdylib"] -path = "lib.rs" - -[dependencies] -candid = "0.10.10" -icrc-ledger-types = "0.1.12" -ic-stable-structures = "0.6.5" -ic-cdk = "0.16.0" -ic-cdk-timers = "0.10.0" -serde = "1.0.210" diff --git a/rust/tokenmania/backend/backend.did b/rust/tokenmania/backend/backend.did deleted file mode 100644 index 6456244ce..000000000 --- a/rust/tokenmania/backend/backend.did +++ /dev/null @@ -1,92 +0,0 @@ -type Account = record { owner : principal; subaccount : opt blob }; -type Allowance = record { allowance : nat; expires_at : opt nat64 }; -type AllowanceArgs = record { account : Account; spender : Account }; -type ApproveArgs = record { - fee : opt nat; - memo : opt blob; - from_subaccount : opt blob; - created_at_time : opt nat64; - amount : nat; - expected_allowance : opt nat; - expires_at : opt nat64; - spender : Account; -}; -type ApproveError = variant { - GenericError : record { message : text; error_code : nat }; - TemporarilyUnavailable; - Duplicate : record { duplicate_of : nat }; - BadFee : record { expected_fee : nat }; - AllowanceChanged : record { current_allowance : nat }; - CreatedInFuture : record { ledger_time : nat64 }; - TooOld; - Expired : record { ledger_time : nat64 }; - InsufficientFunds : record { balance : nat }; -}; -type CreateTokenArgs = record { - initial_supply : nat; - token_symbol : text; - token_logo : text; - token_name : text; -}; -type MetadataValue = variant { Int : int; Nat : nat; Blob : blob; Text : text }; -type Result = variant { Ok : text; Err : text }; -type Result_1 = variant { Ok : nat; Err : TransferError }; -type Result_2 = variant { Ok : nat; Err : ApproveError }; -type Result_3 = variant { Ok : nat; Err : TransferFromError }; -type SupportedStandard = record { url : text; name : text }; -type TransferArg = record { - to : Account; - fee : opt nat; - memo : opt blob; - from_subaccount : opt blob; - created_at_time : opt nat64; - amount : nat; -}; -type TransferError = variant { - GenericError : record { message : text; error_code : nat }; - TemporarilyUnavailable; - BadBurn : record { min_burn_amount : nat }; - Duplicate : record { duplicate_of : nat }; - BadFee : record { expected_fee : nat }; - CreatedInFuture : record { ledger_time : nat64 }; - TooOld; - InsufficientFunds : record { balance : nat }; -}; -type TransferFromArgs = record { - to : Account; - fee : opt nat; - spender_subaccount : opt blob; - from : Account; - memo : opt blob; - created_at_time : opt nat64; - amount : nat; -}; -type TransferFromError = variant { - GenericError : record { message : text; error_code : nat }; - TemporarilyUnavailable; - InsufficientAllowance : record { allowance : nat }; - BadBurn : record { min_burn_amount : nat }; - Duplicate : record { duplicate_of : nat }; - BadFee : record { expected_fee : nat }; - CreatedInFuture : record { ledger_time : nat64 }; - TooOld; - InsufficientFunds : record { balance : nat }; -}; -service : { - create_token : (CreateTokenArgs) -> (Result); - delete_token : () -> (Result); - icrc1_balance_of : (Account) -> (nat) query; - icrc1_decimals : () -> (nat8) query; - icrc1_fee : () -> (nat) query; - icrc1_metadata : () -> (vec record { text; MetadataValue }) query; - icrc1_minting_account : () -> (opt Account) query; - icrc1_name : () -> (text) query; - icrc1_supported_standards : () -> (vec SupportedStandard) query; - icrc1_token_symbol : () -> (text) query; - icrc1_total_supply : () -> (nat) query; - icrc1_transfer : (TransferArg) -> (Result_1); - icrc2_allowance : (AllowanceArgs) -> (Allowance) query; - icrc2_approve : (ApproveArgs) -> (Result_2); - icrc2_transfer_from : (TransferFromArgs) -> (Result_3); - token_created : () -> (bool) query; -} diff --git a/rust/tokenmania/backend/lib.rs b/rust/tokenmania/backend/lib.rs deleted file mode 100644 index 95e3640f2..000000000 --- a/rust/tokenmania/backend/lib.rs +++ /dev/null @@ -1,610 +0,0 @@ -use ic_cdk::{query, update}; -use ic_stable_structures::memory_manager::{MemoryId, MemoryManager}; -use ic_stable_structures::DefaultMemoryImpl; -use icrc_ledger_types::icrc::generic_metadata_value::MetadataValue; -use icrc_ledger_types::icrc1::account::Account; -use icrc_ledger_types::icrc1::transfer::{BlockIndex, Memo, TransferArg, TransferError}; -use icrc_ledger_types::icrc2::allowance::{Allowance, AllowanceArgs}; -use icrc_ledger_types::icrc2::approve::{ApproveArgs, ApproveError}; -use icrc_ledger_types::icrc2::transfer_from::{TransferFromArgs, TransferFromError}; -use icrc_ledger_types::icrc3::transactions::{Approve, Burn, Mint, Transaction, Transfer}; -use std::cell::RefCell; -use types::*; - -mod types; - -const MAX_MEMO_SIZE: usize = 32; -const PERMITTED_DRIFT_NANOS: u64 = 60_000_000_000; -const TRANSACTION_WINDOW_NANOS: u64 = 24 * 60 * 60 * 1_000_000_000; - -// Error codes -const MEMO_TOO_LONG_ERROR_CODE: usize = 0; - -// Create data structures in stable memory so that the data persist across canister upgrades. -const CONFIGURATION_MEMORY_ID: MemoryId = MemoryId::new(1); -const TRANSACTION_LOG_MEMORY_ID: MemoryId = MemoryId::new(2); -thread_local! { - /// Static memory manager to manage the memory available for stable structures. - static MEMORY_MANAGER: RefCell> = - RefCell::new(MemoryManager::init(DefaultMemoryImpl::default())); - - // Initialize canister state. - static STATE: RefCell = MEMORY_MANAGER.with(|cell| { - let mm = cell.borrow(); - let configuration = ConfigCell::init(mm.get(CONFIGURATION_MEMORY_ID), Configuration::default()) - .expect("failed to initialize the config cell"); - let transaction_log = TransactionLog::init(mm.get(TRANSACTION_LOG_MEMORY_ID)) - .expect("failed to initialize the transaction log"); - RefCell::new(State { - configuration, - transaction_log, - }) - }); -} - -// Convenience method to make read operations simpler -pub fn read_state(f: impl FnOnce(&State) -> R) -> R { - STATE.with(|cell| f(&cell.borrow())) -} - -// Convenience method to make state changes simpler -pub fn mutate_state(f: impl FnOnce(&mut State) -> R) -> R { - STATE.with(|cell| f(&mut cell.borrow_mut())) -} - -// Calculates the current balance of an account -fn balance(account: Account) -> Tokens { - read_state(|state| { - let mut balance = 0_usize.into(); - for tx in state.transaction_log.iter() { - if let Some(mint) = tx.0.mint { - if mint.to == account { - balance += mint.amount; - } - } - if let Some(burn) = tx.0.burn { - if burn.from == account { - balance -= burn.amount; - } - } - if let Some(transfer) = tx.0.transfer { - if transfer.to == account { - balance += transfer.amount.clone(); - } - if transfer.from == account { - balance -= transfer.amount; - if let Some(fee) = transfer.fee { - balance -= fee; - } - } - } - if let Some(approve) = tx.0.approve { - if let Some(fee) = approve.fee { - balance -= fee; - } - } - } - balance - }) -} - -// Calculates the current total supply of tokens -fn total_supply() -> Tokens { - read_state(|state| { - let mut supply = 0_usize.into(); - for tx in state.transaction_log.iter() { - if let Some(mint) = tx.0.mint { - supply += mint.amount; - } - if let Some(burn) = tx.0.burn { - supply -= burn.amount; - } - if let Some(transfer) = tx.0.transfer { - if let Some(fee) = transfer.fee { - supply -= fee; - } - } - if let Some(approve) = tx.0.approve { - if let Some(fee) = approve.fee { - supply -= fee; - } - } - } - supply - }) -} - -// Calculates how much `spender` is allowed to spend from `account` at the moment -fn allowance(account: Account, spender: Account, now: u64) -> Allowance { - read_state(|state| { - let mut allowance = 0_usize.into(); - let mut last_approval_expiry = None; - for tx in state.transaction_log.iter() { - // Reset expired approval - if let Some(expires_at) = last_approval_expiry { - if expires_at < tx.0.timestamp { - allowance = 0_usize.into(); - last_approval_expiry = None; - } - } - // Add pending approval - if let Some(approve) = tx.0.approve { - if approve.from == account && approve.spender == spender { - allowance = approve.amount; - last_approval_expiry = approve.expires_at; - } - } - if let Some(transfer) = tx.0.transfer { - if transfer.from == account && transfer.spender == Some(spender) { - allowance -= transfer.amount; - if let Some(fee) = transfer.fee { - allowance -= fee; - } - } - } - } - if let Some(expires_at) = last_approval_expiry { - if expires_at < now { - allowance = 0_usize.into(); - last_approval_expiry = None; - } - } - Allowance { - allowance, - expires_at: last_approval_expiry, - } - }) -} - -// Makes sure `created_at_time` is in the span of allowed timestamps -fn validate_created_at_time(created_at_time: Option, now: u64) -> Result<(), TransferError> { - if let Some(tx_time) = created_at_time { - if tx_time > now && now - tx_time > TRANSACTION_WINDOW_NANOS + PERMITTED_DRIFT_NANOS { - return Err(TransferError::CreatedInFuture { ledger_time: now }); - } - if tx_time < now && now - tx_time > TRANSACTION_WINDOW_NANOS + PERMITTED_DRIFT_NANOS { - return Err(TransferError::TooOld); - } - } - Ok(()) -} - -// Makes sure the memo fits the requirements -fn validate_memo(memo: Option<&Memo>) -> Result<(), TransferError> { - if let Some(memo) = memo { - if memo.0.len() > MAX_MEMO_SIZE { - return Err(TransferError::GenericError { - error_code: MEMO_TOO_LONG_ERROR_CODE.into(), - message: "Memo too long".into(), - }); - } - } - Ok(()) -} - -// Appends a validated transaction to the transaction log -fn record_tx(tx: &StorableTransaction) -> BlockIndex { - mutate_state(|state| { - let idx = state.transaction_log.len(); - state - .transaction_log - .push(tx) - .expect("Failed to grow transaction log."); - idx.into() - }) -} - -// Tries to find a transaction in the transaction log -fn find_tx(tx: &TxInfo) -> Option { - read_state(|state| { - for (i, candidate_tx) in state.transaction_log.iter().enumerate() { - if tx.is_approval { - if let Some(approve) = candidate_tx.0.approve { - if tx.from == approve.from - && tx.spender == Some(approve.spender) - && tx.amount == approve.amount - && tx.expected_allowance == approve.expected_allowance - && tx.expires_at == approve.expires_at - && tx.memo == approve.memo - && tx.created_at_time == approve.created_at_time - { - return Some(i.into()); - } - } - } else { - if let Some(burn) = candidate_tx.0.burn { - if tx.to == state.configuration.get().minting_account - && tx.from == burn.from - && tx.amount == burn.amount - && tx.spender == burn.spender - && tx.memo == burn.memo - && tx.created_at_time == burn.created_at_time - { - return Some(i.into()); - } - } - if let Some(mint) = candidate_tx.0.mint { - if Some(tx.from) == state.configuration.get().minting_account - && tx.to == Some(mint.to) - && tx.amount == mint.amount - && tx.memo == mint.memo - && tx.created_at_time == mint.created_at_time - { - return Some(i.into()); - } - } - if let Some(transfer) = candidate_tx.0.transfer { - if tx.from == transfer.from - && tx.to == Some(transfer.to) - && tx.amount == transfer.amount - && tx.spender == transfer.spender - && tx.memo == transfer.memo - && tx.created_at_time == transfer.created_at_time - { - return Some(i.into()); - } - } - } - } - None - }) -} - -// Turns TxInfo into a validated transaction -fn classify_tx(tx: TxInfo, now: u64) -> Result { - // Deduplication only happens if `created_at_time` is set - if tx.created_at_time.is_some() { - if let Some(duplicate_of) = find_tx(&tx) { - return Err(TransferError::Duplicate { duplicate_of }); - } - } - if let Some(specified_fee) = tx.fee { - let expected_fee = read_state(|state| state.configuration.get().transfer_fee.clone()); - if specified_fee != expected_fee { - return Err(TransferError::BadFee { expected_fee }); - } - } - if tx.is_approval { - return Ok(StorableTransaction(Transaction { - kind: "approve".to_string(), - mint: None, - burn: None, - transfer: None, - approve: Some(Approve { - from: tx.from, - spender: tx.spender.expect("Bug: failed to forward spender"), - amount: tx.amount, - expected_allowance: tx.expected_allowance, - expires_at: tx.expires_at, - memo: tx.memo, - fee: Some(read_state(|state| { - state.configuration.get().transfer_fee.clone() - })), - created_at_time: tx.created_at_time, - }), - timestamp: now, - })); - } else if let Some(minter) = - read_state(|state| state.configuration.get().minting_account.clone()) - { - if Some(tx.from) == Some(minter) { - return Ok(StorableTransaction(Transaction { - kind: "mint".to_string(), - mint: Some(Mint { - amount: tx.amount, - to: tx.to.expect("Bug: failed to forward mint receiver"), - memo: tx.memo, - created_at_time: tx.created_at_time, - fee: None, - }), - burn: None, - transfer: None, - approve: None, - timestamp: now, - })); - } else if tx.to == Some(minter) { - let transfer_fee = read_state(|state| state.configuration.get().transfer_fee.clone()); - if tx.amount < transfer_fee { - return Err(TransferError::BadBurn { - min_burn_amount: transfer_fee, - }); - } - let balance = balance(tx.from); - if balance < tx.amount.clone() + transfer_fee { - return Err(TransferError::InsufficientFunds { balance }); - } - return Ok(StorableTransaction(Transaction { - kind: "burn".to_string(), - mint: None, - burn: Some(Burn { - amount: tx.amount, - from: tx.from, - spender: tx.spender, - memo: tx.memo, - created_at_time: tx.created_at_time, - fee: None, - }), - transfer: None, - approve: None, - timestamp: now, - })); - } - } - let balance = balance(tx.from); - if balance - < tx.amount.clone() + read_state(|state| state.configuration.get().transfer_fee.clone()) - { - return Err(TransferError::InsufficientFunds { balance }); - } - Ok(StorableTransaction(Transaction { - kind: "transfer".to_string(), - mint: None, - burn: None, - transfer: Some(Transfer { - amount: tx.amount, - from: tx.from, - to: tx.to.expect("Bug: failed to forward transfer receiver"), - spender: tx.spender, - memo: tx.memo, - fee: Some(read_state(|state| { - state.configuration.get().transfer_fee.clone() - })), - created_at_time: tx.created_at_time, - }), - approve: None, - timestamp: now, - })) -} - -// Runs validity checks and records the transaction if it is valid -fn apply_tx(tx: TxInfo) -> Result { - validate_memo(tx.memo.as_ref())?; - let now = ic_cdk::api::time(); - validate_created_at_time(tx.created_at_time, now)?; - let transaction = classify_tx(tx, now)?; - Ok(record_tx(&transaction)) -} - -#[update] -fn icrc1_transfer(arg: TransferArg) -> Result { - let from = Account { - owner: ic_cdk::api::caller(), - subaccount: arg.from_subaccount, - }; - let tx = TxInfo { - from, - to: Some(arg.to), - amount: arg.amount, - spender: None, - memo: arg.memo, - fee: arg.fee, - created_at_time: arg.created_at_time, - expected_allowance: None, - expires_at: None, - is_approval: false, - }; - apply_tx(tx) -} - -#[query] -fn icrc1_balance_of(account: Account) -> Tokens { - balance(account) -} - -#[query] -fn icrc1_total_supply() -> Tokens { - total_supply() -} - -#[query] -fn icrc1_minting_account() -> Option { - read_state(|state| state.configuration.get().minting_account.clone()) -} - -#[query] -fn icrc1_name() -> String { - read_state(|state| state.configuration.get().token_name.clone()) -} - -#[query] -fn icrc1_token_symbol() -> String { - read_state(|state| state.configuration.get().token_symbol.clone()) -} - -#[query] -fn icrc1_decimals() -> u8 { - read_state(|state| state.configuration.get().decimals) -} - -#[query] -fn icrc1_fee() -> Tokens { - read_state(|state| state.configuration.get().transfer_fee.clone()) -} - -#[query] -fn icrc1_metadata() -> Vec<(String, MetadataValue)> { - vec![ - ("icrc1:name".to_string(), MetadataValue::Text(icrc1_name())), - ( - "icrc1:symbol".to_string(), - MetadataValue::Text(icrc1_token_symbol()), - ), - ( - "icrc1:decimals".to_string(), - MetadataValue::Nat(icrc1_decimals().into()), - ), - ("icrc1:fee".to_string(), MetadataValue::Nat(icrc1_fee())), - ( - "icrc1:logo".to_string(), - MetadataValue::Text(read_state(|state| { - state.configuration.get().token_logo.clone() - })), - ), - ] -} - -#[query] -fn icrc1_supported_standards() -> Vec { - vec![ - SupportedStandard { - name: "ICRC-1".to_string(), - url: "https://github.com/dfinity/ICRC-1/tree/main/standards/ICRC-1".to_string(), - }, - SupportedStandard { - name: "ICRC-2".to_string(), - url: "https://github.com/dfinity/ICRC-1/tree/main/standards/ICRC-2".to_string(), - }, - ] -} - -#[update] -fn icrc2_approve(arg: ApproveArgs) -> Result { - validate_memo(arg.memo.as_ref()).map_err(to_approve_error)?; - let approver_account = Account { - owner: ic_cdk::api::caller(), - subaccount: arg.from_subaccount, - }; - let now = ic_cdk::api::time(); - if let Some(expected_allowance) = arg.expected_allowance.as_ref() { - let current_allowance = allowance(approver_account, arg.spender, now).allowance; - if current_allowance != *expected_allowance { - return Err(ApproveError::AllowanceChanged { current_allowance }); - } - } - let tx = TxInfo { - from: approver_account, - to: None, - amount: arg.amount, - spender: Some(arg.spender), - memo: arg.memo, - fee: arg.fee, - created_at_time: arg.created_at_time, - expected_allowance: arg.expected_allowance, - expires_at: arg.expires_at, - is_approval: true, - }; - apply_tx(tx).map_err(to_approve_error) -} - -#[update] -fn icrc2_transfer_from(arg: TransferFromArgs) -> Result { - if ic_cdk::api::caller() == arg.from.owner { - return icrc1_transfer(TransferArg { - to: arg.to, - from_subaccount: arg.from.subaccount, - amount: arg.amount, - fee: arg.fee, - memo: arg.memo, - created_at_time: arg.created_at_time, - }) - .map_err(to_transfer_from_error); - } - validate_memo(arg.memo.as_ref()).map_err(to_transfer_from_error)?; - let spender = Account { - owner: ic_cdk::api::caller(), - subaccount: arg.spender_subaccount, - }; - let now = ic_cdk::api::time(); - let allowance = allowance(arg.from, spender, now); - let transfer_fee = read_state(|state| state.configuration.get().transfer_fee.clone()); - if allowance.allowance < arg.amount.clone() + transfer_fee { - return Err(TransferFromError::InsufficientAllowance { - allowance: allowance.allowance, - }); - } - let tx = TxInfo { - from: arg.from, - to: Some(arg.to), - amount: arg.amount, - spender: Some(spender), - memo: arg.memo, - fee: arg.fee, - created_at_time: arg.created_at_time, - expected_allowance: None, - expires_at: None, - is_approval: false, - }; - apply_tx(tx).map_err(to_transfer_from_error) -} - -#[query] -fn icrc2_allowance(arg: AllowanceArgs) -> Allowance { - let now = ic_cdk::api::time(); - allowance(arg.account, arg.spender, now) -} - -#[update] -fn create_token(args: CreateTokenArgs) -> Result { - let minting_account = Account { - owner: ic_cdk::api::caller(), - subaccount: None, - }; - let init_tx = StorableTransaction(Transaction { - kind: "initial mint".to_string(), - mint: Some(Mint { - amount: args.initial_supply, - to: minting_account, - memo: None, - created_at_time: None, - fee: None, - }), - burn: None, - transfer: None, - approve: None, - timestamp: ic_cdk::api::time(), - }); - record_tx(&init_tx); - mutate_state(|state| { - state - .configuration - .set(Configuration { - token_name: args.token_name, - token_symbol: args.token_symbol, - token_logo: args.token_logo, - transfer_fee: 10_000_usize.into(), - decimals: 8, - minting_account: Some(minting_account), - token_created: true, - }) - .map_err(|_| "Failed to set initial config".to_string())?; - Ok("Token created".to_string()) - }) -} - -#[query] -fn token_created() -> bool { - read_state(|state| state.configuration.get().token_created) -} - -#[update] -fn delete_token() -> Result { - if !token_created() { - return Err("Token not created".to_string()); - }; - - if ic_cdk::api::caller() - != read_state(|state| { - state - .configuration - .get() - .minting_account - .clone() - .unwrap() - .owner - }) - { - return Err("Caller is not the token creator".to_string()); - }; - - // Reset STATE - STATE.with_borrow_mut(|state| { - state.configuration.set(Configuration::default()).unwrap(); - - let mem = MEMORY_MANAGER.with(|cell| cell.borrow_mut().get(TRANSACTION_LOG_MEMORY_ID)); - state.transaction_log = TransactionLog::new(mem).unwrap(); - }); - Ok("Token deleted".to_string()) -} - -// Export the interface for the smart contract. -ic_cdk::export_candid!(); diff --git a/rust/tokenmania/backend/types.rs b/rust/tokenmania/backend/types.rs deleted file mode 100644 index 5bdadc4bc..000000000 --- a/rust/tokenmania/backend/types.rs +++ /dev/null @@ -1,147 +0,0 @@ -use candid::{CandidType, Decode, Deserialize, Encode, Nat}; -use ic_stable_structures::memory_manager::VirtualMemory; -use ic_stable_structures::storable::Bound; -use ic_stable_structures::DefaultMemoryImpl; -use ic_stable_structures::{StableCell, StableVec, Storable}; -use icrc_ledger_types::icrc1::account::Account; -use icrc_ledger_types::icrc1::transfer::Memo; -use icrc_ledger_types::icrc1::transfer::TransferError; -use icrc_ledger_types::icrc2::approve::ApproveError; -use icrc_ledger_types::icrc2::transfer_from::TransferFromError; -use icrc_ledger_types::icrc3::transactions::Transaction; -use std::borrow::Cow; - -type VMem = VirtualMemory; -pub type Tokens = Nat; - -pub struct State { - pub configuration: ConfigCell, - pub transaction_log: TransactionLog, -} - -// The struct that holds the token ledger's settings -pub type ConfigCell = StableCell; -#[derive(Debug, Default, CandidType, Deserialize)] -pub struct Configuration { - pub token_name: String, - pub token_symbol: String, - pub token_logo: String, - pub transfer_fee: Tokens, - pub decimals: u8, - pub minting_account: Option, - pub token_created: bool, -} - -// To persist the configuration we need to store it in stable storage. -// Storable describes how the configuration is serialized while stored in stable storage. -// Here, we use Candid en/de-coding. It is not too space efficient, but it is simple to do. -impl Storable for Configuration { - fn to_bytes(&self) -> Cow<'_, [u8]> { - Cow::Owned( - Encode!(&self) - .expect("failed to serialize Configuration") - .into(), - ) - } - fn from_bytes(bytes: Cow<'_, [u8]>) -> Self { - Decode!(&bytes, Configuration).expect("failed to deserialize Configuration") - } - const BOUND: Bound = Bound::Unbounded; -} - -// To persist the transactions we need to store them in stable storage. -// Storable describes how the transactions are serialized while stored in stable storage. -// Here, we use Candid en/de-coding. It is not too space efficient, but it is simple to do. -pub type TransactionLog = StableVec; -pub struct StorableTransaction(pub Transaction); -impl Storable for StorableTransaction { - fn to_bytes(&self) -> Cow<'_, [u8]> { - Cow::Owned( - Encode!(&self.0) - .expect("failed to serialize Transaction") - .into(), - ) - } - fn from_bytes(bytes: Cow<'_, [u8]>) -> Self { - Self(Decode!(&bytes, Transaction).expect("failed to deserialize Transaction")) - } - const BOUND: Bound = Bound::Bounded { - max_size: 1000, - is_fixed_size: false, - }; -} - -#[derive(Debug)] -pub struct TxInfo { - pub from: Account, - pub to: Option, - pub amount: Tokens, - pub spender: Option, - pub memo: Option, - pub fee: Option, - pub created_at_time: Option, - pub expected_allowance: Option, - pub expires_at: Option, - pub is_approval: bool, -} - -#[derive(CandidType, Deserialize, Clone, Debug, PartialEq, Eq)] -pub struct SupportedStandard { - pub name: String, - pub url: String, -} - -pub fn to_approve_error(err: TransferError) -> ApproveError { - match err { - TransferError::BadFee { expected_fee } => ApproveError::BadFee { expected_fee }, - TransferError::TooOld => ApproveError::TooOld, - TransferError::CreatedInFuture { ledger_time } => { - ApproveError::CreatedInFuture { ledger_time } - } - TransferError::TemporarilyUnavailable => ApproveError::TemporarilyUnavailable, - TransferError::Duplicate { duplicate_of } => ApproveError::Duplicate { duplicate_of }, - TransferError::GenericError { - error_code, - message, - } => ApproveError::GenericError { - error_code, - message, - }, - TransferError::BadBurn { .. } | TransferError::InsufficientFunds { .. } => { - ic_cdk::trap("Bug: cannot transform TransferError into ApproveError") - } - } -} - -pub fn to_transfer_from_error(err: TransferError) -> TransferFromError { - match err { - TransferError::BadFee { expected_fee } => TransferFromError::BadFee { expected_fee }, - TransferError::TooOld => TransferFromError::TooOld, - TransferError::CreatedInFuture { ledger_time } => { - TransferFromError::CreatedInFuture { ledger_time } - } - TransferError::TemporarilyUnavailable => TransferFromError::TemporarilyUnavailable, - TransferError::Duplicate { duplicate_of } => TransferFromError::Duplicate { duplicate_of }, - TransferError::GenericError { - error_code, - message, - } => TransferFromError::GenericError { - error_code, - message, - }, - TransferError::InsufficientFunds { balance } => { - TransferFromError::InsufficientFunds { balance } - } - TransferError::BadBurn { min_burn_amount } => { - TransferFromError::BadBurn { min_burn_amount } - } - } -} - -#[derive(Debug, CandidType, Deserialize)] -pub struct CreateTokenArgs { - pub token_name: String, - pub token_symbol: String, - pub initial_supply: Nat, - pub token_logo: String, -} diff --git a/rust/tokenmania/dfx.json b/rust/tokenmania/dfx.json deleted file mode 100644 index 34b93cbed..000000000 --- a/rust/tokenmania/dfx.json +++ /dev/null @@ -1,52 +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" - } - ] - }, - "internet_identity": { - "type": "custom", - "candid": "https://github.com/dfinity/internet-identity/releases/latest/download/internet_identity.did", - "wasm": "https://github.com/dfinity/internet-identity/releases/latest/download/internet_identity_production.wasm.gz", - "specified_id": "rdmx6-jaaaa-aaaaa-aaadq-cai", - "remote": { - "id": { - "ic": "rdmx6-jaaaa-aaaaa-aaadq-cai" - } - } - }, - "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 } } }})" - }, - "frontend": { - "dependencies": ["backend"], - "frontend": { - "entrypoint": "frontend/index.html" - }, - "source": ["frontend/dist"], - "type": "assets" - } - }, - "output_env_file": ".env" -} diff --git a/rust/tokenmania/frontend/index.css b/rust/tokenmania/frontend/index.css deleted file mode 100644 index b5c61c956..000000000 --- a/rust/tokenmania/frontend/index.css +++ /dev/null @@ -1,3 +0,0 @@ -@tailwind base; -@tailwind components; -@tailwind utilities; diff --git a/rust/tokenmania/frontend/index.html b/rust/tokenmania/frontend/index.html deleted file mode 100644 index 2fb96d095..000000000 --- a/rust/tokenmania/frontend/index.html +++ /dev/null @@ -1,14 +0,0 @@ - - - - - - Tokenmania - - - - -
- - - diff --git a/rust/tokenmania/frontend/package.json b/rust/tokenmania/frontend/package.json deleted file mode 100644 index d0365f7ec..000000000 --- a/rust/tokenmania/frontend/package.json +++ /dev/null @@ -1,26 +0,0 @@ -{ - "name": "frontend", - "private": true, - "type": "module", - "scripts": { - "prebuild": "npm i --include=dev && dfx generate backend", - "build": "vite build", - "dev": "vite" - }, - "dependencies": { - "@icp-sdk/auth": "~5.0.0", - "@icp-sdk/core": "~5.2.0", - "react": "18.3.1", - "react-dom": "18.3.1" - }, - "devDependencies": { - "@types/react": "18.3.12", - "@types/react-dom": "18.3.1", - "@vitejs/plugin-react": "4.3.3", - "autoprefixer": "^10.4.20", - "postcss": "8.4.48", - "tailwindcss": "3.4.14", - "vite": "5.4.11", - "vite-plugin-environment": "1.1.3" - } -} diff --git a/rust/tokenmania/frontend/postcss.config.js b/rust/tokenmania/frontend/postcss.config.js deleted file mode 100644 index 8c6e0c42c..000000000 --- a/rust/tokenmania/frontend/postcss.config.js +++ /dev/null @@ -1,6 +0,0 @@ -export default { - plugins: { - autoprefixer: {}, - tailwindcss: {} - } -}; diff --git a/rust/tokenmania/frontend/public/favicon.ico b/rust/tokenmania/frontend/public/favicon.ico deleted file mode 100644 index 338fbf34c..000000000 Binary files a/rust/tokenmania/frontend/public/favicon.ico and /dev/null differ diff --git a/rust/tokenmania/frontend/src/App.jsx b/rust/tokenmania/frontend/src/App.jsx deleted file mode 100644 index e73dea8f1..000000000 --- a/rust/tokenmania/frontend/src/App.jsx +++ /dev/null @@ -1,83 +0,0 @@ -import React, { useState, useEffect } from 'react'; -import ApproveSpender from './TokenApprove'; -import AuthWarning from './AuthWarning'; -import BalanceChecker from './BalanceChecker'; -import Header from './Header'; -import TransferFrom from './TokenTransfer'; -import TokenInfo from './TokenInfo'; -import TokenSender from './TokenSender'; -import CreateToken from './CreateToken'; - -const App = () => { - const [isAuthenticated, setIsAuthenticated] = useState(false); - const [totalSupply, setTotalSupply] = useState(''); - const [actor, setActor] = useState(); - const [tokenCreated, setTokenCreated] = useState(false); - const [decimals, setDecimals] = useState(0n); - - const updateSupply = async () => { - try { - const supply = await actor.icrc1_total_supply(); - const decimals = BigInt(await actor.icrc1_decimals()); - setTotalSupply(`${Number(supply) / Number(10n ** decimals)}`); - setDecimals(decimals); - } catch (error) { - console.error('Error fetching total supply:', error); - } - }; - - const checkTokenCreated = async () => { - try { - const result = await actor.token_created(); - setTokenCreated(result); - } catch (error) { - console.error('Error fetching token created status:', error); - } - }; - - useEffect(() => { - if (isAuthenticated || tokenCreated) { - updateSupply(); - } - }, [isAuthenticated, tokenCreated]); - - useEffect(() => { - if (actor) { - checkTokenCreated(); - } - }, [actor]); - - return ( -
-
- {tokenCreated ? ( -
- -
- {isAuthenticated ? ( -
- - - - -
- ) : ( - - )} -
-
- ) : ( -
{isAuthenticated ? : }
- )} -
- ); -}; - -export default App; diff --git a/rust/tokenmania/frontend/src/AuthWarning.jsx b/rust/tokenmania/frontend/src/AuthWarning.jsx deleted file mode 100644 index df729fe3c..000000000 --- a/rust/tokenmania/frontend/src/AuthWarning.jsx +++ /dev/null @@ -1,34 +0,0 @@ -import React from 'react'; - -const FullPageAuthWarning = ({ showIdentity }) => { - return ( -
-
-
-
-
- - - -

Authentication Required

-
-

Please sign in to access token management features.

-
-
-
-
- ); -}; - -export default FullPageAuthWarning; diff --git a/rust/tokenmania/frontend/src/BalanceChecker.jsx b/rust/tokenmania/frontend/src/BalanceChecker.jsx deleted file mode 100644 index 5cc2c5fa3..000000000 --- a/rust/tokenmania/frontend/src/BalanceChecker.jsx +++ /dev/null @@ -1,81 +0,0 @@ -import React, { useState } from 'react'; -import { Principal } from '@icp-sdk/core/principal'; -import { backend } from 'declarations/backend'; - -const BalanceChecker = ({ decimals }) => { - const [principal, setPrincipal] = useState(''); - const [subaccount, setSubaccount] = useState(''); - const [balance, setBalance] = useState(null); - const [error, setError] = useState(''); - - const handleCheckBalance = async (e) => { - e.preventDefault(); - setBalance(null); - setError(''); - - try { - const owner = Principal.fromText(principal); - const subaccountArray = subaccount - ? [new Uint8Array(subaccount.split(',').map((num) => parseInt(num.trim(), 10)))] - : []; - - const result = await backend.icrc1_balance_of({ - owner: owner, - subaccount: subaccountArray - }); - - const supplyScaler = (s) => { - return Number(s) / Number(10n ** decimals); - }; - setBalance(supplyScaler(result).toString()); - } catch (err) { - console.error('Error checking balance:', err); - setError('Failed to check balance. Please ensure the principal is valid.'); - } - }; - - const inputFields = [ - { - name: 'principal', - value: principal, - setter: setPrincipal, - placeholder: 'Principal ID', - type: 'text', - required: true - }, - { - name: 'subaccount', - value: subaccount, - setter: setSubaccount, - placeholder: 'Subaccount (optional)', - type: 'text', - required: false - } - ]; - - return ( -
-

Check Balance

-
- {inputFields.map(({ name, value, setter, placeholder, type, required }) => ( - setter(e.target.value)} - placeholder={placeholder} - required={required} - className="w-full rounded-md border px-3 py-2" - /> - ))} - -
- {balance !== null &&
Balance: {balance}
} - {error &&
{error}
}{' '} -
- ); -}; - -export default BalanceChecker; diff --git a/rust/tokenmania/frontend/src/CardDisplay.jsx b/rust/tokenmania/frontend/src/CardDisplay.jsx deleted file mode 100644 index d59f81e09..000000000 --- a/rust/tokenmania/frontend/src/CardDisplay.jsx +++ /dev/null @@ -1,36 +0,0 @@ -import React from 'react'; - -const Card = ({ icon, title, value }) => ( -
-
- {icon} - {title} -
-
{Object.values(value)}
-
-); - -const CardDisplay = ({ cards, loading }) => { - if (loading) { - return ( -
-
-
- ); - } - - return ( -
-

- Token Information -

-
- {cards.map((card, index) => ( - - ))} -
-
- ); -}; - -export default CardDisplay; diff --git a/rust/tokenmania/frontend/src/CreateToken.jsx b/rust/tokenmania/frontend/src/CreateToken.jsx deleted file mode 100644 index 47df34d40..000000000 --- a/rust/tokenmania/frontend/src/CreateToken.jsx +++ /dev/null @@ -1,110 +0,0 @@ -import React from 'react'; - -const CreateToken = ({ actor, setTokenCreated }) => { - const [tokenName, setTokenName] = React.useState(); - const [tokenSymbol, setTokenSymbol] = React.useState(); - const [tokenSupply, setTokenSupply] = React.useState(); - const [tokenLogo, setTokenLogo] = React.useState(); - - const createToken = async (e) => { - e.preventDefault(); - if (!tokenName || !tokenSymbol || !tokenSupply || !tokenLogo) { - alert('Please fill in all fields'); - return; - } - const result = await actor.create_token({ - token_name: tokenName, - token_symbol: tokenSymbol, - initial_supply: tokenSupply * Number(10 ** 8), - token_logo: tokenLogo - }); - - if ('Ok' in result) { - setTokenCreated(true); - } else if ('Err' in result) { - console.error('Failed to create token:', result.Err); - } - }; - - const handleImageChange = (e) => { - const file = e.target.files[0]; - - if (!file) { - return; - } - // Check file size - if (file.size > 1024 * 1024) { - alert('File is too large. Please select a file under 1MB.'); - return; - } - const reader = new FileReader(); - reader.onload = (e) => { - setTokenLogo(e.target.result); - }; - reader.readAsDataURL(file); - }; - - return ( -
-

Create a new token

-
-
- - setTokenName(e.target.value)} - /> -
-
- - setTokenSymbol(e.target.value)} - /> -
-
- - setTokenSupply(e.target.value)} - /> -
-
- - - {tokenLogo && Token logo preview} -
-

The principal signed in will be set as the token minter.

- -
-
- ); -}; - -export default CreateToken; diff --git a/rust/tokenmania/frontend/src/Header.jsx b/rust/tokenmania/frontend/src/Header.jsx deleted file mode 100644 index 5e7cd4676..000000000 --- a/rust/tokenmania/frontend/src/Header.jsx +++ /dev/null @@ -1,52 +0,0 @@ -import React from 'react'; -import InternetIdentity from './InternetIdentity'; -import { canisterId } from 'declarations/backend'; - -const Header = ({ actor, setActor, isAuthenticated, setIsAuthenticated, tokenCreated, setTokenCreated }) => { - const handleDeleteToken = async () => { - try { - const result = await actor.delete_token(); - if ('Ok' in result) { - setTokenCreated(false); - } else if ('Err' in result) { - console.error('Failed to delete token:', result.Err); - alert('Failed to delete token: ' + result.Err); - } - } catch (error) { - console.error('Error deleting token:', error); - } - }; - - return ( -
-
-

Tokenmania

-
- - {isAuthenticated && tokenCreated && ( -
- - -
- )} -
-
-
- ); -}; - -export default Header; diff --git a/rust/tokenmania/frontend/src/InternetIdentity.jsx b/rust/tokenmania/frontend/src/InternetIdentity.jsx deleted file mode 100644 index 5e6793b40..000000000 --- a/rust/tokenmania/frontend/src/InternetIdentity.jsx +++ /dev/null @@ -1,72 +0,0 @@ -import React, { useEffect, useState } from 'react'; -import { AuthClient } from '@icp-sdk/auth/client'; -import { createActor, canisterId } from 'declarations/backend'; - -const network = process.env.DFX_NETWORK; -const identityProvider = - network === 'ic' - ? 'https://id.ai/' // Mainnet - : 'http://uqzsh-gqaaa-aaaaq-qaada-cai.localhost:4943'; // Local - -const InternetIdentity = ({ setActor, isAuthenticated, setIsAuthenticated }) => { - const [authClient, setAuthClient] = useState(); - const [principal, setPrincipal] = useState(); - useEffect(() => { - updateActor(); - }, []); - - async function updateActor() { - const authClient = await AuthClient.create(); - const identity = authClient.getIdentity(); - const actor = createActor(canisterId, { - agentOptions: { - identity - } - }); - const isAuthenticated = await authClient.isAuthenticated(); - - setActor(actor); - setAuthClient(authClient); - setIsAuthenticated(isAuthenticated); - setPrincipal(identity.getPrincipal().toString()); - } - - async function login() { - await authClient.login({ - identityProvider, - onSuccess: updateActor - }); - } - - async function logout() { - await authClient.logout(); - updateActor(); - } - - return ( -
- {isAuthenticated ? ( - <> -

- {principal} -

- - - ) : ( - - )} -
- ); -}; - -export default InternetIdentity; diff --git a/rust/tokenmania/frontend/src/StatusMessage.jsx b/rust/tokenmania/frontend/src/StatusMessage.jsx deleted file mode 100644 index f76c8f4ed..000000000 --- a/rust/tokenmania/frontend/src/StatusMessage.jsx +++ /dev/null @@ -1,24 +0,0 @@ -import React from 'react'; - -const StatusMessage = ({ message, isSuccess }) => { - if (!message) return null; - - return ( -
- - {isSuccess ? : } - - {message} -
- ); -}; - -export default StatusMessage; diff --git a/rust/tokenmania/frontend/src/TokenApprove.jsx b/rust/tokenmania/frontend/src/TokenApprove.jsx deleted file mode 100644 index 175cf6561..000000000 --- a/rust/tokenmania/frontend/src/TokenApprove.jsx +++ /dev/null @@ -1,96 +0,0 @@ -import React, { useState } from 'react'; -import { Principal } from '@icp-sdk/core/principal'; -import StatusMessage from './StatusMessage'; - -const ApproveSpender = ({ actor, decimals }) => { - const [spenderAddress, setSpenderAddress] = useState(''); - const [amount, setAmount] = useState(''); - const [fromSubaccount, setFromSubaccount] = useState(''); - const [status, setStatus] = useState({ message: '', isSuccess: null }); - - const handleApprove = async (e) => { - e.preventDefault(); - try { - const result = await actor.icrc2_approve({ - spender: { owner: Principal.fromText(spenderAddress), subaccount: [] }, - amount: amount * Number(10 ** Number(decimals)), - from_subaccount: fromSubaccount ? [fromSubaccount] : [], - expires_at: [], - expected_allowance: [], - memo: [], - fee: [], - created_at_time: [] - }); - if ('Ok' in result) { - setStatus({ message: 'Approval successful', isSuccess: true }); - } else if ('Err' in result) { - setStatus({ - message: `Approval failed: ${Object.keys(result.Err)[0]}`, - isSuccess: false - }); - } - } catch (error) { - console.error('Approval failed:', error); - setStatus({ - message: 'Approval failed failed: Unexpected error', - isSuccess: false - }); - } - }; - - const inputFields = [ - { - name: 'fromSubaccount', - value: fromSubaccount, - setter: setFromSubaccount, - placeholder: 'From Subaccount (optional)', - type: 'text', - required: false - }, - { - name: 'spenderAddress', - value: spenderAddress, - setter: setSpenderAddress, - placeholder: 'Spender Address', - type: 'text', - required: true - }, - { - name: 'amount', - value: amount, - setter: setAmount, - placeholder: 'Approved Amount', - type: 'number', - required: true, - min: '0', - step: '0.000001' - } - ]; - - return ( -
-

Approve Spender

-
- {inputFields.map(({ name, value, setter, placeholder, type, required, min, step }) => ( - setter(e.target.value)} - placeholder={placeholder} - required={required} - min={min} - step={step} - className="w-full rounded-md border px-3 py-2" - /> - ))} - -
- -
- ); -}; - -export default ApproveSpender; diff --git a/rust/tokenmania/frontend/src/TokenInfo.jsx b/rust/tokenmania/frontend/src/TokenInfo.jsx deleted file mode 100644 index 98968e5c2..000000000 --- a/rust/tokenmania/frontend/src/TokenInfo.jsx +++ /dev/null @@ -1,56 +0,0 @@ -import React, { useState, useEffect } from 'react'; -import { backend, canisterId } from 'declarations/backend'; -import CardDisplay from './CardDisplay'; - -const TokenInfo = ({ totalSupply }) => { - const [tokenInfo, setTokenInfo] = useState({ - name: '', - symbol: '', - loading: true - }); - - useEffect(() => { - const fetchTokenInfo = async () => { - try { - const metadata = await backend.icrc1_metadata(); - const newTokenInfo = metadata.reduce((acc, [key, value]) => { - const parsedKey = key.split(':')[1].trim(); - if (parsedKey === 'name' || parsedKey === 'symbol') { - acc[parsedKey] = value.Text; - } - return acc; - }, {}); - - setTokenInfo((prevState) => ({ - ...prevState, - ...newTokenInfo, - loading: false - })); - } catch (error) { - console.error('Error fetching token info:', error); - setTokenInfo((prevState) => ({ ...prevState, loading: false })); - } - }; - - fetchTokenInfo(); - }, []); - - if (tokenInfo.loading) { - return ( -
-
-
- ); - } - - const cardInfo = [ - { icon: '💰', title: 'Name', value: tokenInfo.name }, - { icon: '🏷️', title: 'Symbol', value: tokenInfo.symbol }, - { icon: '📊', title: 'Total Supply', value: totalSupply }, - { icon: '💳', title: 'Token Address (ICRC-2)', value: canisterId } - ]; - - return ; -}; - -export default TokenInfo; diff --git a/rust/tokenmania/frontend/src/TokenSender.jsx b/rust/tokenmania/frontend/src/TokenSender.jsx deleted file mode 100644 index 762d21df3..000000000 --- a/rust/tokenmania/frontend/src/TokenSender.jsx +++ /dev/null @@ -1,105 +0,0 @@ -import React, { useState } from 'react'; -import { Principal } from '@icp-sdk/core/principal'; -import StatusMessage from './StatusMessage'; - -const TokenSender = ({ actor, updateSupply, decimals }) => { - const [fromSubaccount, setFromSubaccount] = useState(''); - const [address, setAddress] = useState(''); - const [amount, setAmount] = useState(); - const [status, setStatus] = useState({ message: '', isSuccess: null }); - - const handleSendTransaction = async (e) => { - e.preventDefault(); - try { - const result = await actor.icrc1_transfer({ - to: { - owner: Principal.fromText(address), - subaccount: [] - }, - fee: [], - memo: [], - from_subaccount: fromSubaccount ? [fromSubaccount] : [], - created_at_time: [], - amount: amount * Number(10 ** Number(decimals)) - }); - if ('Ok' in result) { - setStatus({ message: 'Transfer successful', isSuccess: true }); - updateSupply(); - } else if ('Err' in result) { - if ('InsufficientFunds' in result.Err) { - setStatus({ - message: `Transfer failed: Insufficient funds. Available balance: ${result.Err.InsufficientFunds.balance}`, - isSuccess: false - }); - } else { - setStatus({ - message: `Transfer failed: ${Object.keys(result.Err)[0]}`, - isSuccess: false - }); - } - } - } catch (error) { - console.error('Transfer failed:', error); - setStatus({ - message: 'Transfer failed: Unexpected error', - isSuccess: false - }); - } - }; - - const inputFields = [ - { - name: 'fromSubaccount', - value: fromSubaccount, - setter: setFromSubaccount, - placeholder: 'From Subaccount (optional)', - type: 'text', - required: false - }, - { - name: 'address', - value: address, - setter: setAddress, - placeholder: 'Recipient Address', - type: 'text', - required: true - }, - { - name: 'amount', - value: amount, - setter: setAmount, - placeholder: 'Amount', - type: 'number', - required: true, - min: '0', - step: '0.000001' - } - ]; - - return ( -
-

Send/Mint Tokens

-
- {inputFields.map(({ name, value, setter, placeholder, type, required, min, step }) => ( - setter(e.target.value)} - placeholder={placeholder} - required={required} - min={min} - step={step} - className="w-full rounded-md border px-3 py-2" - /> - ))} - -
- -
- ); -}; - -export default TokenSender; diff --git a/rust/tokenmania/frontend/src/TokenTransfer.jsx b/rust/tokenmania/frontend/src/TokenTransfer.jsx deleted file mode 100644 index 912347b69..000000000 --- a/rust/tokenmania/frontend/src/TokenTransfer.jsx +++ /dev/null @@ -1,104 +0,0 @@ -import React, { useState } from 'react'; -import { Principal } from '@icp-sdk/core/principal'; -import StatusMessage from './StatusMessage'; - -const TransferFrom = ({ actor, decimals }) => { - const [fromAddress, setFromAddress] = useState(''); - const [toAddress, setToAddress] = useState(''); - const [amount, setAmount] = useState(); - const [spenderSubaccount, setSpenderSubaccount] = useState(''); - const [status, setStatus] = useState({ message: '', isSuccess: null }); - - const handleTransferFrom = async (e) => { - e.preventDefault(); - try { - const result = await actor.icrc2_transfer_from({ - from: { owner: Principal.fromText(fromAddress), subaccount: [] }, - to: { owner: Principal.fromText(toAddress), subaccount: [] }, - amount: amount * Number(10 ** Number(decimals)), - spender_subaccount: spenderSubaccount ? [spenderSubaccount] : [], - fee: [], - memo: [], - created_at_time: [] - }); - if ('Ok' in result) { - setStatus({ message: 'Transfer successful', isSuccess: true }); - } else if ('Err' in result) { - setStatus({ - message: `Transfer failed: ${Object.keys(result.Err)[0]}`, - isSuccess: false - }); - } - } catch (error) { - console.error('Transfer failed:', error); - setStatus({ - message: 'Transfer failed: Unexpected error', - isSuccess: false - }); - } - }; - - const inputFields = [ - { - name: 'fromAddress', - value: fromAddress, - setter: setFromAddress, - placeholder: 'From Address', - type: 'text', - required: true - }, - { - name: 'toAddress', - value: toAddress, - setter: setToAddress, - placeholder: 'To Address', - type: 'text', - required: true - }, - { - name: 'amount', - value: amount, - setter: setAmount, - placeholder: 'Amount', - type: 'number', - required: true, - min: '0', - step: '0.000001' - }, - { - name: 'spenderSubaccount', - value: spenderSubaccount, - setter: setSpenderSubaccount, - placeholder: 'Spender Subaccount (optional)', - type: 'text', - required: false - } - ]; - - return ( -
-

Transfer From

-
- {inputFields.map(({ name, value, setter, placeholder, type, required, min, step }) => ( - setter(e.target.value)} - placeholder={placeholder} - required={required} - min={min} - step={step} - className="w-full rounded-md border px-3 py-2" - /> - ))} - -
- -
- ); -}; - -export default TransferFrom; diff --git a/rust/tokenmania/frontend/src/main.jsx b/rust/tokenmania/frontend/src/main.jsx deleted file mode 100644 index 70e834f85..000000000 --- a/rust/tokenmania/frontend/src/main.jsx +++ /dev/null @@ -1,10 +0,0 @@ -import React from 'react'; -import ReactDOM from 'react-dom/client'; -import App from './App'; -import '../index.css'; - -ReactDOM.createRoot(document.getElementById('root')).render( - - - -); diff --git a/rust/tokenmania/frontend/tailwind.config.js b/rust/tokenmania/frontend/tailwind.config.js deleted file mode 100644 index c1d69a222..000000000 --- a/rust/tokenmania/frontend/tailwind.config.js +++ /dev/null @@ -1,19 +0,0 @@ -export default { - content: ['./src/**/*.{js,ts,jsx,tsx}'], - theme: { - extend: { - colors: { - infinite: '#3b00b9', - 'dark-infinite': '#1e005d', - razzmatazz: '#ed1e79', - flamingo: '#f15a24', - 'sea-buckthron': '#fbb03b', - 'picton-blue': '#29abe2', - meteorite: '#522785' - }, - fontFamily: { - body: ['Circular Std', 'sans'] - } - } - } -}; diff --git a/rust/tokenmania/frontend/vite.config.js b/rust/tokenmania/frontend/vite.config.js deleted file mode 100644 index f9e04a9a9..000000000 --- a/rust/tokenmania/frontend/vite.config.js +++ /dev/null @@ -1,37 +0,0 @@ -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/rust/tokenmania/package.json b/rust/tokenmania/package.json deleted file mode 100644 index 38b95f808..000000000 --- a/rust/tokenmania/package.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "name": "tokenmania", - "scripts": { - "build": "npm run build --workspaces --if-present", - "prebuild": "npm run prebuild --workspaces --if-present", - "dev": "npm run dev --workspaces --if-present" - }, - "type": "module", - "workspaces": [ - "frontend" - ] -} diff --git a/rust/tokenmania/rust-toolchain.toml b/rust/tokenmania/rust-toolchain.toml deleted file mode 100644 index 990104f05..000000000 --- a/rust/tokenmania/rust-toolchain.toml +++ /dev/null @@ -1,2 +0,0 @@ -[toolchain] -targets = ["wasm32-unknown-unknown"] diff --git a/rust/vetkeys/encrypted_chat/README.md b/rust/vetkeys/encrypted_chat/README.md deleted file mode 100644 index b2ecfa4bd..000000000 --- a/rust/vetkeys/encrypted_chat/README.md +++ /dev/null @@ -1,62 +0,0 @@ -# Encrypted Chat - -| Rust backend | [![](https://icp.ninja/assets/open.svg)](https://icp.ninja/editor?g=https://github.com/dfinity/examples/tree/master/rust/vetkeys/encrypted_chat/rust) | -| --- | --- | - -> **Disclaimer**: This is an *unfinished* prototype. DO NOT USE IN PRODUCTION. - -The **Encrypted Chat** example demonstrates how to use **[vetKeys](https://internetcomputer.org/docs/building-apps/network-features/vetkeys/introduction)** to build an end-to-end encrypted messaging application on the **Internet Computer (IC)**. Messages are encrypted on the sender's device and can only be decrypted by the intended recipients — the backend canister never sees plaintext. - -See [SPEC.md](./SPEC.md) for the full technical specification. - -## Features - -- **End-to-end encrypted messaging**: Messages are encrypted client-side using keys derived from vetKeys. -- **Direct and group chats**: Support for both one-on-one and multi-participant conversations. -- **Symmetric ratchet key rotation**: Keys evolve over time via a symmetric ratchet, providing forward security. -- **Disappearing messages**: Messages expire and are automatically purged from both frontend and backend. -- **Encrypted state recovery**: Users can recover their decryption capability across devices using IBE-encrypted key resharing. - -## Setup - -### Prerequisites - -- [Internet Computer software development kit](https://internetcomputer.org/docs/building-apps/getting-started/install) -- [npm](https://www.npmjs.com/package/npm) - -### Deploy the Canisters Locally - -From the `rust` folder, run: -```bash -dfx start --background && dfx deploy -``` - -## Example Components - -### Backend - -The Rust backend canister manages: -- Chat creation (direct and group) with configurable key rotation and message expiration periods. -- Encrypted message storage and retrieval. -- VetKey derivation for chat encryption keys, with epoch-based rotation. -- Group membership management (add/remove participants). - -### Frontend - -The frontend is a SvelteKit application providing: -- Internet Identity authentication. -- Real-time encrypted messaging interface. -- Local message caching with IndexedDB. -- Automatic key ratcheting and vetKey epoch management. - -To run the frontend in development mode with hot reloading (after running `dfx deploy`): - -```bash -cd frontend -npm run dev -``` - -## Additional Resources - -- **[SPEC.md](./SPEC.md)** - Full technical specification of the encryption protocol. -- **[What are VetKeys](https://internetcomputer.org/docs/building-apps/network-features/encryption/vetkeys)** - For more information about VetKeys and VetKD. diff --git a/rust/vetkeys/encrypted_chat/SPEC.md b/rust/vetkeys/encrypted_chat/SPEC.md deleted file mode 100644 index 7351b8b42..000000000 --- a/rust/vetkeys/encrypted_chat/SPEC.md +++ /dev/null @@ -1,1178 +0,0 @@ -# vetKey Encrypted Chat - -vetKey Encrypted Chat has two main components: the canister backend and user frontend. It provides the following features: - -* End-to-end encrypted messaging. - -* High security through symmetric ratchet and key rotation via [vetKeys](https://internetcomputer.org/docs/building-apps/network-features/vetkeys/introduction). - -* Disappearing messages, enforced by the canister (ICP smart contract) logic. Messages are automatically removed from the frontend and encrypted messages are purged from the backend once they expire. - -* Encrypted state recovery, enabling users to securely restore their message-decryption capability across different devices. - -## Encryption Keys and State Recovery - -### Key Hierarchy - -vetKey encrypted chat uses three layers of cryptographic keys: -* **vetKeys**: shared keys established thorough the [vetKD protocol](https://internetcomputer.org/docs/references/vetkeys-overview). -They rotate periodically, e.g. upon group configuration changes, to ensure both forward security and post-compromise security. -This means that an adversary who obtains the key material for one vetKey epoch gains no information about past or future epochs. -Deriving new vetKeys incurs some cost, as it requires interaction with the backend canister, which triggers a vetKey-derivation protocol on the ICP. -* **Symmetric key ratchet**: a continuously evolving chain of symmetric keys derived from the current vetKey. -It provides forward security but not post-compromise security: an adversary who obtains the ratchet key for step i can derive all future ratchet states for that same vetKey epoch. -Ratchet advancement is efficient and can be performed locally by each participant without interacting with the backend canister. -* **Message encryption keys**: per-message keys derived from the current symmetric ratchet state. - -### Key Rotation - -While vetKey rotations can occur at arbitrary times, the symmetric ratchet progresses strictly at fixed time-frame boundaries. -The length of this time frame determines how long a user must retain ratchet key material to decrypt messages. - -Retaining some ratchet states is necessary to support out-of-order decryption. -For example, a client may only want to decrypt recent messages shown in the UI—not the entire history, which may include large media requiring unnecessary downloads. -To decrypt the oldest non-expired message, the client must keep the appropriate ratchet state. - -The duration of the symmetric ratchet time frame directly affects how long clients must retain decryption capability beyond what is strictly necessary. -For example, if each symmetric ratchet epoch has length `r` and the encrypted chat maintains a message history of length `h ≤ k·r`, then the frontend may need to keep the ratchet state for up to `k + 1` epochs in order to decrypt any message within that history window. - - -### Encrypted State Recovery - -Encrypted state recovery allows a user to restore their ability to decrypt messages on a new device by securely caching encrypted symmetric ratchet states in the backend. -Each ratchet state is encrypted client-side using an individual key, ensuring that the backend never learns any cryptographic material that would enable message decryption. - -Since the symmetric ratchet is instantiated from a vetKey, the ability to obtain the vetKey allows to decrypt all messages encrypted using any derived ratchet states. -To provide a _limited_ state recovery it is crucial to: -* Prevent the frontend to obtain the initial vetKey once the user has uploaded the encrypted cache for its epoch. -* Encrypt and upload the necessary symmetric ratchet states using user-specific encryption keys. -* Retrieve and decrypt the stored ratchet states during recovery, restoring the user's ability to decrypt messages without exposing the initial vetKey. - -Users who do not wish to enable state recovery may still want to signal to the backend that they have retrieved the vetKey, thereby preventing any future retrievals. This can be achieved, for example, by uploading a deliberately invalid encrypted cache once, which marks the vetKey as unrecoverable for that epoch. - -Practical symmetric-ratchet epoch durations range from a few minutes to several hours or even days. -When state recovery is enabled, shorter epochs increase the frequency of encrypted-cache updates, which in turn raises cycle consumption on the backend. -As a result, very short time frames may be impractical in large or busy chats. - -## Components - -vetKey Encrypted Chat consists of a backend canister on the ICP and a user frontend. - -The backend's responsibilities are: - -* Providing APIs for chat interactions and key retrieval from vetKeys. - -* Storing chat metadata and users' encrypted messages. - -* Ordering of incoming messages. - -* Validation of encryption key metadata correctness for incoming messages. - -* Access control for user requests to both encryption keys and chat data. - -* Cleanup of expired messages if message expiration is turned on in a chat. - -* Storing user's encrypted key cache that allows the user to restore a former symmetric ratchet state in case of a state loss, e.g., upon browser change. - -The frontend's responsibilities are: - -* Providing a chat UI similar to Signal, WhatsApp, etc. - -* Synchronizing metadata for accessible chats. - -* Obtaining keys required for message encryption/decryption. - -* Encrypting and sending outgoing messages. - -* Fetching and decrypting incoming messages. - -* Upload user's encrypted key cache in the backend. - -## Backend Canister Component - -### Backend State - -* Chat data - - * Chat IDs - each chat has a chat ID - - * vetKey epochs - each chat has one or more vetKey epochs - - * vetKey epoch ID for each vetKey epoch in the chat - - * Participants who have access to the chat at the vetKey epoch - - * Creation time of the vetKey epoch - - * Symmetric key ratchet rotation duration at the vetKey epoch - - * Message ID that the vetKey epoch starts with in the chat - - * Messages - - * Chat Message ID that is assigned by the canister - - * Nonce use for message encryption that is assigned by the user - - * Consensus time at message receival - - * vetKey epoch ID when the message was received - - * Encrypted bytes of the message content. - - * Message expiry - - * Number of expired messages in the chat - - * Message expiry setting - how long does it take for a message to expire - -* User data per chat and vetKey epoch - - * [User-uploaded optional encrypted symmetric ratchet state cache](#state-cache) - - * Optional optimization: [IBE-encrypted vetKey reshared by another user](#ibe-encrypted-vetkey-resharing) - -### Chat Creation - -Upon receiving a call from the frontend to create a chat via one of the following APIs - -``` -type OtherParticipant = principal; -type TimeNanos = nat64; -type SymmetricKeyRotationMins = nat64; -type GroupChatId = nat64; -type GroupChatMetadata = record { creation_timestamp : TimeNanos; chat_id : GroupChatId }; - -create_direct_chat : (OtherParticipant, SymmetricKeyRotationMins) -> variant { Ok : TimeNanos; Err : text }; -create_group_chat : (vec OtherParticipant, SymmetricKeyRotationMins) -> (variant { Ok : GroupChatMetadata; Err : text }); -``` - -the backend does the following: - -* Checks that a direct chat does not exist yet if `create_direct_chat` was called and returns an error if the check fails. - -* Checks that `SymmetricKeyRotationMins` do not cause overflows in `nat64` types if converted to nanoseconds and returns an error if the check fails. - -* Deduplicates group chat participants if `create_group_chat` was called. - -* If all checks pass, adds the chat ID and users who have access to it to the state. The return value of `create_direct_chat` is the current consensus time indicating the chat creation time (which is required to correctly compute the symmetric key epoch that the frontend needs to encrypt messages with). The return value of `create_group_chat` is `GroupChatMetadata`, which contains the chat creation time as well as the group chat ID. The group chat ID does not depend on the caller's inputs (in contrast to direct chat IDs), and thus must be returned explicitly. - -### Group Changes - -Group changes in a group chat such as addition or removal of users can be triggered using the following backend canister API: -``` -type GroupChatId = nat64; -type VetKeyEpochId = nat64; -type KeyRotationResult = variant { Ok : VetKeyEpochId; Err : text }; -type GroupModification = record { - remove_participants : vec principal; - add_participants : vec principal; -}; - -modify_group_chat_participants : (GroupChatId, GroupModification) -> (KeyRotationResult); -``` - -The API takes in a group chat ID and a set of group changes. -When this API is triggered, the canister checks that: - -* The group chat exists. - -* The user has access to the group chat at the latest vetKey epoch. - -* The user is authorized to make group changes. This is an implementation detail and is out of scope of this document. Authorizing users to make group changes can be performed via a separate API and can be implemented with different rules, e.g., admins can make changes, or more fine-grained access can be implemented such as admins, moderators, etc., or even every user can perform group changes. - -* The passed `GroupModification` is valid: - - * `remove_participants` or `add_participants` is non-empty. - - * Every `principal` in `remove_participants` has access to the chat. - - * No `principal` in `add_participants` has access to the chat. - -Note that the latter two points guarantee that there is no intersection between `remove_participants` and `add_participants`. - -A group change triggers a vetKey epoch rotation that updates the set of group participants according to the passed `GroupModification` and stores it in the next vetKey epoch for the chat. The effects of vetKey epoch rotation are further discussed in the [vetKet Epoch Rotation](#vetkey-epoch-rotation) section. - -The user removed from a group chat loses access to the messages and vetKeys (as well as key cache) in that chat and does not regain access if added to that group chat later. -Instead, if two users are added to the chat in the same call, while one of the users has previously had access to the chat but was removed and the other user never had access to that chat, they would be able to access only the same messages and vetKeys. - -> [!NOTE] -> One call to `modify_group_chat_participants` triggers one vetKey epoch rotation even if multiple `principals` are added or removed. Further potential optimizations for reducing the number of vetKey epoch rotations or the number of vetKey retrievals are discussed in [Optimizations](#optimizations). - -### Incoming Message Validation - -Upon receival of a user message via the following API - -``` -type GroupChatId = nat64; -type ChatId = variant { - Group : GroupChatId; - Direct : record { principal; principal }; -}; -type VetKeyEpochId = nat64; -type EncryptedBytes = blob; -type SymmetricKeyEpochId = nat64; -type Nonce = blob; -type UserMessage = record { - vetkey_epoch_id : VetKeyEpochId; - content : EncryptedBytes; - symmetric_key_epoch_id : SymmetricKeyEpochId; - nonce : Nonce; -}; -type MessagingError = variant { WrongVetKeyEpoch; WrongSymmetricKeyEpoch; Custom: text }; - -send_message : (ChatId, UserMessage) -> (variant { Ok; Err : MessageSendingError }); -``` - -the canister validates the message metadata and ensures that the caller has access. - -More specifically, the canister checks that: -* The caller has access to the chat at the passed `vetkey_epoch_id` or returns a `Custom` variant of `MessageSendingError` if the check fails. -* `vetkey_epoch_id` attached to the message is the latest for the chat ID or returns the `WrongVetKeyEpoch` variant of `MessageSendingError` if the check fails. -* `symmetric_key_epoch_id` attached to the message is equal to the [current symmetric key epoch ID](#calculating-current-symmetric-ratchet-epoch-id) corresponding the current consensus time. To check that, the canister calculates the current symmetric ratchet epoch ID for the chat and `vetkey_epoch_id`. If the check fails, the canister returns the `WrongSymmetricKeyEpoch` variant of `MessageSendingError` if the check fails. - -> [!NOTE] -> This API assumes that the frontend's clock is reasonably synchronized with the ICP to encrypt the messages with the key from the right symmetric ratchet state. This does not pose a significant limitation, since 1) it must already be the case for facilitating reliable communication with the ICP in general and 2) re-encrypting and re-sending in case of failures can be done automatically by the frontend. - -If the checks pass, the canister accepts the message, assigns to it: - -* The current consensus time as its timestamp, which is needed for computing the message expiry but also to display the message arrival time in the chat UI. - -* A chat message ID, which is unique and assigned from an incrementing counter starting from zero. Note that the current number of messages in the chat is different than the value of the counter if some messages have expired. - -Finally, the canister adds the message to the state and returns an `Ok`. - -### Exposing Metadata about Chats and New Messages - -The backend canister exposes the following APIs for fetching metadata: - -``` -type GroupChatId = nat64; -type ChatId = variant { - Group : GroupChatId; - Direct : record { principal; principal }; -}; -type NumberOfMessages = nat64; -type ChatMetadata = record { - chat_id : ChatId; - number_of_messages : NumberOfMessages; - vetkey_epoch_id : VetKeyEpochId; -}; - -type SymmetricKeyRotationMins = nat64; -type ChatMessageId = nat64; -type TimeNanos = nat64; -type VetKeyEpochId = nat64; -type VetKeyEpochMetadata = record { - symmetric_key_rotation_duration : SymmetricKeyRotationMins; - participants : vec principal; - messages_start_with_id : ChatMessageId; - creation_timestamp : TimeNanos; - epoch_id : VetKeyEpochId; -}; - -get_my_chats_and_time : () -> (record { chats : vec ChatMetadata; consensus_time_now : TimeNanos }) query; -get_vetkey_epoch_metadata : (ChatId, VetKeyEpochId) -> (variant { Ok : VetKeyEpochMetadata; Err : text }) query; -``` - -* The `get_my_chats_and_time` API returns a vector of all chat IDs accessible to the user as well as their their current total number of messages and vetKey epoch ID. The frontend can detect new chats and new messages in existing chats by periodically querying `get_my_chats_and_time`. Also, this API returns the current consensus time, which is e.g. useful to compute the message expiry and to determine if symmetric ratchet states need to be evolved. The current total number of messages (`NumberOfMessages`) includes the messages in the accessible chat ID that are not accessible to the user. This can happen if some messages have expired or in group chats, where the user joined at a later point. If a user is [removed from a chat](#group-changes), the result of `get_my_chats_and_time` called by the user will not include that chat anymore. Note that the latter only leaks to the user how many messages were in the chat before the user joined. - -* The `get_vetkey_epoch_metadata` API checks that the user has access to `ChatId` at `VetKeyEpochId` and if the test passes, the API returns the corresponding `VetKeyEpochMetadata` or an error otherwise. - -> [!NOTE] -> This API is exposed as `query` and, therefore, requires handling of cases where a replica would return incorrect data. This is further discussed [Ensuring Correctness of Query Calls](#ensuring-correctness-of-query-calls). - -### Encrypted Message Retrieval - -To allow the frontend to retrieve encrypted messages, the backend canister exposes the following backend canister API: - -``` -type GroupChatId = nat64; -type ChatId = variant { - Group : GroupChatId; - Direct : record { principal; principal }; -}; -type ChatMessageId = nat64; -type Limit = nat32; -type EncryptedBytes = blob; -type EncryptedMessage = record { - content : EncryptedBytes; - metadata : EncryptedMessageMetadata; -}; -type VetKeyEpochId = nat64; -type SymmetricKeyEpochId = nat64; -type TimeNanos = nat64; -type Nonce = blob; -type EncryptedMessageMetadata = record { - vetkey_epoch : VetKeyEpochId; - sender : principal; - symmetric_key_epoch_id : SymmetricKeyEpochId; - chat_message_id : ChatMessageId; - timestamp : TimeNanos; - nonce : Nonce; -}; - -get_messages : (ChatId, ChatMessageId, opt Limit) -> ( - vec EncryptedMessage, - ) query; -``` - -The `get_messages` API takes in a chat ID, the first message ID to retrieve, and an optional limit value for the maximum number of messages to retrieve in this call. -The API returns a vector of `EncryptedMessage`s. -If the user does not have access to the chat or the chat does not exist, an empty vector is returned. -If the user does not have access to particular messages, e.g., if the user was [added to a group chat](#group-changes) after some activity, or if some of the messages [expired](#disappearing-messages), then those messages are skipped. - -If a user is removed from a chat and afterwards the user is added to the chat again (with or without some messages being added in-between), the user will not have access to the messages that were visible before the user was removed from the chat. -This applies to any number of repetitions of this process. -That is, only the last range of messages without gaps is accessible to the user. -This also applies to other backend canister APIs that require the user to have access to a particular vetKey epoch such as vetKey epoch and encrypted user cache retrieval, and vetKey derivation. - -> [!NOTE] -> This API is exposed as `query` and, therefore, requires handling of cases where a replica would return incorrect data. This is further discussed [Ensuring Correctness of Query Calls](#ensuring-correctness-of-query-calls). - -### Providing vetKeys for Symmetric Ratchet Initialization - -Symmetric ratchet state is initialized from a vetKey that is the same for all chat participants. -To fetch a vetKey, the user calls the following backend canister API: -``` -type PublicTransportKey = blob; -type GroupChatId = nat64; -type VetKeyEpochId = nat64; -type ChatId = variant { - Group : GroupChatId; - Direct : record { principal; principal }; -}; -type EncryptedVetKey = blob; - -derive_chat_vetkey : (ChatId, VetKeyEpochId, PublicTransportKey) -> (variant { Ok : EncryptedVetKey; Err : text }); -``` - -Then, the canister checks that: - -* The chat corresponding to the passed `ChatId` exists. - -* The user has access to the chat at the passed `VetKeyEpochId`. - -* The user did not upload an encrypted cache for his symmetric ratchet state for the vetKey epoch in question (see [State Recovery](#state-recovery)). - -If the checks pass, the canister calls the [`vetkd_derive_key`](https://internetcomputer.org/docs/building-apps/network-features/vetkeys/api) API of the management canister with: - -* `context` being computed by invoking the `ratchet_context` function defined below. - -* `input` being the big-endian encoding of `VetKeyEpochId`. - -* `transport_public_key` being the `PublicTransportKey` input argument. - -* `key_id` being an implementation detail. - -```rust -pub fn ratchet_context(chat_id_bytes: &[u8]) -> Vec { - pub static DOMAIN_SEPARATOR_VETKEY_ROTATION: &str = "vetkeys-example-encrypted-chat-rotation"; - - let mut context = vec![]; - context.extend_from_slice(&[DOMAIN_SEPARATOR_VETKEY_ROTATION.len() as u8]); - context.extend_from_slice(DOMAIN_SEPARATOR_VETKEY_ROTATION.as_bytes()); - context.extend_from_slice(chat_id_bytes); - context -} -``` - -> [!NOTE] -> `chat_id_bytes` is a serialization of the chat ID and is an implementation detail. - -The actual initialization of the symmetric ratchet state is performed in the frontend and is, therefore, specified in [Ratchet Initialization](#ratchet-initialization) in the frontend. - -### Encrypted Symmetric Ratchet State Cache - -The encrypted ratchet state cache is intended to allow the user to upload encrypted symmetric ratchet epoch keys to the canister and then [recover](#state-recovery) the local state in the frontend whenever needed, e.g., for disaster recovery or after a browser change. - -There is a relation between state recovery and the [disappearing messages](#disappearing-messages) duration. -The state recovery duration is always smaller or equal to the disappearing messages duration because keys for messages that don't exist anymore are not useful. - -While disappearing messages duration is a chat-level setting, the state recovery duration is a user-level setting in a chat. -In general, the state recovery limit can be both chat-level and user-level setting, but it makes more sense to make state recovery a user setting because the user frontend is in full control of state recovery for a non-expired vetKey epoch. [Note that the current MVP of encrypted-chat does not support this setting.] - -In the canister backend, the state recovery limit is used in three cases - all related to expired vetKey epochs: -* Removal of expired caches. -* Acceptance/rejection of cache uploads. -* Acceptance/rejection of cache downloads (because removal may not happen instantly). - -The canister backend provides the following APIs for storing and obtaining users' encrypted symmetric ratchet states in the canister: -``` -type PublicTransportKey = blob; -type EncryptedVetKey = blob; -type GroupChatId = nat64; -type ChatId = variant { - Group : GroupChatId; - Direct : record { principal; principal }; -}; -type VetKeyEpochId = nat64; -type EncryptedSymmetricRatchetCache = blob; -type DerivedVetKeyPublicKey = blob; - -get_vetkey_for_my_cache_encryption : (PublicTransportKey) -> (EncryptedVetKey); -get_my_symmetric_key_cache : (ChatId, VetKeyEpochId) -> (variant { Ok : opt EncryptedSymmetricRatchetCache; Err : text }); -update_my_symmetric_key_cache : (ChatId, VetKeyEpochId, EncryptedSymmetricRatchetCache) -> (variant { Ok; Err : text }); -``` - -To facilitate those APIs, we make use of [Encrypted Maps](https://docs.rs/ic-vetkeys/latest/ic_vetkeys/encrypted_maps/struct.EncryptedMaps.html), which, as the name says, allow users to upload encrypted data into a map structure inside the canister. -The advantage in using Encrypted Maps is that 1) we do not need to design and implement such a scheme ourselves, and 2) Encrypted Maps allow to encrypt data efficiently in terms of both the number of fetched vetKeys and the efficiency of the used cryptography. - -On a high level, the canister creates exactly one encrypted map for the user that stores _all_ their key caches. -The cache is stored in the map as a `(key, value)`, where `key` is a serialization of tuple `(ChatId, VetKeyEpochId)` and `value` is `EncryptedSymmetricRatchetCache`. - -The user calls `get_vetkey_for_my_cache_encryption` to obtain the vetKey used for data encryption for their storage, which is called once upon initialization of the state in the frontend. -It can be called multiple times in general if the client loses their local state in the browser, e.g., when the client wants to use it on another device or in a different browser. - -The user calls `update_my_symmetric_key_cache` and `get_my_symmetric_key_cache` to update or fetch their cache. -In `update_my_symmetric_key_cache`, the canister checks that: - -* The user has access to the passed `ChatId` and `VetKeyEpochId`. - -* The passed `EncryptedSymmetricRatchetCache` has a reasonable size. The purpose of this check is to prevent misuse e.g. for cycles draining attacks where an attacker would store huge amounts of data as `EncryptedSymmetricRatchetCache`. What a reasonable size is depends on how the symmetric ratchet state is serialized in the frontend, which is an implementation detail, but generally the limit can be quite generous, e.g., 100 bytes. In most cases the size will be fixed though, since `EncryptedSymmetricRatchetCache` contains an encryption of 1) a symmetric epoch key and 2) a `VetKeyEpochId`, which both have a fixed size. For example, if using AES-GCM with a 16-byte authentication tag and a 12-byte nonce, then `EncryptedSymmetricRatchetCache` will have the ciphertext overhead of 16 + 12 = 28 bytes in terms of size and the total ciphertext size will be 28 + 32 (symmetric epoch key) + 8 (`VetKeyEpochId`) = 68 bytes. - -If the checks pass, the canister accepts the call and stores the cache, or overwrites if it exists, in the state. - -The `get_my_symmetric_key_cache` call retrieves the response of Encrypted Maps for getting their cache corresponding to the input arguments, which is the encrypted bytes if the entry exists or `null` if it does not. - -The expired caches are removed transparently to the frontend by the canister, i.e., the removal does not require explicit calls for doing that. -Expired cache is a cache, where both of the following is true: -* The cache neither has any messages associated with its vetKey epoch (see [Disappearing Messages](#disappearing-messages)) nor does it correspond to the _latest_ vetKey epoch for the chat ID. -* The vetKey epoch has not expired with regards to the state recovery limit (see below). - -Let's denote the state recovery limit as `limit`, current consensus time as `consensus_time` and next vetKey epoch creation time as `next_epoch_creation`, which equals `None` it current epoch is the latest and there is no next epoch. -To determine if a vetKey epoch has expired with regards to the state recovery limit, the following function is used. - -```rust -fn has_expired(limit: u64, consensus_time: u64, next_epoch_creation: Option) : bool { - if next_epoch.is_none() { - return false; - } else { - next_epoch.map(|next_epoch| consensus_time.saturating_sub(next_epoch) > limit) - } -} -``` - -The canister backend API for setting the state recovery limit is defined as follows: -```candid -set_state_recovery_limit : (ChatId, MessageExpiryMins) -> (variant { Ok; Err : text }); -``` -and must remove expired caches before setting a _higher_ limit. In general, it could make sense to remove expired caches on each update, since the latency of this operation is usually not critical. - -The canister backend API `get_state_recovery_limit` can be used by a frontend that has lost its state to recover the information about user's state recovery limit for a chat with ID `ChatId`. -```candid -get_state_recovery_limit : (ChatId) -> (variant { Ok : ?MessageExpiryMins; Err : text }) query; -``` - -Since cache update is usually a relatively rare operation compared to e.g. potential message updates, adding the cache metadata to the `get_my_chats_and_time` API of the backend canister seems like overkill. Instead, we could add a separate API that would return this metadata s.t. the frontend can find out which caches should be updated. -```candid -type StateRecoveryCacheMetadata = record { - vetkey_epoch_id: VetKeyEpochId; - symmetric_epoch_id : SymmetricEpochId; -}; - -get_metadata_for_my_state_recovery_caches : () -> (vec StateRecoveryCacheMetadata) query; -``` - -The garbage collection of expired caches happens when there can be no messages that need a particular ratchet state cache for decryption. -That ratchet state cache is then removed by the canister. -This can either happen during user's calls e.g. to add a new message to a chat, or by a timer job, or, actually, both in parallel to reduce the latency of cache deletion. - -As a potential further optimization, it is possible to fetch the available cached ratchet states for all chats at once to reduce the number of calls. - -In the best case, user's ratchet state cache should be synchronized with the frontend's state, i.e., whenever a ratchet state is evolved in the frontend, it should also be updated in the backend. -However, this only works if the user is online and active. -If the user is offline, the canister will delete the ratchet states that don't have any messages in canister storage that can be decrypted using those states, except for the very last state in order to be able to evolve it to a later when the user is online. - -As an idea for further improvement in cases if keeping an older ratchet state is not desirable by some users even if the users may potentially go offline for longer time frames, this can be mitigated by setting up a periodic key rotation (not defined in this spec), where a frontend will not encrypt new messages with a ratchet state from an old vetKey epoch and will instead request a vetKey rotation before the next encryption. -In that case, the users' encrypted caches associated with the older vetKey epoch will be garbage-collected automatically by the canister once they are not required for state recovery anymore. -In that case, the caches for the latest vetKey epoch that should have been rotated could also be garbage-collected. -Note though that this is normally performed by a timer job, so despite that the canister APIs will return correct results for the state recovery, the actual cache deletion from a canister might happen at a later point, e.g., during an hour or a day, depending on the configuration. - -Also, if the symmetric ratchet evolution duration is too short or the number of users in a chat is very high, updating the cache may be costly because that would involve many canister calls (one per user per chat). -To allow for mass adoption of a chat app, one could think of the following strategies to reduce costs: - -* Allow only larger time frames for the symmetric ratchet evolution, e.g., in the order of hours or days. - -* Instead of updating the caches separately, introduce a batch API for batch updates, which is performed only once per time frame for all chats. - -* Let the privacy-focused users pay for the cost of the more frequent updates in the chats where they want to have the maximally fined-grained privacy guarantees. - -### vetKey Epoch Rotation - -The vetKey epoch rotation can happen because of two different reasons: - -* Group change: this prevents a newly added user to be able to decrypt old messages or a deleted user to be able to decrypt new encrypted messages in the chat. - -* User request: this prevents an attacker that obtained a symmetric ratchet state to be able do decrypt messages that will be encrypted after the vetKey epoch rotation takes place. - -To facilitate this functionality, the canister provides two APIs: - -``` -type GroupChatId = nat64; -type ChatId = variant { - Group : GroupChatId; - Direct : record { principal; principal }; -}; -type VetKeyEpochId = nat64; -type GroupModification = record { - remove_participants : vec principal; - add_participants : vec principal; -}; -type KeyRotationResult = variant { Ok : VetKeyEpochId; Err : text }; - -// Group change in a group chat such as user addition (without access to the past chat history) or user removal. -// Takes in a batch of such changes to potentially reduce the number of required rotations. -modify_group_chat_participants : (GroupChatId, GroupModification) -> (KeyRotationResult); - -// User-initiated key rotation, e.g., periodic key rotation. -rotate_chat_vetkey : (ChatId) -> (KeyRotationResult); -``` - -Both have the same effect of rotation the vetKey epoch, but they follow different input validation rules. The rules for `modify_group_chat_participants` are discussed in [Group Changes](#group-changes). - -To validate the inputs in a `rotate_chat_vetkey` call, the canister checks that the user has access to the passed chat ID and eventually if the user is authorized to perform a key rotation (see [Group Changes](#group-changes)). - -Further validation rules can be added here and are an implementation detail. For example, the canister can rate-limit calls to `rotate_chat_vetkey` or make such calls dependent on further conditions such as user's subscription type. - -### Disappearing Messages - -The prefix of messages to be removed from a chat is defined by a non-negative integer, which identifies the size of the prefix of expired messages in the chat history, i.e., `e` expired messages means that any message ID in the chat smaller than `e` has expired. -The value of `e` is calculated from the consensus time and the messages in the chat and does not necessarily need to be stored in memory. -Expired messages cannot be [retrieved](#encrypted-message-retrieval) from the canister backend anymore and the canister backend will delete them eventually. - -The deletion algorithm is an implementation detail of the canister backend, but in general we see two options: - -* Delete expired messages in a timer job. This allows to delete messages periodically but running a timer job too often may be too expensive, so messages will be deleted with a delay. - -* Delete expired messages while sending new messages, i.e., whenever the API for sending messages is invoked, it internally calls the message deletion routine. This works well if there is a lot of activity in the chat but will leave messages undeleted or deleted after a long delay if there is no or very little activity. - -The chat allows to assign or update a disappearing messages duration for messages in a chat via the following backend canister API: - -``` -type GroupChatId = nat64; -type ChatId = variant { - Group : GroupChatId; - Direct : record { principal; principal }; -}; -type MessageExpiryMins = nat64; - -set_message_expiry : (ChatId, MessageExpiryMins) -> (variant { Ok; Err : text }); -``` - -Upon receival of a `set_message_expiry` call, the backend canister checks that the user has access to `ChatId` and eventually that the user is authorized to call `set_message_expiry` for `ChatId` and sets `MessageExpiryMins` in the state for `ChatId`. - -The semantics of `MessageExpiryMins` is as follows: - -* If `MessageExpiryMins` is equal to zero, then this means no expiry is set and all messages are always returned. - -* Otherwise the value of `MessageExpiryMins` is used as the message expiry. - -Every chat is [created](#chat-creation) with the expiry of 0, which holds a special meaning that messages do not expire. -Once the state is updated, it affects the behavior of [the APIs returning the metadata about message IDs](#exposing-metadata-about-chats-and-new-messages) and [the APIs returning the actual messages](#encrypted-message-retrieval). - -* `get_messages : (ChatId, ChatMessageId, opt Limit) -> (vec EncryptedMessage) query;` does not add expired messages to the output. - -## User Frontend Component - -### Frontend State - -* Chat metadata for each chat. - - * Chat ID. - - * Number of received and decrypted messages so far. - - * Latest vetKey epoch ID. - - * VetKey epoch metadata for all vetKey epochs that were required for encryption or decryption. Note that the metadata of the vetKey epochs that are not the last vetKey epoch and whose messages have expired are removed from the state. - - * Message expiry for each decrypted message. - -* Symmetric ratchet states for all vetKey epochs that were required for encryption or decryption. Similarly to vetKey epoch metadata, expired symmetric ratchet states are deleted from the state. - -* Decrypted non-expired messages for each chat. - -* Latest consensus time. - -* Optional optimization: All messages and chats stored in browser storage. - -### Chat UI - -The chat UI is a UI that displays chats and their decrypted messages, and allows the user to send their own messages and change settings such as group membership and message expiration via UI elements. -It may also display the metadata about the encrypted chat such as the current epoch information if that fits the application. - -The chat UI calls a few canister backend APIs directly, normally via dedicated UI buttons: [chat creation](#chat-creation), [vetKey rotation](#vetkey-epoch-rotation), [group changes](#group-changes), [updating the message expiry](#disappearing-messages). - -Since the chat UI is mostly an implementation detail, only its interaction with [Encrypted Messaging Service (EMS)](#encrypted-messaging-service) is described here, whose purpose is it to take care of encryption and decryption of messages. - -The chat UI uses the EMS in a black-box way to: - -* Dispatch user messages to be encrypted and sent via the `enqueueSendMessage` API of the EMS. - -* Fetch received and decrypted messages via periodically querying the `takeReceivedMessages` API of the EMS. - -* Find out which chats are accessible at the moment via the `getCurrentChatIds` API of the EMS. New chats need to be added to the UI and chats that the user has lost access to need to be removed from the UI. - -### Encrypted Messaging Service - -Encrypted Messaging Service (EMS) is a component that gives the developer a transparent way to interact with the encrypted chat by reading from a stream of received and decrypted messages and putting user messages to be encrypted and sent into a stream that the EMS will take care of encrypting and sending. - -Types: - -* `type ChatId = { 'Group' : bigint } | { 'Direct' : [Principal, Principal] };` - -* `type ChatIdAsString = string` - -* `interface Message { - nonce: bigint; - chatId: ChatId; - senderId: Principal; - content: string; - timestamp: Date; - vetkeyEpoch: bigint; - symmetricRatchetEpoch: bigint; - } - ` - -The EMS exposes the following APIs: - -* `enqueueSendMessage(chatId: ChatId, content: Uint8Array)`: adds the message `content` to be encrypted for and sent to the chat with ID `chatId`. This API does not give any guarantees that the message will actually be added to the chat but it makes attempts to recover from recoverable errors (see [Encrypting and Sending Messages in the EMS](#encrypting-and-sending-messages)). - -* `takeReceivedMessages(): Map`: returns latest chat messages that were received and decrypted by the EMS and were not yet taken by the user from the EMS (see [Fetching and Decrypting Messages in the EMS](#fetching-and-decrypting-messages)). - -* `start()`: starts the EMS service. Before the service is started, calling any other APIs should throw an error. Once it is started, the APIs start to return their intended values, and the EMS starts background tasks to continuously update the relevant chat information from the canister. - -* `skipMessagesAvailableLocally(chatId: ChatId, lastKnownChatMessageId: bigint)`: tells the EMS what chat message ID should be the first one to be fetched. This is relevant if some of the messages are available from another source such as [browser storage](#local-cache-in-indexeddb). - -* `getCurrentChatIds(): ChatId[]`: returns the chat IDs that are currently accessible to the user. This particular API is mostly an efficiency optimization, since the message retrieval in the EMS anyways requires fetching the information about the currently accessible chats. - -* `getCurrentUsersInChat(chatId: ChatId): Principal[]`: takes in a chat ID and returns the current users in the chat. If the EMS does not have data about the chat with `chatId` because either the user does not have access to the chat, or if `chatId` does not exist, or the frontend did not yet manage to synchronize with the backend, this function throws an error. - -#### Encrypting and Sending Messages - -For message [encryption](#ratchet-message-encryption) and [sending](#incoming-message-validation), the EMS makes use of the [`send_message`](#incoming-message-validation) backend canister API. - -The EMS periodically takes a message from the sending stream that was added via the `enqueueSendMessage` API. If the stream is empty, the EMS retries with a timeout. If the stream is non-empty, the EMS takes the oldest message from the stream and performs the following steps: - -1. If there is no symmetric ratchet state for the latest vetKey epoch of the chat, the EMS [initializes](#ratchet-initialization) it and calls `get_vetkey_epoch_metadata` to obtain its metadata, which the frontend stores in its state. - -2. The EMS encrypts the message using the symmetric ratchet state that corresponds to the latest known vetKey epoch ID (see [Symmetric Ratchet](#symmetric-ratchet)) for the chat at the current time. It may happen that the symmetric ratchet epoch of the symmetric ratchet state is smaller than needed for the encryption at the current time. In that case, the state is copied to a temporary state and the temporary state is evolved to encrypt the message. After encryption, the temporary state may be deleted. Note that neither encryption nor decryption evolves the symmetric ratchet state and instead the ratchet evolution is performed in a background task (see [Ratchet Evolution](#ratchet-evolution)). - -3. The EMS [sends](#incoming-message-validation) the encrypted message via the `send_message` canister backend API (see [Incoming Message Validation](#incoming-message-validation)). - -4. If the canister returns the `WrongSymmetricKeyEpoch` variant of `MessageSendingError`, then the EMS was unlucky and the sent message arrived at a wrong (normally the next) symmetric key epoch. In this case the EMS goes to step 2. - -5. If the canister returns the `WrongVetKeyEpoch` variant of `MessageSendingError`, then either the [manual vetKey epoch rotation](#vetkey-epoch-rotation) or a [group change](#group-changes) took place. In this case, the EMS makes a query call to `get_latest_vetkey_epoch` and updates the latest vetKey epoch ID of the chat in the state, and then goes to step 1. - -To avoid infinite loops in case of too strict parameters, bad network connectivity, etc., the maximum number of retries should be capped. - -> [!TIP] -> A useful feature of chat applications is displaying when a user joined or left the chat directly in the chat history. The current spec only makes use of such information in the vetKey epoch metadata, which returns the full list of participants for each vetKey epoch, which is a bit redundant for the purpose. More succinct data can be exposed by providing an additional backend API that returns a vector of `GroupModification`s (see [Group Changes](#group-changes)). - -#### Fetching and Decrypting Messages - -For message [retrieval](#encrypted-message-retrieval) and [decryption](#ratchet-message-decryption), the EMS makes use of the following backend canister APIs: - -* [`get_my_chats_and_time`](#exposing-metadata-about-chats-and-new-messages) to retrieve the chat IDs and the number of messages to retrieve. - -* [`get_messages`](#encrypted-message-retrieval) to retrieve the encrypted messages for the chats accessible to the user. - -* Further canister APIs required for [Ratchet Initialization](#ratchet-initialization). - -The frontend stores the following related data related in its state: - -* The chat IDs accessible to the user. - -* The first accessible message ID for the user for each chat - -* The last fetched message for the chat. - -* The total number of messages in the chat. - -Let's call this information frontend chat metadata. - -Periodically, the EMS queries the `get_my_chats_and_time` backend canister API. -Its result is compared to the frontend chat metadata in the state. -The existing chat metadata is updated if required. -If there is a new chat in the result that is not yet in the state, the EMS adds it to the state along with the information that no messages were obtained for this chat yet. -If one of the chats in the state does not appear in the result of `get_my_chats_and_time` anymore, then this chat is deleted from the state including from the queues containing received and decrypted messages. - -Also periodically, two separate routines run. - -1. Check if there are new messages to be fetched from the canister: if the largest received message ID for the chat plus one is smaller than the total number of messages in the chat. If it is, then `get_messages` is invoked with the first message ID to be fetched that is equal to the largest received message ID for the chat plus one, or if no messages were received so far for a chat, message ID 0 is used. If an error occurs due to too large messages that don't fit into the canister's response due to the query response limits on the ICP, the query to `get_messages` is retried with a limit of e.g. one. A successful result appended to the received messages queue. - -2. Try to take a message from the received messages queue and decrypt it. - - a. The EMS checks if it already has the symmetric ratchet state in its state that is required to decrypt the message, whose metadata specifies the required vetKey epoch and symmetric ratchet epoch. If the symmetric ratchet state is not yet initialized, the EMS [initializes](#ratchet-initialization) it. An error to do so is unrecoverable. - - b. The EMS [decrypts](#ratchet-message-decryption) the message using the symmetric ratchet state and the vetKey epoch ID stored in the message metadata. A successfully decrypted message is put into the decrypted message queue that is exposed to the chat UI component via the [`takeReceivedMessages` API](#encrypted-messaging-service) of the EMS. If the decryption returns an error, such an error is unrecoverable and instead of a decrypted message, a message of special form is put into the decrypted message queue that indicated that this message could not be decrypted. Note that user-side errors cannot be avoided, since the canister cannot check if the encryption is valid. - -#### Symmetric Ratchet - -The backend canister APIs required for ratchet initialization are described in the [Providing vetKeys for Symmetric Ratchet Initialization](#providing-vetkeys-for-symmetric-ratchet-initialization) section. - -A symmetric ratchet state consists of: - -* Epoch key that is used to: - - 1. Derive the next ratchet state. - - 2. Derive chat participants' message encryption keys. - -* Symmetric ratchet epoch ID that the key corresponds to, which is a non-negative number. - -##### Ratchet Initialization - -The ratchet state is initialized from a vetKey as follows: - -1. Obtain the vetKey for the vetKey epoch of the chat - - a. [Generate](https://dfinity.github.io/vetkeys/classes/_dfinity_vetkeys.TransportSecretKey.html#random) a transport key pair. - - b. [Fetch](#providing-vetkeys-for-symmetric-ratchet-initialization) the encrypted vetKey for the chat and vetKey epoch from the backend canister. - - c. Compute the verification public key either [locally](https://dfinity.github.io/vetkeys/classes/_dfinity_vetkeys.MasterPublicKey.html) or via querying the `chat_public_key` backend canister API. - - d. [Decrypt and verify](https://dfinity.github.io/vetkeys/classes/_dfinity_vetkeys.EncryptedVetKey.html#decryptandverify) the vetKey. - -2. Compute and save the ratchet state - - a. Compute [`let rootKey = deriveSymmetricKey(vetKeyBytes, DOMAIN_RATCHET_INIT, 32)`](https://github.com/dfinity/vetkeys/blob/83b887f220a2c1c40713a3512ce5a9994d5ec4c6/frontend/ic_vetkeys/src/utils/utils.ts#L352), where `DOMAIN_RATCHET_INIT` is a unique domain separator for ratchet initialization (TODO: this function is currently internal - we should make it public). - - b. Initialize the symmetric ratchet state as `rootKey` and symmetric ratchet epoch that is equal to zero. - -More details about the retrieval and decryption of vetKeys can be found in the [developer docs](https://internetcomputer.org/docs/building-apps/network-features/vetkeys/api) of the ICP. - -After initializing the ratchet state, the user uploads encrypted cache of the state to the backend canister which is further described in [Encrypted Ratchet State Cache](#encrypted-symmetric-ratchet-state-cache). - -##### Ratchet Evolution - -```ts -import { deriveSymmetricKey } from '@dfinity/vetkeys'; - -type RawSymmetricRatchetState = { epochKey: Uint8Array, epochId: bigint }; - -function ratchetStepDomainSeparator(epoch: bigInt) { - return new Uint8Array([ - ...DOMAIN_RATCHET_STEP, - ...uBigIntTo8ByteUint8ArrayBigEndian(symmetricRatchetState.epochId) - ]); -} - -function evolve(symmetricRatchetState: RawSymmetricRatchetState) : RawSymmetricRatchetState { - const nextEpoch = symmetricRatchetState.epochId + 1n; - const domainSeparator = ratchetStepDomainSeparator(nextEpoch); - const newEpochkey = deriveSymmetricKey(symmetricRatchetState.epochKey, domainSeparator, 32); - - return { epochKey: newEpochkey, epochId: nextEpoch }; -} -``` -where `DOMAIN_RATCHET_STEP` is a unique domain separator. - -Alternatively, this can be implemented using Web Crypto API to make the current key non-extractable. Note though that Web Crypto API's `deriveKey` cannot derive an HKDF key and, therefore, derivation of the next epoch key is a two-step process: -1. Derive the next epoch key in form of a byte vector via `deriveBits`. -2. Import the derived byte vector as `CryptoKey`. - -```ts -type SymmetricRatchetState = { epochKey: CryptoKey, epochId: bigint }; - -async function deriveNextSymmetricRatchetEpochCryptoKey(symmetricRatchetState: RawSymmetricRatchetState) : Promise { - const exportable = false; - const domainSeparator = ratchetStepDomainSeparator(symmetricRatchetState.epochKey) - const algorithm = { - name: 'HKDF', - hash: 'SHA-256', - length: 32 * 8, - info: domainSeparator, - salt: new Uint8Array() - }; - - const rawKey = await globalThis.crypto.subtle.deriveBits(algorithm, epochKey, 8 * 32); - - return await globalThis.crypto.subtle.importKey('raw', rawKey, algorithm, exportable, [ - 'deriveKey', - 'deriveBits' - ]); -} -``` - -It would be quite natural and similar to [Signal's symmetric ratchet](https://signal.org/docs/specifications/doubleratchet/) if the decryption would trigger the ratchet evolution. However, that would force the frontend to decrypt all messages belonging to one vetKey epoch in cases where a chat has many messages and we only want to display the latest ones. This incurs a big and unnecessary overhead in terms of both communication and computation. - -Therefore, neither encryption nor decryption directly evolve `SymmetricRatchetState` but instead, the state is evolved whenever the current consensus time obtained via `get_my_chats_and_time` minus the message expiry is larger than the timestamp of the current symmetric key epoch id + 1, i.e., whenever there can be no non-expired message that we would need the current symmetric ratchet epoch to decrypt. The state evolution can be triggered by a background job that periodically checks if state evolution should be performed. - -After evolving the ratchet state, the user uploads encrypted updated state cache to the canister backend which is further described in [Encrypted Ratchet State Cache](#encrypted-symmetric-ratchet-state-cache). - -##### Ratchet Message Encryption -```ts -import { DerivedKeyMaterial } from '@dfinity/vetkeys'; - -async encrypt( - epochKey: CryptoKey, - sender: Principal, - nonce: Uint8Array, - message: Uint8Array -): Promise { - const domainSeparator = messageEncryptionDomainSeparator(sender, nonce); - const derivedKeyMaterial = DerivedKeyMaterial.fromCryptoKey(epochKey); - return await derivedKeyMaterial.encryptMessage(message, domainSeparator); -} -``` -where [`messageEncryptionDomainSeparator`](#typescript-domain-separators) is a unique [size-prefixed](#typescript-size-prefix) domain separator and the `nonce` argument is a user-assigned nonce associated with the message that must be unique for `epochKey` (i.e., the symmetric ratchet epoch) und MUST NOT be reused. - -##### Ratchet Message Decryption - -```ts - -import { DerivedKeyMaterial } from '@dfinity/vetkeys'; - -async decrypt( - epochKey: CryptoKey, - sender: Principal, - nonce: Uint8Array, - encryptedMessage: Uint8Array -): Promise { - const domainSeparator = messageEncryptionDomainSeparator(sender, nonce); - const derivedKeyMaterial = DerivedKeyMaterial.fromCryptoKey(epochKey); - return await derivedKeyMaterial.decryptMessage(encryptedMessage, domainSeparator); -} -``` -where [`messageEncryptionDomainSeparator`](#typescript-domain-separators) is a unique [size-prefixed](#typescript-size-prefix) domain separator. - -## Ensuring Correctness of Query Calls - -TODO - -Idea 1: use query calls and start work, while in the background an update call is invoked to compare the result. - -Idea 2: use certified variables. - -Comment by Andrea regarding Idea 2: -In a multi-user canister scenario then it may be difficult to add certificates for this endpoint. However, for individual user/chat canisters, then it should be easy to provide certificates, e.g. of chat ID, time and latest message ID. - -Is something like mixed hash tree possible in a single canister scenario to reduce hashing times if hashing a huge state? Probably not extremely important, but may be useful to discuss. - -## Optimizations - -Here, we discuss potential further optimizations that are not an essential part of the spec. - -### Local Cache in indexedDB - -The local cache in indexedDB is essentially a copy of the frontend state stored in a persistent storage. - -* Keys - whenever a new ratchet state is created or evolved, it is stored in indexedDB at an index containing its chat ID and the vetKey epoch ID. Whenever a ratchet state is deleted locally, it is also deleted from indexedDB. - -* Messages - in a similar fashion to keys, the decrypted messages are also added to and removed from indexedDB to keep it in sync with the frontend. - -An important difference between keys and messages in indexedDB is the component that is responsible for caching. -For keys, the [EMS](#encrypted-messaging-service) is responsible. -Namely, whenever a ratchet state is required, the EMS first checks if it is available from IndexedDB. -For messages, the UI is responsible. -More specifically, upon initialization of the app, it first loads all messages available in indexedDB into the local state and updates the count of the messages available locally via the EMS API s.t. they are not loaded and decrypted again from the canister. - -### IBE-Encrypted vetKey Resharing - -To reduce the number of vetKeys required for group changes and periodic key rotations, IBE with long-term keys can be used: - -1. Each user fetches a long-term IBE key for their principal. - -2. Whenever a user fetches a vetKey for a chat, the user decrypts it and reshares with all other users by encrypting the vetKey with their public IBE keys, resulting in a vector containing a separate encryption for each user. Then, the user sends this vector to a special canister API (not described further in this spec). - -3. Upon receival of a call to that API, the canister checks which users already have either an existing reshared vetKey or have a cached ratchet state. Such reshared vetKeys are filtered out and the rest is added to the canister state. - -Then, the user frontend would check if there is a reshared vetKey stored for them in the canister state before trying to obtain the vetKey in the normal way. -If that is the case, the user would fetch and IBE-decrypt it, and then verify that the vetKey is valid (recall that the vetKey is a BLS signature that can efficiently be verified). -If the check fails, the reshared vetKey is ignored and the frontend proceeds as if there was no resharing, i.e., it obtains the vetKey via the normal API. - -Note that this adds the overhead of resharing keys with potentially many users in the chat which incurs additional runtime overhead. -However, this can be done in the background and doesn't have to block the app. -For that, most of the more costly vetKey derivation calls by the canister can be replaced by cheaper calls to store small encrypted vetKeys. -Also, this optimization works well only if most of the users provide valid reshared vetKeys, but, on the other hand, the penalty to the honest users (some additional latency due to the higher number of steps in the vetKey retrieval logic) is rather small. - -An additional step useful to save a small amount of storage in the resharing routine is to remove the reshared vetKey after an encrypted cache has been added for a user to the canister state. - -### Allowing New Users to See Old Message History - -It is not necessarily always the case that a user added to a chat should not see the previous history, which is the default setting in this spec. -This not only allows specific users to see the chat history but also reduces the number of calls to derive vetKeys. - -In the chat UI, this functionality can be realized e.g. via a flag set in the chat UI while adding a new user. - -The current spec does not allow to integrate this optimization in a completely non-invasive way, since the chat participants of a vetKey epoch cannot be changed. -To facilitate this, the list of chat participants could have versioning, where for each version the list change would be stored in the canister and `get_my_chats_and_time` would return no only the last message in the chat but also all vetKey epoch IDs for each chat and their participant list versions, or only for those where actual changes happened. - -### State Recovery - -The frontend's role in enabling state recovery (see also [Encrypted Ratchet State Cache](#encrypted-symmetric-ratchet-state-cache) in the backend) is twofold: -* Initialization of caches. -* Updates of caches. - -Independent of how the local ratchet state was initialized, once initialized the frontend checks if the ratchet state cache in the canister needs to be updated. If no cache state exists in the canister or it is outdated, the frontend encrypts and uploads the state via the `update_my_symmetric_key_cache` backend canister API. - -In addition to cache updates upon initialization, the frontend should periodically check that the cache is up-to-date. How often this is done depends on the symmetric ratchet duration window. -However, note that only the client that is online can update their cache, which normally doesn't work perfectly practice, e.g., if we rotate the symmetric ratchet state every hour, there will be very few clients that will be online for actually performing cache updates every hour. -Also, cache updates cost cycles in the ICP, and hence excessive use undesirably costs additional money. -Therefore, cache updates every (half a) day, and therefore the symmetric ratchet duration of a similar length, seem to be most practical in the general use case. -If another use case requires more strict parameters, they can be set appropriately to enable that use case. - -Encrypted maps handles the encryption of the map values transparently to the developer, i.e., the developer does not need to know how encryption exactly works in encrypted maps and can use encrypted maps as a black box. -For the purpose of using encrypted maps for the encryption of user's cached keys, the encrypted maps object is instantiated with the domain separator `"vetkeys-example-encrypted-chat-user-cache"` in the backend. -Then, to handle the cache, the frontend relies on the following: - -* Store to encrypted maps - when the frontend obtained a new ratchet state or wants to update a ratchet state cache, the frontend calls. - -* Load from encrypted maps - when the frontend requires to recover its previous ratchet states, it retrieves the encrypted cache and decrypts it locally. - -```ts - import { EncryptedMaps } from '@dfinity/vetkeys/encrypted_maps'; - - function store(encryptedMaps: EncryptedMaps, myPrincipal: Principal, chatId: ChatId, vetKeyEpochId: bigint, cache: Uint8Array) { - const epochBytes = uBigIntTo8ByteUint8ArrayBigEndian(vetKeyEpochId); - const chatIdBytes = chatIdToBytes(chatId); - const mapKey = new Uint8Array([...chatIdBytes, ...epochBytes]); - encryptedMaps.setValue(myPrincipal, mapName(), mapKey, cache); - } - - function load(encryptedMaps: EncryptedMaps, myPrincipal: Principal, chatId: ChatId, vetKeyEpochId: bigint) : Uint8Array { - const epochBytes = uBigIntTo8ByteUint8ArrayBigEndian(vetKeyEpochId); - const chatIdBytes = chatIdToBytes(chatId); - const mapKey = new Uint8Array([...chatIdBytes, ...epochBytes]); - return encryptedMaps.getValue(myPrincipal, mapName(), mapKey); - } - - function chatIdToBytes(chatId: ChatId) : Uint8Array { - if ('Direct' in chatId) { - return new Uint8Array([ - 0, - ...chatId.Direct[0].toUint8Array(), - ...chatId.Direct[1].toUint8Array() - ]); - } else { - return new Uint8Array([1, ...uBigIntTo8ByteUint8ArrayBigEndian(chatId.Group)]); - } - } - - function uBigIntTo8ByteUint8ArrayBigEndian(value: bigint): Uint8Array { - if (value < 0n) throw new RangeError('Accepts only bigint n >= 0'); - - const bytes = new Uint8Array(8); - for (let i = 0; i < 8; i++) { - bytes[i] = Number((value >> BigInt(i * 8)) & 0xffn); - } - return bytes; - } - - function mapName(): Uint8Array { - return new TextEncoder().encode('encrypted_chat_cache'); - } -``` - -## Appendix - -### Constructing `ChatId` - -``` -type GroupChatId = nat64; - -type ChatId = variant { - Group : GroupChatId; - Direct : record { principal; principal }; -}; -``` - -**Direct**: The `ChatId` type is constructed from a pair of _sorted_ principals. It is valid if the principals are equal and corresponds to a user's private chat (similar to e.g. Signal's "Note to Self" chat). - -**Group**: The `ChatId` type is constructed from a _unique_ group chat ID, which is defined by an unsigned 64-bit number. The backend is responsible of issuing those and ensuring their uniqueness. - -### Calculating Current Symmetric Ratchet Epoch ID - -```rust -fn current_symmetric_ratchet_epoch( - vetkey_epoch_creation_time_nanos: u64, - symmetric_ratchet_rotation_duration_nanos: u64 -) { - let now = ic_cdk::api::time(); - let elapsed = vetkey_epoch_creation_time_nanos - now; - return elapsed / symmetric_ratchet_rotation_duration_nanos; -} -``` - -### TypeScript Conversion of Unsigned BigInt to Uint8Array - -```ts -function uBigIntTo8ByteUint8ArrayBigEndian(value: bigint): Uint8Array { - if (value < 0n) throw new RangeError('Accepts only bigint n >= 0'); - if ((value >> 128) > 0n) throw new RangeError('Accepts only bigint fitting into an 8-byte array'); - - const bytes = new Uint8Array(8); - for (let i = 0; i < 8; i++) { - bytes[i] = Number((value >> BigInt(i * 8)) & 0xffn); - } - return bytes; -} -``` - -### TypeScript CryptoKey Import -```ts -let keyBytes = /* */; -let exportable = false; -await globalThis.crypto.subtle.importKey( - 'raw', - keyBytes, - 'HKDF', - exportable, - ['deriveKey', 'deriveBits'] - ); -``` - -### TypeScript Size Prefix - -```ts -export function sizePrefixedBytesFromString(text: string): Uint8Array { - const bytes = new TextEncoder().encode(text); - if (bytes.length > 255) { - throw new Error('Text is too long'); - } - const size = new Uint8Array(1); - size[0] = bytes.length & 0xff; - return new Uint8Array([...size, ...bytes]); -} -``` - -### TypeScript Domain Separators - -```ts - -// Example definition of the domain separators -const DOMAIN_RATCHET_INIT = sizePrefixedBytesFromString('ic-vetkeys-chat-ratchet-init'); -const DOMAIN_RATCHET_STEP = sizePrefixedBytesFromString('ic-vetkeys-chat-ratchet-step'); -const DOMAIN_MESSAGE_ENCRYPTION = sizePrefixedBytesFromString( - 'ic-vetkeys-chat-message-encryption' -); - -export function messageEncryptionDomainSeparator( - sender: Principal, - nonce: Uint8Array -): Uint8Array { - if (nonce.length !== 16) { throw RangeError("Expected nonce of size 16 but got " + nonce.length); } - return new Uint8Array([ - ...DOMAIN_MESSAGE_ENCRYPTION, - ...sender.toUint8Array(), - ...uBigIntTo8ByteUint8ArrayBigEndian(nonce) - ]); -} - -export function ratchetStepDomainSeparator(currentSymmetricKeyEpoch: bigint){ - new Uint8Array([ - ...DOMAIN_RATCHET_STEP, - ...uBigIntTo8ByteUint8ArrayBigEndian(currentSymmetricKeyEpoch) - ]); -} -``` - -### Candid Interface of the Backend - -```candid -type GroupChatId = nat64; -type ChatId = variant { - Group : GroupChatId; - Direct : record { principal; principal }; -}; -type VetKeyEpochId = nat64; -type SymmetricKeyEpochId = nat64; -type ChatMessageId = nat64; -type Nonce = blob; -type Limit = nat32; -type TimeNanos = nat64; -type NumberOfMessages = nat64; -type SymmetricKeyRotationMins = nat64; -type MessageExpiryMins = nat64; - -type KeyRotationResult = variant { Ok : VetKeyEpochId; Err : text }; -type EncryptedMessage = record { - content : EncryptedBytes; - metadata : EncryptedMessageMetadata; -}; - -// vetKeys -type PublicTransportKey = blob; -type DerivedVetKeyPublicKey = blob; -type EncryptedVetKey = blob; -type IbeEncryptedVetKey = blob; -type EncryptedSymmetricRatchetCache = blob; - -type EncryptedMessageMetadata = record { - vetkey_epoch : VetKeyEpochId; - sender : principal; - symmetric_key_epoch_id : SymmetricKeyEpochId; - chat_message_id : ChatMessageId; - timestamp : TimeNanos; - nonce : Nonce; -}; -type GroupChatMetadata = record { creation_timestamp : TimeNanos; chat_id : GroupChatId }; -type GroupModification = record { - remove_participants : vec principal; - add_participants : vec principal; -}; -type EncryptedBytes = blob; -type UserMessage = record { - vetkey_epoch_id : VetKeyEpochId; - content : EncryptedBytes; - symmetric_key_epoch_id : SymmetricKeyEpochId; - nonce : Nonce; -}; -type NumberOfMessages = nat64; -type Receiver = principal; -type OtherParticipant = principal; -type VetKeyEpochMetadata = record { - symmetric_key_rotation_duration : SymmetricKeyRotationMins; - participants : vec principal; - messages_start_with_id : ChatMessageId; - creation_timestamp : TimeNanos; - epoch_id : VetKeyEpochId; -}; -type KeyRotationResult = variant { Ok : VetKeyEpochId; Err : text }; -type MessageSendingError = variant { WrongVetKeyEpoch; WrongSymmetricKeyEpoch; Custom: text }; -type ChatMetadata = record { - chat_id : ChatId; - number_of_messages : NumberOfMessages; - vetkey_epoch_id : VetKeyEpochId; - symmetric_epoch_id : SymmetricEpochId; -}; -type StateRecoveryCacheMetadata = record { - vetkey_epoch_id: VetKeyEpochId; - symmetric_epoch_id : SymmetricEpochId; -}; - -service : (text) -> { - chat_public_key : (ChatId, VetKeyEpochId) -> (DerivedVetKeyPublicKey); - create_direct_chat : (OtherParticipant, SymmetricKeyRotationMins) -> variant { Ok : TimeNanos; Err : text }; - create_group_chat : (vec OtherParticipant, SymmetricKeyRotationMins) -> (variant { Ok : GroupChatMetadata; Err : text }); - derive_chat_vetkey : (ChatId, VetKeyEpochId, PublicTransportKey) -> (variant { Ok : EncryptedVetKey; Err : text }); - get_vetkey_for_my_cache_encryption : (PublicTransportKey) -> (EncryptedVetKey); - get_latest_chat_vetkey_epoch_metadata : (ChatId) -> (variant { Ok : VetKeyEpochMetadata; Err : text }) query; - get_my_chats_and_time : () -> (vec ChatMetadata) query; - get_my_reshared_ibe_encrypted_vetkey : (ChatId, VetKeyEpochId) -> (variant { Ok : opt IbeEncryptedVetKey; Err : text }); - get_my_symmetric_key_cache : (ChatId, VetKeyEpochId) -> (variant { Ok : opt EncryptedSymmetricRatchetCache; Err : text }); - // Returns messages for a chat starting from a given message id. - get_messages : (ChatId, ChatMessageId, opt Limit) -> ( - vec EncryptedMessage, - ) query; - get_metadata_for_my_state_recovery_caches : () -> (vec StateRecoveryCacheMetadata) query; - get_vetkey_epoch_metadata : (ChatId, VetKeyEpochId) -> (variant { Ok : VetKeyEpochMetadata; Err : text }) query; - get_vetkey_resharing_ibe_decryption_key : (PublicTransportKey) -> (EncryptedVetKey); - get_vetkey_resharing_ibe_encryption_key : (Receiver) -> (DerivedVetKeyPublicKey); - modify_group_chat_participants : (ChatGroupId, GroupModification) -> (KeyRotationResult); - reshare_ibe_encrypted_vetkeys : ( - ChatId, - VetKeyEpochId, - vec record { Receiver; IbeEncryptedVetKey }, - ) -> (variant { Ok; Err : text }); - rotate_chat_vetkey : (ChatId) -> (KeyRotationResult); - send_message : (ChatId, UserMessage) -> (variant { Ok; Err : MessageSendingError }); - set_message_expiry : (ChatId, MessageExpiryMins) -> (variant { Ok; Err : text }); - set_state_recovery_limit : (ChatId, MessageExpiryMins) -> (variant { Ok; Err : text }); - get_state_recovery_limit : (ChatId) -> (variant { Ok : ?MessageExpiryMins; Err : text }) query; - update_my_symmetric_key_cache : (ChatId, VetKeyEpochId, EncryptedSymmetricRatchetCache) -> (variant { Ok; Err : text }); -} -``` diff --git a/rust/vetkeys/encrypted_chat/frontend/.gitignore b/rust/vetkeys/encrypted_chat/frontend/.gitignore deleted file mode 100644 index bff793d5e..000000000 --- a/rust/vetkeys/encrypted_chat/frontend/.gitignore +++ /dev/null @@ -1,24 +0,0 @@ -test-results -node_modules - -# Output -.output -.vercel -.netlify -.wrangler -/.svelte-kit -/build - -# OS -.DS_Store -Thumbs.db - -# Env -.env -.env.* -!.env.example -!.env.test - -# Vite -vite.config.js.timestamp-* -vite.config.ts.timestamp-* diff --git a/rust/vetkeys/encrypted_chat/frontend/.npmrc b/rust/vetkeys/encrypted_chat/frontend/.npmrc deleted file mode 100644 index b6f27f135..000000000 --- a/rust/vetkeys/encrypted_chat/frontend/.npmrc +++ /dev/null @@ -1 +0,0 @@ -engine-strict=true diff --git a/rust/vetkeys/encrypted_chat/frontend/.prettierignore b/rust/vetkeys/encrypted_chat/frontend/.prettierignore deleted file mode 100644 index 7d74fe246..000000000 --- a/rust/vetkeys/encrypted_chat/frontend/.prettierignore +++ /dev/null @@ -1,9 +0,0 @@ -# Package Managers -package-lock.json -pnpm-lock.yaml -yarn.lock -bun.lock -bun.lockb - -# Miscellaneous -/static/ diff --git a/rust/vetkeys/encrypted_chat/frontend/.prettierrc b/rust/vetkeys/encrypted_chat/frontend/.prettierrc deleted file mode 100644 index 8103a0b5d..000000000 --- a/rust/vetkeys/encrypted_chat/frontend/.prettierrc +++ /dev/null @@ -1,16 +0,0 @@ -{ - "useTabs": true, - "singleQuote": true, - "trailingComma": "none", - "printWidth": 100, - "plugins": ["prettier-plugin-svelte", "prettier-plugin-tailwindcss"], - "overrides": [ - { - "files": "*.svelte", - "options": { - "parser": "svelte" - } - } - ], - "tailwindStylesheet": "./src/app.css" -} diff --git a/rust/vetkeys/encrypted_chat/frontend/README.md b/rust/vetkeys/encrypted_chat/frontend/README.md deleted file mode 100644 index 75842c404..000000000 --- a/rust/vetkeys/encrypted_chat/frontend/README.md +++ /dev/null @@ -1,38 +0,0 @@ -# sv - -Everything you need to build a Svelte project, powered by [`sv`](https://github.com/sveltejs/cli). - -## Creating a project - -If you're seeing this, you've probably already done this step. Congrats! - -```sh -# create a new project in the current directory -npx sv create - -# create a new project in my-app -npx sv create my-app -``` - -## Developing - -Once you've created a project and installed dependencies with `npm install` (or `pnpm install` or `yarn`), start a development server: - -```sh -npm run dev - -# or start the server and open the app in a new browser tab -npm run dev -- --open -``` - -## Building - -To create a production version of your app: - -```sh -npm run build -``` - -You can preview the production build with `npm run preview`. - -> To deploy your app, you may need to install an [adapter](https://svelte.dev/docs/kit/adapters) for your target environment. diff --git a/rust/vetkeys/encrypted_chat/frontend/e2e/demo.test.ts b/rust/vetkeys/encrypted_chat/frontend/e2e/demo.test.ts deleted file mode 100644 index 9985ce113..000000000 --- a/rust/vetkeys/encrypted_chat/frontend/e2e/demo.test.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { expect, test } from '@playwright/test'; - -test('home page has expected h1', async ({ page }) => { - await page.goto('/'); - await expect(page.locator('h1')).toBeVisible(); -}); diff --git a/rust/vetkeys/encrypted_chat/frontend/eslint.config.js b/rust/vetkeys/encrypted_chat/frontend/eslint.config.js deleted file mode 100644 index 52621ab2c..000000000 --- a/rust/vetkeys/encrypted_chat/frontend/eslint.config.js +++ /dev/null @@ -1,64 +0,0 @@ -import prettier from 'eslint-config-prettier'; -import { includeIgnoreFile } from '@eslint/compat'; -import js from '@eslint/js'; -import svelte from 'eslint-plugin-svelte'; -import globals from 'globals'; -import { fileURLToPath } from 'node:url'; -import ts from 'typescript-eslint'; -import svelteConfig from './svelte.config.js'; -import path from 'node:path'; - -const gitignorePath = fileURLToPath(new URL('./.gitignore', import.meta.url)); - -export default ts.config( - includeIgnoreFile(gitignorePath), - js.configs.recommended, - ...ts.configs.recommendedTypeChecked, - ...svelte.configs.recommended, - prettier, - ...svelte.configs.prettier, - { - languageOptions: { - globals: { ...globals.browser, ...globals.node } - }, - rules: { - // typescript-eslint strongly recommend that you do not use the no-undef lint rule on TypeScript projects. - // see: https://typescript-eslint.io/troubleshooting/faqs/eslint/#i-get-errors-from-the-no-undef-rule-about-global-variables-not-being-defined-even-though-there-are-no-typescript-errors - 'no-undef': 'off' - } - }, - { - files: ['**/*.ts'], - languageOptions: { - parser: ts.parser, - parserOptions: { - projectService: true, - tsconfigRootDir: import.meta.dirname - } - } - }, - { - files: ['**/*.svelte', '**/*.svelte.ts', '**/*.svelte.js'], - languageOptions: { - parserOptions: { - extraFileExtensions: ['.svelte'], - parser: ts.parser, - svelteConfig, - projectService: true, - tsconfigRootDir: import.meta.dirname - } - } - }, - { - ignores: [ - 'dist/', - 'src/declarations', - '*.config.js', - '*.config.ts', - '*.config.cjs', - '*.config.mjs', - '.svelte-kit', - 'e2e/' - ] - } -); diff --git a/rust/vetkeys/encrypted_chat/frontend/package.json b/rust/vetkeys/encrypted_chat/frontend/package.json deleted file mode 100644 index ece26206a..000000000 --- a/rust/vetkeys/encrypted_chat/frontend/package.json +++ /dev/null @@ -1,63 +0,0 @@ -{ - "name": "svelte-latest", - "private": true, - "version": "0.0.1", - "type": "module", - "scripts": { - "build": "npm run build:bindings && vite build", - "dev": "npm run build:bindings && vite build --mode development && vite dev --host", - "build:bindings": "cd scripts && ./gen_bindings.sh", - "preview": "vite preview", - "prepare": "svelte-kit sync || echo ''", - "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json", - "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch", - "lint": "eslint . && prettier --check .", - "format": "prettier --write .", - "test:e2e": "playwright test", - "test": "npm run test:e2e" - }, - "devDependencies": { - "@eslint/compat": "^1.2.5", - "@eslint/js": "^9.18.0", - "@playwright/test": "^1.57.0", - "@rollup/plugin-typescript": "^12.1.4", - "@sveltejs/adapter-auto": "^6.0.0", - "@sveltejs/kit": "^2.53.4", - "@sveltejs/vite-plugin-svelte": "^6.0.0", - "@tailwindcss/vite": "^4.0.0", - "@types/isomorphic-fetch": "^0.0.39", - "eslint": "^9.18.0", - "eslint-config-prettier": "^10.0.1", - "eslint-plugin-svelte": "^3.0.0", - "globals": "^16.0.0", - "prettier": "^3.4.2", - "prettier-plugin-svelte": "^3.3.3", - "prettier-plugin-tailwindcss": "^0.6.11", - "rollup-plugin-css-only": "^4.5.2", - "svelte": "^5.53.6", - "svelte-check": "^4.0.0", - "tailwindcss": "^4.0.0", - "tslib": "^2.8.1", - "typescript": "^5.0.0", - "typescript-eslint": "^8.20.0", - "vite": "^7.3.1", - "vite-plugin-environment": "^1.1.3" - }, - "dependencies": { - "@dfinity/agent": "^3.1.0", - "@dfinity/auth-client": "^3.1.0", - "@dfinity/vetkeys": "^0.4.0", - "@melt-ui/svelte": "^0.86.6", - "@skeletonlabs/skeleton": "^3.1.7", - "@skeletonlabs/tw-plugin": "^0.4.1", - "@sveltejs/adapter-static": "^3.0.8", - "base64-js": "^1.5.1", - "cbor-x": "^1.6.0", - "fake-indexeddb": "^6.0.1", - "idb-keyval": "^6.2.2", - "isomorphic-fetch": "^3.0.0", - "js-base64": "^3.7.8", - "lucide-svelte": "^0.536.0", - "sass": "^1.90.0" - } -} diff --git a/rust/vetkeys/encrypted_chat/frontend/playwright.config.ts b/rust/vetkeys/encrypted_chat/frontend/playwright.config.ts deleted file mode 100644 index f6c81af8a..000000000 --- a/rust/vetkeys/encrypted_chat/frontend/playwright.config.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { defineConfig } from '@playwright/test'; - -export default defineConfig({ - webServer: { - command: 'npm run build && npm run preview', - port: 4173 - }, - testDir: 'e2e' -}); diff --git a/rust/vetkeys/encrypted_chat/frontend/scripts/gen_bindings.sh b/rust/vetkeys/encrypted_chat/frontend/scripts/gen_bindings.sh deleted file mode 100755 index 14669582a..000000000 --- a/rust/vetkeys/encrypted_chat/frontend/scripts/gen_bindings.sh +++ /dev/null @@ -1,15 +0,0 @@ -#!/bin/bash - -cd ../../backend && make extract-candid - -cd .. && dfx generate encrypted_chat || exit 1 - -rm -r frontend/src/declarations/encrypted_chat > /dev/null 2>&1 || true - -mkdir -p frontend/src/declarations/encrypted_chat -mv src/declarations/encrypted_chat frontend/src/declarations -rmdir -p src/declarations > /dev/null 2>&1 || true - -# dfx 0.31+ generates @icp-sdk/core imports; rewrite to @dfinity/* to match deps -find frontend/src/declarations -type f \( -name '*.ts' -o -name '*.js' \) -exec \ - perl -i -pe 's|\@icp-sdk/core/agent|\@dfinity/agent|g; s|\@icp-sdk/core/principal|\@dfinity/principal|g; s|\@icp-sdk/core/candid|\@dfinity/candid|g' {} + \ No newline at end of file diff --git a/rust/vetkeys/encrypted_chat/frontend/src/app.css b/rust/vetkeys/encrypted_chat/frontend/src/app.css deleted file mode 100644 index bda1fcb2e..000000000 --- a/rust/vetkeys/encrypted_chat/frontend/src/app.css +++ /dev/null @@ -1,279 +0,0 @@ -@import 'tailwindcss'; - -/* Modern Professional Design System */ -:root { - /* Color Palette - Professional & Modern */ - --primary-50: #eff6ff; - --primary-100: #dbeafe; - --primary-200: #bfdbfe; - --primary-300: #93c5fd; - --primary-400: #60a5fa; - --primary-500: #3b82f6; - --primary-600: #2563eb; - --primary-700: #1d4ed8; - --primary-800: #1e40af; - --primary-900: #1e3a8a; - - /* CSS variable mappings for primary colors */ - --color-primary-50: var(--primary-50); - --color-primary-100: var(--primary-100); - --color-primary-200: var(--primary-200); - --color-primary-300: var(--primary-300); - --color-primary-400: var(--primary-400); - --color-primary-500: var(--primary-500); - --color-primary-600: var(--primary-600); - --color-primary-700: var(--primary-700); - --color-primary-800: var(--primary-800); - --color-primary-900: var(--primary-900); - - /* CSS variable mappings for surface colors */ - --color-surface-50: var(--gray-50); - --color-surface-100: var(--gray-100); - --color-surface-200: var(--gray-200); - --color-surface-300: var(--gray-300); - --color-surface-400: var(--gray-400); - --color-surface-500: var(--gray-500); - --color-surface-600: var(--gray-600); - --color-surface-700: var(--gray-700); - --color-surface-800: var(--gray-800); - --color-surface-900: var(--gray-900); - - --gray-50: #f9fafb; - --gray-100: #f3f4f6; - --gray-200: #e5e7eb; - --gray-300: #d1d5db; - --gray-400: #9ca3af; - --gray-500: #6b7280; - --gray-600: #4b5563; - --gray-700: #374151; - --gray-800: #1f2937; - --gray-900: #111827; - - /* Modern shadows */ - --shadow-xs: 0 1px 2px 0 rgb(0 0 0 / 0.05); - --shadow-sm: 0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1); - --shadow-md: 0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1); - --shadow-lg: 0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1); - --shadow-xl: 0 20px 25px -5px rgb(0 0 0 / 0.1), 0 8px 10px -6px rgb(0 0 0 / 0.1); - - /* Border radius */ - --radius-sm: 0.375rem; - --radius: 0.5rem; - --radius-md: 0.75rem; - --radius-lg: 1rem; - --radius-xl: 1.5rem; -} - - -/* Professional Component Styles */ -.btn { - @apply inline-flex items-center justify-center rounded-lg text-sm font-medium whitespace-nowrap transition-all duration-200; - @apply focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-2 focus-visible:outline-none; - @apply disabled:pointer-events-none disabled:opacity-50; -} - -.btn-sm { - @apply h-9 px-3; -} - -.btn-md { - @apply h-10 px-4; -} - -.btn-lg { - @apply h-11 px-8; -} - -.variant-filled-primary { - @apply transform bg-blue-600 text-white shadow-md hover:-translate-y-0.5 hover:bg-blue-700 hover:shadow-lg; -} - -.variant-outline-primary { - @apply border border-blue-200 bg-white text-blue-700 shadow-sm hover:bg-blue-50 hover:shadow-md; -} - -.variant-ghost-primary { - @apply text-blue-700 hover:bg-blue-100 hover:text-blue-800; -} - -.card { - @apply rounded-xl border border-gray-200 bg-white shadow-lg; -} - -.input { - @apply flex w-full rounded-lg border border-gray-200 bg-white px-3 py-2 text-sm transition-all; - @apply focus:border-blue-500 focus:ring-2 focus:ring-blue-500/20 focus:outline-none; -} - -.badge { - @apply inline-flex items-center rounded-full px-2.5 py-0.5 text-xs font-medium; -} - -.badge-secondary { - @apply bg-gray-100 text-gray-800; -} - -/* Modern Chat Interface Classes */ -.bg-surface-50-900 { - @apply bg-gray-50; -} - -.bg-surface-100-800 { - @apply bg-gray-100; -} - -.bg-surface-200-700 { - @apply bg-gray-200; -} - -.border-surface-200-700 { - @apply border-gray-200; -} - -.text-surface-500-400 { - @apply text-gray-500; -} - -/* Professional glass effect */ -.glass-effect { - backdrop-filter: blur(16px); - background: rgba(255, 255, 255, 0.9); - border: 1px solid rgba(255, 255, 255, 0.2); - box-shadow: - 0 20px 25px -5px rgba(0, 0, 0, 0.1), - 0 8px 10px -6px rgba(0, 0, 0, 0.1); -} - -/* Message bubbles */ -.message-bubble-own { - @apply rounded-2xl rounded-br-md bg-blue-600 text-white shadow-lg; -} - -.message-bubble-other { - @apply rounded-2xl rounded-bl-md border border-gray-200 bg-white text-gray-900 shadow-lg; -} - -/* Modern scrollbar */ -.modern-scrollbar { - scrollbar-width: thin; - scrollbar-color: rgb(156 163 175) transparent; -} - -.modern-scrollbar::-webkit-scrollbar { - width: 6px; -} - -.modern-scrollbar::-webkit-scrollbar-track { - background: transparent; -} - -.modern-scrollbar::-webkit-scrollbar-thumb { - @apply rounded-full bg-gray-300 hover:bg-gray-400; -} - -/* Professional animations */ -@keyframes fade-in { - from { - opacity: 0; - transform: translateY(10px); - } - to { - opacity: 1; - transform: translateY(0); - } -} - -@keyframes slide-in { - from { - opacity: 0; - transform: translateX(-20px); - } - to { - opacity: 1; - transform: translateX(0); - } -} - -@keyframes pulse-glow { - 0%, - 100% { - box-shadow: 0 0 0 0 rgba(59, 130, 246, 0.4); - } - 50% { - box-shadow: 0 0 0 10px rgba(59, 130, 246, 0); - } -} - -.animate-fade-in { - animation: fade-in 0.3s ease-out; -} - -.animate-slide-in { - animation: slide-in 0.3s ease-out; -} - -.animate-pulse-glow { - animation: pulse-glow 2s infinite; -} - -/* Professional gradients */ -.gradient-primary { - background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); -} - -.gradient-secondary { - background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%); -} - -.gradient-success { - background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%); -} - -.message-input { - scrollbar-width: thin; -} - -.message-input::-webkit-scrollbar { - width: 4px; -} - -.message-input::-webkit-scrollbar-thumb { - @apply bg-gray-400; - border-radius: 2px; -} - -.alert { - display: flex; - align-items: center; - gap: 0.75rem; - padding: 1rem; - border-radius: 0.5rem; -} - -.alert-message { - flex: 1 1 0%; - min-width: 0; -} - -.alert-actions { - display: flex; - align-items: center; - gap: 0.5rem; -} - -/* Alert/Notification variants with softer, muted colors */ -.variant-filled-primary.alert { - @apply bg-blue-50 text-blue-900 border border-blue-200; -} - -.variant-filled-success.alert { - @apply bg-green-50 text-green-900 border border-green-200; -} - -.variant-filled-warning.alert { - @apply bg-orange-50 text-orange-900 border border-orange-200; -} - -.variant-filled-error.alert { - @apply bg-red-50 text-red-900 border border-red-200; -} diff --git a/rust/vetkeys/encrypted_chat/frontend/src/app.d.ts b/rust/vetkeys/encrypted_chat/frontend/src/app.d.ts deleted file mode 100644 index da08e6da5..000000000 --- a/rust/vetkeys/encrypted_chat/frontend/src/app.d.ts +++ /dev/null @@ -1,13 +0,0 @@ -// See https://svelte.dev/docs/kit/types#app.d.ts -// for information about these interfaces -declare global { - namespace App { - // interface Error {} - // interface Locals {} - // interface PageData {} - // interface PageState {} - // interface Platform {} - } -} - -export {}; diff --git a/rust/vetkeys/encrypted_chat/frontend/src/app.html b/rust/vetkeys/encrypted_chat/frontend/src/app.html deleted file mode 100644 index ba84abc99..000000000 --- a/rust/vetkeys/encrypted_chat/frontend/src/app.html +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - VetKeys Encrypted Chat - %sveltekit.head% - - -
%sveltekit.body%
- - diff --git a/rust/vetkeys/encrypted_chat/frontend/src/lib/assets/favicon.svg b/rust/vetkeys/encrypted_chat/frontend/src/lib/assets/favicon.svg deleted file mode 100644 index cc5dc66a3..000000000 --- a/rust/vetkeys/encrypted_chat/frontend/src/lib/assets/favicon.svg +++ /dev/null @@ -1 +0,0 @@ -svelte-logo \ No newline at end of file diff --git a/rust/vetkeys/encrypted_chat/frontend/src/lib/components/ChatHeader.svelte b/rust/vetkeys/encrypted_chat/frontend/src/lib/components/ChatHeader.svelte deleted file mode 100644 index 5b2ddd3f4..000000000 --- a/rust/vetkeys/encrypted_chat/frontend/src/lib/components/ChatHeader.svelte +++ /dev/null @@ -1,268 +0,0 @@ - - -
-
- -
- - {#if showMobileBackButton} - - {/if} -
- {getDisplayAvatar()} -
-
-

- {getDisplayName()} -

-
-
- - -
- - - {#if chat.type === 'group'} - - {/if} -
-
- - - {#if showChatInfo} - -

Chat Information

- -
- -
-

Details

-
-
- Type: - {chat.type} -
-
- Participants: - {chat.participants.length} -
-
- Disappearing messages: - {chat.disappearingMessagesDuration === 0 - ? 'Disabled' - : `${chat.disappearingMessagesDuration} days`} -
-
- Status: - -
- {chat.isReady ? 'Ready' : 'Not ready'} -
-
-
-
- - -
-

Encryption

-
-
- Last key rotation: - {formatDate(chat.keyRotationStatus.lastRotation)} -
-
-
- - - {#if ratchetStats} -
-

Ratchet Statistics

-
-
- Current epoch: - {ratchetStats.currentEpoch} -
-
- Messages in epoch: - {ratchetStats.messagesInCurrentEpoch} -
-
- Last rotation: - {formatDate(ratchetStats.lastRotation)} -
-
- Next scheduled: - {formatDate(ratchetStats.nextScheduledRotation)} -
-
-
- {:else if loadingRatchetStats} -
-
- - Loading ratchet statistics... -
-
- {/if} - - - {#if chat.type === 'group'} -
-

Participants

-
- {#each chat.participants as participant (participant.principal)} -
-
- {participant.avatar || '👤'} -
- {participant.name} -
- {/each} -
-
- {/if} -
-
- {/if} -
- - -{#if chat.type === 'group'} - (showGroupManagement = false)} - /> -{/if} diff --git a/rust/vetkeys/encrypted_chat/frontend/src/lib/components/ChatInterface.svelte b/rust/vetkeys/encrypted_chat/frontend/src/lib/components/ChatInterface.svelte deleted file mode 100644 index 5c7342f06..000000000 --- a/rust/vetkeys/encrypted_chat/frontend/src/lib/components/ChatInterface.svelte +++ /dev/null @@ -1,108 +0,0 @@ - - -
- {#if selectedChat} - - - - - - - - {#key selectedChat.idStr} - - {/key} - {:else} - - -
-
-

- VetKeys Chat -

-
-
- - -
-
-
- 💬 -
-

Welcome to VetKeys Chat

-

- Select a conversation from the sidebar to start chatting securely with end-to-end - encryption. -

-
-
- 🔒 - End-to-end encrypted messages -
-
- 🔑 - Automatic key rotation -
-
- - Disappearing messages support -
-
-
-
- {/if} -
- - diff --git a/rust/vetkeys/encrypted_chat/frontend/src/lib/components/ChatList.svelte b/rust/vetkeys/encrypted_chat/frontend/src/lib/components/ChatList.svelte deleted file mode 100644 index fb63924ac..000000000 --- a/rust/vetkeys/encrypted_chat/frontend/src/lib/components/ChatList.svelte +++ /dev/null @@ -1,70 +0,0 @@ - - -
- - - - -
-

- Chats -

-

- {chats.state.length} conversation{chats.state.length !== 1 ? 's' : ''} -

-
- -
-
- - -
- {#each chats.state as chat (chat.idStr)} - - {:else} -
-

No chats yet

-

Your conversations will appear here

-
- {/each} -
-
- - - - diff --git a/rust/vetkeys/encrypted_chat/frontend/src/lib/components/ChatListItem.svelte b/rust/vetkeys/encrypted_chat/frontend/src/lib/components/ChatListItem.svelte deleted file mode 100644 index 585f7c8e9..000000000 --- a/rust/vetkeys/encrypted_chat/frontend/src/lib/components/ChatListItem.svelte +++ /dev/null @@ -1,219 +0,0 @@ - - - - - diff --git a/rust/vetkeys/encrypted_chat/frontend/src/lib/components/DisclaimerCopy.svelte b/rust/vetkeys/encrypted_chat/frontend/src/lib/components/DisclaimerCopy.svelte deleted file mode 100644 index 82446f728..000000000 --- a/rust/vetkeys/encrypted_chat/frontend/src/lib/components/DisclaimerCopy.svelte +++ /dev/null @@ -1,4 +0,0 @@ - - -Disclaimer: This sample dapp is intended exclusively for experimental purpose. You are -advised not to use this dapp for storing your critical data such as keys or passwords. diff --git a/rust/vetkeys/encrypted_chat/frontend/src/lib/components/EmojiPicker.svelte b/rust/vetkeys/encrypted_chat/frontend/src/lib/components/EmojiPicker.svelte deleted file mode 100644 index 9502d5bbc..000000000 --- a/rust/vetkeys/encrypted_chat/frontend/src/lib/components/EmojiPicker.svelte +++ /dev/null @@ -1,233 +0,0 @@ - - -{#if show} - -
{}} - >
- - -
-
-

Add Emoji

- -
- -
- {#each Object.entries(emojiCategories) as [category, emojis] (category)} -
-

{category}

-
- {#each emojis as emoji (emoji)} - - {/each} -
-
- {/each} -
- -
-

- You can also type emoji shortcodes like :smile:, :heart:, - :rocket: -

-
-
-{/if} - - diff --git a/rust/vetkeys/encrypted_chat/frontend/src/lib/components/GroupManagementModal.svelte b/rust/vetkeys/encrypted_chat/frontend/src/lib/components/GroupManagementModal.svelte deleted file mode 100644 index e8846c441..000000000 --- a/rust/vetkeys/encrypted_chat/frontend/src/lib/components/GroupManagementModal.svelte +++ /dev/null @@ -1,229 +0,0 @@ - - -{#if show} - -
- - -
- -
-
- -

Manage Group

- {groupChat.name} -
- -
- - -
- -
-

Current Members ({groupChat.participants.length})

-
- {#each groupChat.participants as member (member.principal)} -
-
-
- {member.avatar || '👤'} -
-
-

{member.name}

-
-
- {#if member.principal.toText() === getMyPrincipal().toText()} - Me - {/if} -
-
-
- - {#if canRemoveUser(member.principal)} - - {/if} -
- {/each} -
-
- - -
-

Add by Principal

-
- - - {#if invalidPrincipalTokens.length > 0} -
- Invalid principals: -
- {#each invalidPrincipalTokens as t (t)} - {t} - {/each} -
-
- {:else if validPrincipalStrings.length > 0} -
- Will add {validPrincipalStrings.length} principal{validPrincipalStrings.length !== - 1 - ? 's' - : ''} -
- {/if} -
-
- - - {#if totalAddCount > 0 || selectedToRemove.length > 0} -
-

Changes Summary

-
- {#if totalAddCount > 0} -

- + {totalAddCount} member{totalAddCount !== 1 ? 's' : ''} to add -

- {/if} - {#if selectedToRemove.length > 0} -

- - {selectedToRemove.length} member{selectedToRemove.length !== 1 ? 's' : ''} to remove -

- {/if} -
-
- {/if} -
- - -
- - -
-
-
-
-{/if} diff --git a/rust/vetkeys/encrypted_chat/frontend/src/lib/components/Hero.svelte b/rust/vetkeys/encrypted_chat/frontend/src/lib/components/Hero.svelte deleted file mode 100644 index 5a52e4d53..000000000 --- a/rust/vetkeys/encrypted_chat/frontend/src/lib/components/Hero.svelte +++ /dev/null @@ -1,35 +0,0 @@ - - -
-
-
-

- Encrypted Chat using vetKeys -

-

Your private chat on the Internet Computer.

-

A safe place to store your personal messages and much more...

- - {#if auth.state.label === 'initializing-auth'} -
- - Initializing... -
- {:else if auth.state.label === 'anonymous'} - - {:else if auth.state.label === 'error'} -
An error occurred.
- {/if} - -
- -
-
-
-
diff --git a/rust/vetkeys/encrypted_chat/frontend/src/lib/components/MessageBubble.svelte b/rust/vetkeys/encrypted_chat/frontend/src/lib/components/MessageBubble.svelte deleted file mode 100644 index e44788779..000000000 --- a/rust/vetkeys/encrypted_chat/frontend/src/lib/components/MessageBubble.svelte +++ /dev/null @@ -1,324 +0,0 @@ - - -
- - {#if showAvatar && !isOwnMessage} -
- {sender?.avatar || '👤'} -
- {:else if showAvatar && isOwnMessage} -
- {/if} - - -
- - {#if !isOwnMessage && sender && showAvatar} -
- {sender.name} -
- {/if} - - -
- {#if message.fileData === undefined} - -

{@html parseEmojis(message.content)}

- {:else if message.fileData !== undefined} -
- {#if isImageFile(message.fileData.type)} -
- {message.fileData.name} -
- {:else} -
- -
- {/if} - -
-
-
-

{message.fileData.name}

-

- {formatFileSize(message.fileData.size)} -

-
- -
-
-
- {/if} -
- - - {#if showTimestamp} -
- {formatTime(message.timestamp)} - 🔒 - vetKeyEpoch - {message.vetkeyEpoch} SymmRatchetEpoch {message.symmetricRatchetEpoch} -
- {/if} -
-
- - diff --git a/rust/vetkeys/encrypted_chat/frontend/src/lib/components/MessageHistory.svelte b/rust/vetkeys/encrypted_chat/frontend/src/lib/components/MessageHistory.svelte deleted file mode 100644 index 67f7473d7..000000000 --- a/rust/vetkeys/encrypted_chat/frontend/src/lib/components/MessageHistory.svelte +++ /dev/null @@ -1,285 +0,0 @@ - - -
- {#if selectedChat} -
- {#if selectedChatMessages.length === 0} - -
-
- {#if selectedChat.type === 'direct'} - {selectedChat.participants.find( - (p) => p.principal.toString() !== getMyPrincipal().toString() - )?.avatar || '👤'} - {:else} - {selectedChat.avatar || '👥'} - {/if} -
-

- {#if selectedChat.type === 'direct'} - {selectedChat.participants.find( - (p) => p.principal.toString() !== getMyPrincipal().toString() - )?.name || 'Me'} - {:else} - {selectedChat.name} - {/if} -

-

- {getParticipantInfo()} -

- {#if selectedChat.disappearingMessagesDuration > 0} -
- - 🕐 Messages disappear after {selectedChat.disappearingMessagesDuration} day{selectedChat.disappearingMessagesDuration !== - 1 - ? 's' - : ''} - -
- {/if} -
- {:else} - -
- {#each selectedChatMessages as message, index (message.messageId)} - - {#if shouldShowDateSeparator(message, index)} -
-
- {formatDateSeparator(message.timestamp)} -
-
- {/if} - - -
- -
- {/each} -
- {/if} -
- - - {#if !autoScroll} -
- -
- {/if} - {:else} - -
-
- 💬 -
-

Welcome to VetKeys Chat

-

- Select a conversation from the sidebar to start chatting securely with end-to-end - encryption. -

-
-
- 🔒 - End-to-end encrypted messages -
-
- 🔑 - Automatic key rotation -
-
- - Disappearing messages support -
-
-
- {/if} -
- - diff --git a/rust/vetkeys/encrypted_chat/frontend/src/lib/components/MessageInput.svelte b/rust/vetkeys/encrypted_chat/frontend/src/lib/components/MessageInput.svelte deleted file mode 100644 index f5add7e4a..000000000 --- a/rust/vetkeys/encrypted_chat/frontend/src/lib/components/MessageInput.svelte +++ /dev/null @@ -1,207 +0,0 @@ - - -
- - {#if selectedFile} - -
- {#if selectedFile.preview} - Preview - {:else} -
- -
- {/if} - -
-

{selectedFile.file.name}

-

{formatFileSize(selectedFile.file.size)}

- {#if !selectedFile.isValid && selectedFile.error} -

{selectedFile.error}

- {/if} -
- - -
-
- {/if} - - -
- - - - -
- - - -
- - - - - -
-
- - -
- -
-
-
- - - (showEmojiPicker = false)} -/> diff --git a/rust/vetkeys/encrypted_chat/frontend/src/lib/components/NewChatModal.svelte b/rust/vetkeys/encrypted_chat/frontend/src/lib/components/NewChatModal.svelte deleted file mode 100644 index 0d7803a3e..000000000 --- a/rust/vetkeys/encrypted_chat/frontend/src/lib/components/NewChatModal.svelte +++ /dev/null @@ -1,156 +0,0 @@ - - -{#if show} - -{/if} diff --git a/rust/vetkeys/encrypted_chat/frontend/src/lib/components/NotificationBanner.svelte b/rust/vetkeys/encrypted_chat/frontend/src/lib/components/NotificationBanner.svelte deleted file mode 100644 index 5ce7cbe56..000000000 --- a/rust/vetkeys/encrypted_chat/frontend/src/lib/components/NotificationBanner.svelte +++ /dev/null @@ -1,98 +0,0 @@ - - - -{#if showDisclaimer} -
-
- -
- - Disclaimer: This sample dapp is intended exclusively for experimental purposes. - You are advised not to use this dapp for storing your critical data such as keys or passwords. - -
- -
-
-{/if} - - -
- {#each notifications.state as notification (notification.id)} -
- -
-

{notification.title}

-

{notification.message}

-
- {#if notification.isDismissible} -
- -
- {/if} -
- {/each} -
diff --git a/rust/vetkeys/encrypted_chat/frontend/src/lib/components/Spinner.svelte b/rust/vetkeys/encrypted_chat/frontend/src/lib/components/Spinner.svelte deleted file mode 100644 index e421e7910..000000000 --- a/rust/vetkeys/encrypted_chat/frontend/src/lib/components/Spinner.svelte +++ /dev/null @@ -1,5 +0,0 @@ - - - diff --git a/rust/vetkeys/encrypted_chat/frontend/src/lib/components/UserProfile.svelte b/rust/vetkeys/encrypted_chat/frontend/src/lib/components/UserProfile.svelte deleted file mode 100644 index 1a7387573..000000000 --- a/rust/vetkeys/encrypted_chat/frontend/src/lib/components/UserProfile.svelte +++ /dev/null @@ -1,169 +0,0 @@ - - - - - -{#if showConfig} - -
{}} - >
- - -
-
- -
-

Settings

- -
- - -
-
- - -

- How long to keep symmetric key cache before automatic cleanup -

-
-
- - -
- - -
-
-
-{/if} - - diff --git a/rust/vetkeys/encrypted_chat/frontend/src/lib/components/ui/Badge.svelte b/rust/vetkeys/encrypted_chat/frontend/src/lib/components/ui/Badge.svelte deleted file mode 100644 index 881223778..000000000 --- a/rust/vetkeys/encrypted_chat/frontend/src/lib/components/ui/Badge.svelte +++ /dev/null @@ -1,18 +0,0 @@ - - - - - {@render children?.()} - diff --git a/rust/vetkeys/encrypted_chat/frontend/src/lib/components/ui/Button.svelte b/rust/vetkeys/encrypted_chat/frontend/src/lib/components/ui/Button.svelte deleted file mode 100644 index 174cdfebf..000000000 --- a/rust/vetkeys/encrypted_chat/frontend/src/lib/components/ui/Button.svelte +++ /dev/null @@ -1,55 +0,0 @@ - - - diff --git a/rust/vetkeys/encrypted_chat/frontend/src/lib/components/ui/Card.svelte b/rust/vetkeys/encrypted_chat/frontend/src/lib/components/ui/Card.svelte deleted file mode 100644 index 67462836f..000000000 --- a/rust/vetkeys/encrypted_chat/frontend/src/lib/components/ui/Card.svelte +++ /dev/null @@ -1,35 +0,0 @@ - - -
- - {@render children?.()} -
diff --git a/rust/vetkeys/encrypted_chat/frontend/src/lib/crypto/keyManager.ts b/rust/vetkeys/encrypted_chat/frontend/src/lib/crypto/keyManager.ts deleted file mode 100644 index 69c7394a3..000000000 --- a/rust/vetkeys/encrypted_chat/frontend/src/lib/crypto/keyManager.ts +++ /dev/null @@ -1,95 +0,0 @@ -import { SymmetricRatchetState } from './symmetricRatchet'; -import { Principal } from '@dfinity/principal'; - -export class KeyManager { - #symmetricRatchetStates: Map> = new Map(); - - constructor() {} - - getCurrentChatIdStrs(): string[] { - return Array.from(this.#symmetricRatchetStates.keys()); - } - - inductSymmetricRatchetState(chatId: string, vetKeyEpoch: bigint, state: SymmetricRatchetState) { - if (this.#symmetricRatchetStates.get(chatId)?.has(vetKeyEpoch)) { - throw new Error( - `KeyManager.inductState: Symmetric ratchet state for chatId ${chatId} and vetKeyEpoch ${vetKeyEpoch} already exists` - ); - } - console.log( - `KeyManager.inductState: Inducting state for chatId ${chatId} and vetKeyEpoch ${vetKeyEpoch} with creation time ${state.getCreationTime().toUTCString()} and ratchet epoch ${state.getCurrentEpoch().toString()}` - ); - this.#symmetricRatchetStates.set(chatId, new Map([[vetKeyEpoch, state]])); - } - - async encryptNow( - chatId: string, - sender: Principal, - senderMessageId: bigint, - message: Uint8Array - ): Promise<{ encryptedBytes: Uint8Array; vetKeyEpoch: bigint; symmetricRatchetEpoch: bigint }> { - const { vetKeyEpoch, symmetricRatchetState } = this.lastVetKeyEpoch(chatId); - const { encryptedBytes, symmetricRatchetEpoch } = await symmetricRatchetState.encryptNow( - sender, - senderMessageId, - message - ); - - return { encryptedBytes, vetKeyEpoch, symmetricRatchetEpoch }; - } - - async decryptAtTimeAndEvolveIfNeeded( - chatId: string, - sender: Principal, - senderMessageId: bigint, - vetKeyEpoch: bigint, - encryptedBytes: Uint8Array, - time: Date - ): Promise { - const symmetricRatchetState = this.#symmetricRatchetStates.get(chatId)?.get(vetKeyEpoch); - if (!symmetricRatchetState) { - throw new Error( - `KeyManager.decryptAtTimeAndEvolveIfNeeded: No symmetric ratchet states found for chatId ${chatId} and vetKeyEpoch ${vetKeyEpoch}` - ); - } - return await symmetricRatchetState.decryptAtTimeAndEvolveIfNeeded( - sender, - senderMessageId, - encryptedBytes, - time - ); - } - - doesChatHaveKeys(chatId: string): boolean { - return this.#symmetricRatchetStates.has(chatId); - } - - doesChatHaveRatchetStateForEpoch(chatId: string, vetKeyEpoch: bigint): boolean { - return this.#symmetricRatchetStates.get(chatId)?.has(vetKeyEpoch) ?? false; - } - - private lastVetKeyEpoch(chatId: string): { - vetKeyEpoch: bigint; - symmetricRatchetState: SymmetricRatchetState; - } { - const chatIdEpochStates = this.#symmetricRatchetStates.get(chatId); - if (!chatIdEpochStates) { - throw new Error( - `KeyManager.lastVetKeyEpoch: No symmetric ratchet states found for chatId ${chatId}` - ); - } - let last = null; - - for (const [vetKeyEpoch, symmetricRatchetState] of chatIdEpochStates.entries()) { - if (last === null || vetKeyEpoch > last.vetKeyEpoch) { - last = { vetKeyEpoch, symmetricRatchetState }; - } - } - if (last === null) { - throw new Error( - `Bug: KeyManager.lastVetKeyEpoch: chatIdEpochStates.size === 0 for chatId ${chatId}` - ); - } - return last; - } -} diff --git a/rust/vetkeys/encrypted_chat/frontend/src/lib/crypto/symmetricRatchet.ts b/rust/vetkeys/encrypted_chat/frontend/src/lib/crypto/symmetricRatchet.ts deleted file mode 100644 index 69977b53c..000000000 --- a/rust/vetkeys/encrypted_chat/frontend/src/lib/crypto/symmetricRatchet.ts +++ /dev/null @@ -1,435 +0,0 @@ -import { DerivedKeyMaterial, deriveSymmetricKey, type VetKey } from '@dfinity/vetkeys'; -import { - sizePrefixedBytesFromString, - u8ByteUint8ArrayBigEndianToUBigInt, - uBigIntTo8ByteUint8ArrayBigEndian -} from '../utils'; -import { Principal } from '@dfinity/principal'; - -const DOMAIN_RATCHET_INIT = sizePrefixedBytesFromString('ic-vetkeys-chat-ratchet-init'); -const DOMAIN_RATCHET_STEP = sizePrefixedBytesFromString('ic-vetkeys-chat-ratchet-step'); -const DOMAIN_MESSAGE_ENCRYPTION = sizePrefixedBytesFromString( - 'ic-vetkeys-chat-message-encryption' -); - -// Notes -// -// We need the following functionalities here: -// - Raw key -// - Derive the root epoch key from a vetKey as bytes. Those bytes can be cached in a canister in encrypted form. -// - Import bytes from a canister's decrypted cache. -// - Eventually evolve the state to the needed epoch. -// - CryptoKey -// - Store the epoch key in CryptoKey form in RAM and indexedDB. -// - Evolve the crypto key. -// - Peek at a future key without evolving the state, e.g., we encrypt our message with a key in the next epoch -// but we are not sure if the canister holds a message that we will need to decrypt using the current key. -// - Encrypt/decrypt messages using the crypto key. -// -// In summary, the raw key state allows to cache the key and the CryptoKey state allows to encrypt/decrypt messages. - -export type StorableSymmetricRatchetState = { - cryptoKey: CryptoKey; - symmetricRatchetEpoch: bigint; - creationTime: Date; - rotationDuration: Date; -}; - -export class SymmetricRatchetState { - #cryptoKey: CryptoKey; - #symmetricRatchetEpoch: bigint; - readonly #creationTime: Date; - readonly #rotationDuration: Date; - - constructor( - key: CryptoKey, - symmetricRatchetEpoch: bigint, - creationTime: Date, - rotationDuration: Date - ) { - this.#cryptoKey = key; - this.#symmetricRatchetEpoch = symmetricRatchetEpoch; - this.#creationTime = creationTime; - this.#rotationDuration = rotationDuration; - } - - toStorable(): StorableSymmetricRatchetState { - return { - cryptoKey: this.#cryptoKey, - symmetricRatchetEpoch: this.#symmetricRatchetEpoch, - creationTime: this.#creationTime, - rotationDuration: this.#rotationDuration - }; - } - - static async fromRawKeyState( - rawKeyState: CacheableSymmetricRatchetState - ): Promise { - const { key, symmetricKeyEpoch } = await importKeyFromBytes( - rawKeyState.rawKey, - rawKeyState.symmetricRatchetEpoch - ); - return new SymmetricRatchetState( - key, - symmetricKeyEpoch, - rawKeyState.creationTime, - rawKeyState.rotationDuration - ); - } - - async decryptAtTimeAndEvolveIfNeeded( - sender: Principal, - senderMessageId: bigint, - message: Uint8Array, - time: Date - ): Promise { - if (time < this.#creationTime) { - throw new Error('Cannot decrypt message before the state was created'); - } - const expectedEpoch = this.getExpectedEpochAtTime(time); - await this.evolveTo(expectedEpoch); - const domainSeparator = messageEncryptionDomainSeparator(sender, senderMessageId); - const derivedKeyMaterial = DerivedKeyMaterial.fromCryptoKey(this.#cryptoKey); - return await derivedKeyMaterial.decryptMessage(message, domainSeparator); - } - - async encryptNow( - sender: Principal, - senderMessageId: bigint, - message: Uint8Array - ): Promise<{ encryptedBytes: Uint8Array; symmetricRatchetEpoch: bigint }> { - const now = new Date(Date.now()); - if (now < this.#creationTime) { - throw new Error('Cannot decrypt message before the state was created'); - } - const expectedEpoch = this.getExpectedEpochAtTime(now); - const neededSymmetricRatchetState = await this.peekAtEpoch(expectedEpoch); - const domainSeparator = messageEncryptionDomainSeparator(sender, senderMessageId); - const derivedKeyMaterial = DerivedKeyMaterial.fromCryptoKey( - neededSymmetricRatchetState.#cryptoKey - ); - const encryptedBytes = await derivedKeyMaterial.encryptMessage(message, domainSeparator); - console.log( - `SymmetricRatchetState.encryptNow: encrypted message symmetric ratchet epoch ${neededSymmetricRatchetState.#symmetricRatchetEpoch.toString()}` - ); - return { - encryptedBytes, - symmetricRatchetEpoch: neededSymmetricRatchetState.#symmetricRatchetEpoch - }; - } - - /// Evolve the state to the next epoch. - async evolve() { - const newCryptoKey = await deriveNextSymmetricRatchetEpochCryptoKey( - this.#cryptoKey, - this.#symmetricRatchetEpoch - ); - - this.#cryptoKey = newCryptoKey; - this.#symmetricRatchetEpoch += 1n; - } - - async evolveTo(desiredEpoch: bigint) { - if (desiredEpoch < this.#symmetricRatchetEpoch) { - throw new Error( - `SymmetricRatchetState.evolveTo: desiredEpoch ${desiredEpoch.toString()} is less than the current epoch ${this.#symmetricRatchetEpoch.toString()}` - ); - } - if (desiredEpoch === this.#symmetricRatchetEpoch) { - return; - } - while (desiredEpoch > this.#symmetricRatchetEpoch) { - console.log( - `SymmetricRatchetState.evolveTo: evolving from epoch ${this.#symmetricRatchetEpoch.toString()} to epoch ${desiredEpoch.toString()}` - ); - await this.evolve(); - } - } - - /// Peek at a future epoch without evolving the state. - /// - /// Returns an error if desiredEpoch is less than the current epoch. - async peekAtEpoch(desiredEpoch: bigint): Promise { - if (desiredEpoch < this.#symmetricRatchetEpoch) { - throw new Error( - `Cannot peek at epoch ${desiredEpoch} because the current epoch is ${this.#symmetricRatchetEpoch}` - ); - } else if (desiredEpoch === this.#symmetricRatchetEpoch) { - return this; - } - const newSymmetricRatchetState = new SymmetricRatchetState( - this.#cryptoKey, - desiredEpoch, - this.#creationTime, - this.#rotationDuration - ); - await newSymmetricRatchetState.evolveTo(desiredEpoch); - return newSymmetricRatchetState; - } - - getExpectedEpochAtTime(time: Date): bigint { - if (time < this.#creationTime) { - throw new Error('Cannot get expected epoch before the state was created'); - } - - const elapsedSinceCreation = time.getTime() - this.#creationTime.getTime(); - const result = BigInt(Math.floor(elapsedSinceCreation / this.#rotationDuration.getTime())); - console.log( - `SymmetricRatchetState.getExpectedEpochAtTime: now ${time.toUTCString()} creationTime ${this.#creationTime.toUTCString()} elapsedSinceCreation ${elapsedSinceCreation.toString()}ms expectedEpoch ${result.toString()}` - ); - return result; - } - - getCurrentEpoch(): bigint { - return this.#symmetricRatchetEpoch; - } - - getCreationTime(): Date { - return this.#creationTime; - } -} - -/// Contains a raw key and the symmetric ratchet epoch. -/// -/// The raw key can be exported e.g. to a canister's encrypted cache. -export class CacheableSymmetricRatchetState { - rawKey: Uint8Array; - symmetricRatchetEpoch: bigint; - creationTime: Date; - rotationDuration: Date; - - private constructor( - rawKey: Uint8Array, - symmetricRatchetEpoch: bigint, - creationTime: Date, - rotationDuration: Date - ) { - this.rawKey = rawKey; - this.symmetricRatchetEpoch = symmetricRatchetEpoch; - this.creationTime = creationTime; - this.rotationDuration = rotationDuration; - } - - /// Evolve the state to the next epoch. - evolve() { - this.symmetricRatchetEpoch += 1n; - const domainSeparator = new Uint8Array([ - ...DOMAIN_RATCHET_STEP, - ...uBigIntTo8ByteUint8ArrayBigEndian(this.symmetricRatchetEpoch) - ]); - const newRawKey = deriveSymmetricKey(this.rawKey, domainSeparator, 32); - this.rawKey = newRawKey; - } - - evolveTo(desiredEpoch: bigint) { - if (desiredEpoch < this.symmetricRatchetEpoch) { - throw new Error( - `Cannot evolve to epoch ${desiredEpoch} because the current epoch is ${this.symmetricRatchetEpoch}` - ); - } - while (desiredEpoch > this.symmetricRatchetEpoch) { - this.evolve(); - } - } - - /// Peek at a future epoch without evolving the state. - /// - /// Returns an error if desiredEpoch is less than the current epoch. - peekAtEpoch(desiredEpoch: bigint): CacheableSymmetricRatchetState { - const newState = new CacheableSymmetricRatchetState( - structuredClone(this.rawKey), - this.symmetricRatchetEpoch, - this.creationTime, - this.rotationDuration - ); - newState.evolveTo(desiredEpoch); - return newState; - } - - toSymmetricRatchetState(): Promise { - return SymmetricRatchetState.fromRawKeyState(this); - } - - static initializeFromVetKey( - vetKey: VetKey, - creationTime: Date, - rotationDuration: Date - ): CacheableSymmetricRatchetState { - const vetKeyBytes = vetKey.signatureBytes(); - const rawKey = deriveSymmetricKey(vetKeyBytes, DOMAIN_RATCHET_INIT, 32); - return new CacheableSymmetricRatchetState(rawKey, 0n, creationTime, rotationDuration); - } - - serialize(): Uint8Array { - return new Uint8Array([ - ...this.rawKey, - ...uBigIntTo8ByteUint8ArrayBigEndian(this.symmetricRatchetEpoch), - ...uBigIntTo8ByteUint8ArrayBigEndian(BigInt(this.creationTime.getMilliseconds())), - ...uBigIntTo8ByteUint8ArrayBigEndian(BigInt(this.rotationDuration.getMilliseconds())) - ]); - } - - static deserialize(bytes: Uint8Array): CacheableSymmetricRatchetState { - if (bytes.length !== 32 + 3 * 8) { - throw new Error('Invalid serialized state'); - } - const rawKey = bytes.slice(0, 32); - const symmetricRatchetEpoch = u8ByteUint8ArrayBigEndianToUBigInt(bytes.slice(32)); - const creationTime = new Date(Number(u8ByteUint8ArrayBigEndianToUBigInt(bytes.slice(32 + 8)))); - const rotationDuration = new Date( - Number(u8ByteUint8ArrayBigEndianToUBigInt(bytes.slice(32 + 8 + 8))) - ); - return new CacheableSymmetricRatchetState( - rawKey, - symmetricRatchetEpoch, - creationTime, - rotationDuration - ); - } -} - -export function vetKeyToSymmetricRatchetStateBytes(vetKey: VetKey): Uint8Array { - const vetKeyBytes = vetKey.signatureBytes(); - return deriveSymmetricKey(vetKeyBytes, DOMAIN_RATCHET_INIT, 32); -} - -async function deriveNextSymmetricRatchetEpochCryptoKey( - epochKey: CryptoKey, - currentSymmetricKeyEpoch: bigint -): Promise { - console.log(`deriveNextEpochKey: ${currentSymmetricKeyEpoch.toString()}`); - const exportable = false; - const domainSeparator = new Uint8Array([ - ...DOMAIN_RATCHET_STEP, - ...uBigIntTo8ByteUint8ArrayBigEndian(currentSymmetricKeyEpoch) - ]); - const algorithm = { - name: 'HKDF', - hash: 'SHA-256', - length: 32 * 8, - info: domainSeparator, - salt: new Uint8Array() - }; - - const rawKey = await globalThis.crypto.subtle.deriveBits(algorithm, epochKey, 8 * 32); - - return await globalThis.crypto.subtle.importKey('raw', rawKey, algorithm, exportable, [ - 'deriveKey', - 'deriveBits' - ]); -} - -async function importKeyFromBytes( - keyBytes: Uint8Array, - symmetricKeyEpoch: bigint -): Promise<{ key: CryptoKey; symmetricKeyEpoch: bigint }> { - const exportable = false; - const key = await globalThis.crypto.subtle.importKey( - 'raw', - new Uint8Array(keyBytes), - 'HKDF', - exportable, - ['deriveKey', 'deriveBits'] - ); - return { key, symmetricKeyEpoch: symmetricKeyEpoch }; -} - -export function messageEncryptionDomainSeparator( - sender: Principal, - senderMessageId: bigint -): Uint8Array { - return new Uint8Array([ - ...DOMAIN_MESSAGE_ENCRYPTION, - ...sender.toUint8Array(), - ...uBigIntTo8ByteUint8ArrayBigEndian(senderMessageId) - ]); -} - -export function deriveRootKeyBytes(vetKeyBytes: Uint8Array): Uint8Array { - return deriveSymmetricKey(vetKeyBytes, DOMAIN_RATCHET_INIT, 32); -} - -// function deriveSymmetricRatchetRootKey( -// actor: ActorSubclass<_SERVICE>, -// chatId: ChatId, -// vetKeyEpoch: bigint, -// vetKeyBytes: Uint8Array -// ): { keyBytes: Uint8Array; symmetricKeyEpoch: bigint } { -// const rootKey = deriveSymmetricKey(vetKeyBytes, DOMAIN_RATCHET_INIT, 32); -// console.log( -// `Computed rootKey=${stringifyBigInt(rootKey)} from vetKey=${stringifyBigInt(vetKeyBytes)}` -// ); - -// console.log('starting to store the root key in cache: ', rootKey); -// const vetKeyEncryptedCache = new EncryptedCacheManager(getMyPrincipal(), actor); -// const keyState = { keyBytes: rootKey, symmetricKeyEpoch: 0n }; -// // await this future in background -// vetKeyEncryptedCache.encryptAndStoreFor(chatId, vetKeyEpoch, keyState).catch((error) => { -// console.error( -// `Failed to store root key in cache for chat ${chatIdToString(chatId)} vetkeyEpoch ${vetKeyEpoch.toString()}: `, -// error -// ); -// }); -// return keyState; -// } - -// async function symmetricRatchetUntil( -// chatId: ChatId, -// vetkeyEpoch: bigint, -// symmetricKeyEpoch: bigint -// ) { -// while ((await getCurrentSymmetricEpoch(chatId, vetkeyEpoch)) < symmetricKeyEpoch) { -// try { -// const currentSymmetricEpoch = await getCurrentSymmetricEpoch(chatId, vetkeyEpoch); -// const mapKey = chatIdVetKeyEpochToString(chatId, vetkeyEpoch); -// chatIdStringToEpochKeyState.set(mapKey, { -// status: 'ready', -// symmetricKeyEpoch: currentSymmetricEpoch + 1n, -// key: await symmetricRatchet(chatId, vetkeyEpoch, currentSymmetricEpoch) -// }); -// } catch (error) { -// console.warn('symmetricRatchetUntil: ', error); -// } -// } -// } - -// async function fastForwardSymmetricRatchetWithoutSavingUntil( -// chatId: ChatId, -// currentVetkeyEpoch: bigint, -// neededSymmetricKeyEpoch: bigint -// ): Promise { -// const mapKey = chatIdVetKeyEpochToString(chatId, currentVetkeyEpoch); -// const cur = chatIdStringToEpochKeyState.get(mapKey); -// if (!cur) { -// chatIdStringToEpochKeyState.set(mapKey, { -// status: 'missing' -// }); -// return await fastForwardSymmetricRatchetWithoutSavingUntil( -// chatId, -// currentVetkeyEpoch, -// neededSymmetricKeyEpoch -// ); -// } -// if (cur.status === 'error') { -// throw new Error('Failed to get epoch key: ' + cur.error); -// } -// if (cur.status !== 'ready') { -// throw new Error('Epoch key is not ready for symmetric ratchet'); -// } -// const { key, symmetricKeyEpoch: currentSymmetricKeyEpoch } = cur; -// if (currentSymmetricKeyEpoch >= neededSymmetricKeyEpoch) { -// return DerivedKeyMaterial.fromCryptoKey(key); -// } - -// let derivedKeyState = { epochKey: key, symmetricKeyEpoch: currentSymmetricKeyEpoch }; -// while (derivedKeyState.symmetricKeyEpoch < neededSymmetricKeyEpoch) { -// derivedKeyState = { -// epochKey: await deriveNextSymmetricRatchetEpochCryptoKey( -// derivedKeyState.epochKey, -// derivedKeyState.symmetricKeyEpoch -// ), -// symmetricKeyEpoch: derivedKeyState.symmetricKeyEpoch + 1n -// }; -// } -// return DerivedKeyMaterial.fromCryptoKey(derivedKeyState.epochKey); -// } diff --git a/rust/vetkeys/encrypted_chat/frontend/src/lib/index.ts b/rust/vetkeys/encrypted_chat/frontend/src/lib/index.ts deleted file mode 100644 index 856f2b6c3..000000000 --- a/rust/vetkeys/encrypted_chat/frontend/src/lib/index.ts +++ /dev/null @@ -1 +0,0 @@ -// place files you want to import through the `$lib` alias in this folder. diff --git a/rust/vetkeys/encrypted_chat/frontend/src/lib/services/canisterApi.ts b/rust/vetkeys/encrypted_chat/frontend/src/lib/services/canisterApi.ts deleted file mode 100644 index 77983d1a6..000000000 --- a/rust/vetkeys/encrypted_chat/frontend/src/lib/services/canisterApi.ts +++ /dev/null @@ -1,183 +0,0 @@ -import type { ActorSubclass } from '@dfinity/agent'; -import type { SymmetricRatchetStats } from '../types'; -import type { - _SERVICE, - ChatId, - EncryptedMessage, - GroupChatMetadata, - UserMessage, - VetKeyEpochMetadata -} from '../../declarations/encrypted_chat/encrypted_chat.did'; -import { Principal } from '@dfinity/principal'; -import { stringifyBigInt } from '$lib/utils'; -import { TransportSecretKey, EncryptedVetKey, DerivedPublicKey, VetKey } from '@dfinity/vetkeys'; - -// Dummy API service that simulates backend calls -// In real implementation, these would make actual API calls to the backend - -export class CanisterAPI { - async createDirectChat( - actor: ActorSubclass<_SERVICE>, - receiver: Principal, - symmetricKeyRotationDurationMinutes: bigint, - messageExpirationDurationMinutes: bigint - ): Promise<{ creationDate: Date }> { - const result = await actor.create_direct_chat( - receiver, - symmetricKeyRotationDurationMinutes, - messageExpirationDurationMinutes - ); - if ('Err' in result) { - throw new Error(result.Err); - } else { - return { creationDate: new Date(Number(result.Ok / BigInt(1_000_000))) }; - } - } - - async createGroupChat( - actor: ActorSubclass<_SERVICE>, - otherParticipants: Principal[], - symmetricKeyRotationDurationMinutes: bigint, - messageExpirationDurationMinutes: bigint - ): Promise { - const result = await actor.create_group_chat( - otherParticipants, - symmetricKeyRotationDurationMinutes, - messageExpirationDurationMinutes - ); - if ('Ok' in result) { - return result.Ok; - } else { - throw new Error(result.Err); - } - } - - async sendDirectMessage( - actor: ActorSubclass<_SERVICE>, - receiver: Principal, - message: UserMessage - ): Promise<{ chatMessageId: bigint }> { - const result = await actor.send_direct_message(message, receiver); - console.log( - `sendDirectMessage: ${stringifyBigInt(message)} to ${receiver.toText()} with result ${stringifyBigInt(result)}` - ); - if ('Ok' in result) { - return { chatMessageId: result.Ok }; - } else { - throw new Error(result.Err); - } - } - - async sendGroupMessage( - actor: ActorSubclass<_SERVICE>, - groupChatId: bigint, - message: UserMessage - ): Promise<{ chatMessageId: bigint }> { - const result = await actor.send_group_message(message, groupChatId); - console.log( - `sendGroupMessage: ${stringifyBigInt(message)} to ${groupChatId} with result ${stringifyBigInt(result)}` - ); - if ('Ok' in result) { - return { chatMessageId: result.Ok }; - } else { - throw new Error(result.Err); - } - } - - async getChatIdsAndCurrentNumbersOfMessages( - actor: ActorSubclass<_SERVICE> - ): Promise<{ chatId: ChatId; numMessages: bigint }[]> { - const chatIds = await actor.get_my_chat_ids(); - return chatIds.map(([chatId, numMessages]) => { - return { chatId, numMessages }; - }); - } - - async getLatestVetKeyEpochMetadata( - actor: ActorSubclass<_SERVICE>, - chatId: ChatId - ): Promise { - const metadata = await actor.get_latest_chat_vetkey_epoch_metadata(chatId); - console.log( - `getLatestVetKeyEpochMetadata: ${stringifyBigInt(chatId)} with result ${stringifyBigInt(metadata)}` - ); - if ('Ok' in metadata) { - return metadata.Ok; - } else { - throw new Error(metadata.Err); - } - } - - async getVetKeyEpochMetadata( - actor: ActorSubclass<_SERVICE>, - chatId: ChatId, - vetKeyEpoch: bigint - ): Promise { - const metadata = await actor.get_vetkey_epoch_metadata(chatId, vetKeyEpoch); - console.log( - `getLatestVetKeyEpochMetadata: ${stringifyBigInt(chatId)} with result ${stringifyBigInt(metadata)}` - ); - if ('Ok' in metadata) { - return metadata.Ok; - } else { - throw new Error(metadata.Err); - } - } - - async getDerivedPublicKey( - actor: ActorSubclass<_SERVICE>, - chatId: ChatId, - vetKeyEpoch: bigint - ): Promise { - const bytes = await actor.chat_public_key(chatId, vetKeyEpoch); - console.log( - `getDerivedPublicKey: ${stringifyBigInt(chatId)} with result ${stringifyBigInt(bytes)}` - ); - return DerivedPublicKey.deserialize(Uint8Array.from(bytes)); - } - - async getVetKey( - actor: ActorSubclass<_SERVICE>, - chatId: ChatId, - vetKeyEpoch: bigint - ): Promise { - const tsk = TransportSecretKey.random(); - const result = await actor.derive_chat_vetkey(chatId, [vetKeyEpoch], tsk.publicKeyBytes()); - console.log(`getVetKey: ${stringifyBigInt(chatId)} with result ${stringifyBigInt(result)}`); - if ('Ok' in result) { - const encryptedVetKey = EncryptedVetKey.deserialize(Uint8Array.from(result.Ok)); - const derivedPublicKey = await this.getDerivedPublicKey(actor, chatId, vetKeyEpoch); - const vetKey = encryptedVetKey.decryptAndVerify(tsk, derivedPublicKey, new Uint8Array()); - return vetKey; - } else { - throw new Error(result.Err); - } - } - - getRatchetStats(): SymmetricRatchetStats { - const now = Date.now(); - const last = new Date(now - 1000 * 60 * 60 * Math.random() * 24); - const next = new Date(now + 1000 * 60 * 60 * Math.random() * 24); - return { - currentEpoch: Math.floor(Math.random() * 30) + 1, - messagesInCurrentEpoch: Math.floor(Math.random() * 200), - lastRotation: last, - nextScheduledRotation: next - }; - } - - async fetchEncryptedMessages( - actor: ActorSubclass<_SERVICE>, - chatId: ChatId, - startId: bigint, - limit: bigint | undefined - ): Promise { - const result = await actor.get_messages(chatId, startId, limit ? [Number(limit)] : []); - console.log( - `fetchEncryptedMessages: ${stringifyBigInt(chatId)} from ${startId.toString()} with limit ${limit} with result ${stringifyBigInt(result)}` - ); - return result; - } -} - -export const canisterAPI = new CanisterAPI(); diff --git a/rust/vetkeys/encrypted_chat/frontend/src/lib/services/chatStorage.ts b/rust/vetkeys/encrypted_chat/frontend/src/lib/services/chatStorage.ts deleted file mode 100644 index e2d6564b8..000000000 --- a/rust/vetkeys/encrypted_chat/frontend/src/lib/services/chatStorage.ts +++ /dev/null @@ -1,186 +0,0 @@ -import { get, set, del, clear, keys } from 'idb-keyval'; -import type { Message, Chat, UserConfig } from '../types'; -import { storagePrefixes } from '../types'; -import * as cbor from 'cbor-x'; -import { fromHex, toHex } from '$lib/utils'; -import { Principal } from '@dfinity/principal'; - -// IndexedDB storage service for persistent chat data -export class ChatStorageService { - async saveMessage(message: Message): Promise { - console.log( - `ChatStorageService: Saving from chat ${message.chatId} message ${message.messageId} to indexedDB` - ); - - const encodedMessage = toHex(cbor.encode(message) as Uint8Array); - - await set([storagePrefixes.MESSAGE_PREFIX, message.chatId, message.messageId], encodedMessage); - } - - async getMessages(chatId: string): Promise { - const allKeys = await keys(); - const chatMessageKeys = allKeys.filter( - (key) => Array.isArray(key) && key[0] === storagePrefixes.MESSAGE_PREFIX && key[1] === chatId - ); - if (chatMessageKeys.length === 0) { - console.log(`ChatStorageService: No messages found in indexedDB for chat ${chatId}`); - } else { - console.log( - `ChatStorageService: Loaded ${chatMessageKeys.length} messages in indexedDB for chat ${chatId}` - ); - } - - const messages: Message[] = []; - for (const key of chatMessageKeys) { - const encodedMessage = (await get(key)) as string; - if (!encodedMessage) { - console.error('ChatStorageService: Failed to get encoded message from indexedDB'); - continue; - } - const message = cbor.decode(fromHex(encodedMessage)) as Message; - if (message) { - // Ensure timestamp is a Date object - if (typeof message.timestamp === 'string') { - message.timestamp = new Date(message.timestamp); - } - messages.push(message); - } - } - - // Sort by timestamp - return messages.sort((a, b) => a.timestamp.getTime() - b.timestamp.getTime()); - } - - async deleteMessage(chatId: string, messageId: string): Promise { - await del([storagePrefixes.MESSAGE_PREFIX, chatId, messageId]); - } - - async containsMessage(chatId: string, messageId: string): Promise { - return (await keys()).some( - (key) => - Array.isArray(key) && - key[0] === storagePrefixes.MESSAGE_PREFIX && - key[1] === chatId && - key[2] === messageId - ); - } - - // Chat metadata storage - async saveChat(chat: Chat): Promise { - console.log(`ChatStorageService: Saving chat ${chat.idStr} to indexedDB`); - // example of the JSON encoding is - const value = JSON.stringify(chat, (key, value) => { - if (value instanceof Principal) { - const principal = { - __principal__: true, - value: value.toText() - }; - return principal; - } - return value as unknown; - }); - if (!value) { - throw new Error('ChatStorageService: Failed to stringify chat'); - } - await set([storagePrefixes.CHAT_PREFIX, chat.idStr], value); - } - - async deleteChat(chatId: string): Promise { - await del([storagePrefixes.CHAT_PREFIX, chatId]); - } - - async getAllChats(): Promise { - const allKeys = await keys(); - const chatKeys = allKeys.filter((key) => { - console.log( - 'getAllChats: key', - key, - Array.isArray(key) && key[0] === storagePrefixes.CHAT_PREFIX - ); - return Array.isArray(key) && key[0] === storagePrefixes.CHAT_PREFIX; - }); - - console.log(`ChatStorageService: Getting ${chatKeys.length} chats from indexedDB`); - - const chats: Chat[] = []; - for (const key of chatKeys) { - const chatStr = (await get(key)) as string; - if (chatStr) { - console.log('getAllChats: getting key', key, ' with value ', chatStr); - chats.push( - JSON.parse(chatStr, (key, value) => { - if (typeof value === 'object' && value !== null && '__principal__' in value) { - // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access - const principal = Principal.fromText(value.__principal__ as string); - return principal; - } - return value as unknown; - }) as Chat - ); - } - } - return chats; - } - - // User configuration - async saveUserConfig(config: UserConfig): Promise { - console.log(`ChatStorageService: Saving user config to indexedDB`); - await set([storagePrefixes.CONFIG_KEY], config); - } - - async getUserConfig(): Promise { - console.log(`ChatStorageService: Getting user config from indexedDB`); - return (await get([storagePrefixes.CONFIG_KEY])) || null; - } - - getMyUserConfig(): UserConfig { - console.log(`ChatStorageService: Getting my user config from indexedDB`); - return { - cacheRetentionDays: 7, - userId: 'Me', - userName: 'You', - userAvatar: '👤' - }; - } - - // Disclaimer - async setDisclaimerDismissed(): Promise { - console.log(`ChatStorageService: Setting disclaimer dismissed to true in indexedDB`); - await set([storagePrefixes.DISCLAIMER_KEY], true); - } - - async isDisclaimerDismissed(): Promise { - console.log(`ChatStorageService: Getting disclaimer dismissed from indexedDB`); - return (await get([storagePrefixes.DISCLAIMER_KEY])) || false; - } - - // Cache cleanup based on user config - async cleanupUserCache(retentionDays: number): Promise { - console.log(`ChatStorageService: Cleaning up user cache for ${retentionDays} days`); - const cutoffDate = new Date(Date.now() - retentionDays * 24 * 60 * 60 * 1000); - const allKeys = await keys(); - - // Clean up old message keys - for (const key of allKeys) { - if (typeof key === 'string' && key.startsWith(storagePrefixes.MESSAGE_PREFIX)) { - const message = (await get(key)) as Message; - if (message && new Date(message.timestamp) < cutoffDate) { - await del(key); - } - } - } - } - - async discardCacheCompletely(): Promise { - console.log(`ChatStorageService: Discarding cache completely`); - await clear(); - } - - // Clear all data (for testing/reset) - async clearAllData(): Promise { - console.log(`ChatStorageService: Clearing all data from indexedDB`); - await clear(); - } -} - -export const chatStorageService = new ChatStorageService(); diff --git a/rust/vetkeys/encrypted_chat/frontend/src/lib/services/encryptedCanisterCacheService.ts b/rust/vetkeys/encrypted_chat/frontend/src/lib/services/encryptedCanisterCacheService.ts deleted file mode 100644 index a46b21e18..000000000 --- a/rust/vetkeys/encrypted_chat/frontend/src/lib/services/encryptedCanisterCacheService.ts +++ /dev/null @@ -1,230 +0,0 @@ -import { - chatIdToString, - stringifyBigInt, - uBigIntTo8ByteUint8ArrayBigEndian, - u8ByteUint8ArrayBigEndianToUBigInt -} from '$lib/utils'; -import { - EncryptedMaps, - type AccessRights, - type ByteBuf, - type EncryptedMapData, - type EncryptedMapsClient -} from '@dfinity/vetkeys/encrypted_maps'; -import type { ChatId } from '../../declarations/encrypted_chat/encrypted_chat.did'; -import type { Principal } from '@dfinity/principal'; -import { getActor, getMyPrincipal } from '$lib/stores/auth.svelte'; - -export class EncryptedCanisterCacheService { - #encryptedMaps: EncryptedMaps; - - constructor() { - this.#encryptedMaps = new EncryptedMaps(new EncryptedMapsClientForEncryptedCache()); - } - - async fetchAndDecryptFor( - chatId: ChatId, - vetKeyEpoch: bigint - ): Promise<{ keyBytes: Uint8Array; symmetricKeyEpoch: bigint }> { - console.log( - `get_my_symmetric_key_cache: chatId=${chatIdToString(chatId)} vetKeyEpoch=${vetKeyEpoch.toString()}` - ); - const keyCacheBytes = await getActor().get_my_symmetric_key_cache(chatId, vetKeyEpoch); - if ('Err' in keyCacheBytes) { - throw new Error('Failed to get key cache bytes: ' + keyCacheBytes.Err); - } else if (keyCacheBytes.Ok.length === 0) { - throw new Error('Failed to get key cache bytes: no key cache found'); - } - - const mapName_ = mapName(); - const mapKey = await mapKeyId(chatId, vetKeyEpoch); - const decryptedBytes = await this.#encryptedMaps.decryptFor( - getMyPrincipal(), - mapName_, - mapKey, - new Uint8Array(keyCacheBytes.Ok[0]) - ); - - console.log( - `VetKeyEncryptedCache: successfully fetched and decrypted key cache for chatId=${chatIdToString(chatId)} vetKeyEpoch=${vetKeyEpoch.toString()}: ${stringifyBigInt(deserializeCache(decryptedBytes))}` - ); - return deserializeCache(decryptedBytes); - } - - async encryptAndStoreFor( - chatId: ChatId, - vetKeyEpoch: bigint, - cache: { keyBytes: Uint8Array; symmetricKeyEpoch: bigint } - ): Promise { - console.log('encryptAndStoreFor: starting to store the root key in cache: ', cache); - const mapName_ = mapName(); - const mapKey = await mapKeyId(chatId, vetKeyEpoch); - - const ciphertext = await this.#encryptedMaps.encryptFor( - getMyPrincipal(), - mapName_, - mapKey, - serializeCache(cache) - ); - const result = await getActor().update_my_symmetric_key_cache(chatId, vetKeyEpoch, ciphertext); - if ('Err' in result) { - throw new Error('Failed to update key cache: ' + result.Err); - } else { - console.log( - `VetKeyEncryptedCache: successfully stored key cache for chatId=${chatIdToString(chatId)} vetKeyEpoch=${vetKeyEpoch.toString()}: ${stringifyBigInt(serializeCache(cache))}` - ); - } - } -} - -function serializeCache(cache: { keyBytes: Uint8Array; symmetricKeyEpoch: bigint }): Uint8Array { - return new Uint8Array([ - ...cache.keyBytes, - ...uBigIntTo8ByteUint8ArrayBigEndian(cache.symmetricKeyEpoch) - ]); -} - -function deserializeCache(data: Uint8Array): { keyBytes: Uint8Array; symmetricKeyEpoch: bigint } { - return { - keyBytes: data.slice(0, 32), - symmetricKeyEpoch: u8ByteUint8ArrayBigEndianToUBigInt(data.slice(32)) - }; -} - -/* eslint-disable @typescript-eslint/no-unused-vars */ -class EncryptedMapsClientForEncryptedCache implements EncryptedMapsClient { - constructor() {} - - get_accessible_shared_map_names(): Promise<[Principal, ByteBuf][]> { - throw Error('unavailable EncryptedMaps function get_accessible_shared_map_names'); - } - - get_shared_user_access_for_map( - owner: Principal, - mapName: ByteBuf - ): Promise<{ Ok: Array<[Principal, AccessRights]> } | { Err: string }> { - throw Error('unavailable EncryptedMaps function get_shared_user_access_for_map'); - } - - get_owned_non_empty_map_names(): Promise> { - throw Error('unavailable EncryptedMaps function get_owned_non_empty_map_names'); - } - - get_all_accessible_encrypted_values(): Promise<[[Principal, ByteBuf], [ByteBuf, ByteBuf][]][]> { - throw Error('unavailable EncryptedMaps function get_all_accessible_encrypted_values'); - } - - get_all_accessible_encrypted_maps(): Promise> { - throw Error('unavailable EncryptedMaps function get_all_accessible_encrypted_maps'); - } - - get_encrypted_value( - mapOwner: Principal, - mapName: ByteBuf, - - mapKey: ByteBuf - ): Promise<{ Ok: [] | [ByteBuf] } | { Err: string }> { - throw Error('unavailable EncryptedMaps function get_encrypted_value'); - } - - get_encrypted_values_for_map( - mapOwner: Principal, - mapName: ByteBuf - ): Promise<{ Ok: Array<[ByteBuf, ByteBuf]> } | { Err: string }> { - throw Error('unavailable EncryptedMaps function get_encrypted_values_for_map'); - } - - async get_encrypted_vetkey( - mapOwner: Principal, - mapName: ByteBuf, - transportKey: ByteBuf - ): Promise<{ Ok: ByteBuf } | { Err: string }> { - const data = new Uint8Array( - await getActor().get_encrypted_vetkey_for_my_cache_storage(transportKey.inner) - ); - const result: { Ok: ByteBuf } | { Err: string } = { - Ok: { inner: data } - }; - return Promise.resolve(result); - } - - insert_encrypted_value( - mapOwner: Principal, - mapName: ByteBuf, - mapKey: ByteBuf, - data: ByteBuf - ): Promise<{ Ok: [] | [ByteBuf] } | { Err: string }> { - throw Error('unavailable EncryptedMaps function insert_encrypted_value'); - } - - remove_encrypted_value( - mapOwner: Principal, - mapName: ByteBuf, - mapKey: ByteBuf - ): Promise<{ Ok: [] | [ByteBuf] } | { Err: string }> { - throw Error('unavailable EncryptedMaps function remove_encrypted_value'); - } - - remove_map_values( - mapOwner: Principal, - mapName: ByteBuf - ): Promise<{ Ok: Array } | { Err: string }> { - throw Error('unavailable EncryptedMaps function remove_map_values'); - } - - async get_vetkey_verification_key(): Promise { - return { inner: await getActor().get_vetkey_verification_key_for_my_cache_storage() }; - } - - set_user_rights( - owner: Principal, - mapName: ByteBuf, - user: Principal, - userRights: AccessRights - ): Promise<{ Ok: [] | [AccessRights] } | { Err: string }> { - throw Error('unavailable EncryptedMaps function set_user_rights'); - } - - get_user_rights( - owner: Principal, - mapName: ByteBuf, - user: Principal - ): Promise<{ Ok: [] | [AccessRights] } | { Err: string }> { - throw Error('unavailable EncryptedMaps function get_user_rights'); - } - - remove_user( - owner: Principal, - mapName: ByteBuf, - user: Principal - ): Promise<{ Ok: [] | [AccessRights] } | { Err: string }> { - throw Error('unavailable EncryptedMaps function remove_user'); - } -} -/* eslint-enable @typescript-eslint/no-unused-vars */ - -function mapName(): Uint8Array { - return new TextEncoder().encode('encrypted_chat_cache'); -} - -async function mapKeyId(chat_id: ChatId, vetkey_epoch_id: bigint): Promise { - const input = serializeChatId(chat_id); - - const hashBuffer = await crypto.subtle.digest( - 'SHA-256', - new Uint8Array([...input, ...uBigIntTo8ByteUint8ArrayBigEndian(vetkey_epoch_id)]) - ); - return new Uint8Array(hashBuffer); -} - -function serializeChatId(chatId: ChatId): Uint8Array { - if ('Direct' in chatId) { - return new Uint8Array([ - 0, - ...chatId.Direct[0].toUint8Array(), - ...chatId.Direct[1].toUint8Array() - ]); - } else { - return new Uint8Array([1, ...uBigIntTo8ByteUint8ArrayBigEndian(chatId.Group)]); - } -} diff --git a/rust/vetkeys/encrypted_chat/frontend/src/lib/services/encryptedMessagingService.ts b/rust/vetkeys/encrypted_chat/frontend/src/lib/services/encryptedMessagingService.ts deleted file mode 100644 index c3fa46896..000000000 --- a/rust/vetkeys/encrypted_chat/frontend/src/lib/services/encryptedMessagingService.ts +++ /dev/null @@ -1,448 +0,0 @@ -import { auth, getActor, getMyPrincipal } from '$lib/stores/auth.svelte'; -import type { ActorSubclass } from '@dfinity/agent'; -import type { - _SERVICE, - ChatId, - EncryptedMessage, - EncryptedMessageMetadata -} from '../../declarations/encrypted_chat/encrypted_chat.did'; -import { KeyManager } from '$lib/crypto/keyManager'; -import { RatchetInitializationService } from './ratchetInitializationService'; -import { SymmetricRatchetEpochError, VetKeyEpochError, type Message } from '$lib/types'; -import { canisterAPI } from './canisterApi'; -import { - chatIdFromString, - chatIdsNumMessagesToSummary, - chatIdToString, - randomNonce -} from '$lib/utils'; -import * as cbor from 'cbor-x'; -import type { SymmetricRatchetState } from '$lib/crypto/symmetricRatchet'; - -type MessageContent = { - textContent: string; - fileData?: { name: string; size: number; type: string; data: Uint8Array }; -}; - -export class EncryptedMessagingService { - #ratchetInitializationService: RatchetInitializationService; - #keyManager: KeyManager; - - #sendingQueue: Map; - - #receivingQueue: Map; - #receivingQueueToDecrypt: Map; - #chatIdToCurrentNumberOfRemoteMessages: Map; - #chatIdToCurrentNumberOfFetchedMessages: Map; - - #backgroundWorker: BackgroundWorker; - - constructor() { - this.#ratchetInitializationService = new RatchetInitializationService(); - this.#keyManager = new KeyManager(); - - // Initialize sending and receiving queues - this.#sendingQueue = new Map(); - this.#receivingQueue = new Map(); - this.#receivingQueueToDecrypt = new Map(); - - this.#chatIdToCurrentNumberOfRemoteMessages = new Map(); - this.#chatIdToCurrentNumberOfFetchedMessages = new Map(); - - // Start the background worker to handle encryption, sending, polling, and decryption - this.#backgroundWorker = new BackgroundWorker(); - } - - start() { - // Start the worker loop - // The worker will periodically: - // 1. Take outgoing messages from the sending queue, encrypt using RatchetInitializationService, and send via the actor. - // 2. Poll for new encrypted messages from the canister, queue them for decryption. - // 3. Decrypt received messages using RatchetInitializationService and put them into the receiving queue. - void this.#backgroundWorker.start( - async () => { - await this.#pollForNewMessages(); - await this.#decryptReceivedMessages(); - }, - async () => this.#handleOutgoingMessages() - ); - } - - inductSymmetricRatchetState( - chatIdStr: string, - vetKeyEpoch: bigint, - symmetricRatchetState: SymmetricRatchetState - ) { - this.#keyManager.inductSymmetricRatchetState(chatIdStr, vetKeyEpoch, symmetricRatchetState); - } - - skipMessagesAvailableLocally(chatId: ChatId, numMessages: bigint) { - console.log( - 'skipMessagesAvailableLocally: chatId', - chatIdToString(chatId), - 'numMessages', - numMessages.toString() - ); - this.#chatIdToCurrentNumberOfFetchedMessages.set(chatIdToString(chatId), numMessages); - } - - getCurrentChatIds(): ChatId[] { - return this.#keyManager.getCurrentChatIdStrs().map(chatIdFromString); - } - - enqueueSendMessage(chatId: ChatId, content: Uint8Array) { - this.#sendingQueue.set(chatIdToString(chatId), [ - ...(this.#sendingQueue.get(chatIdToString(chatId)) || []), - content - ]); - } - - takeReceivedMessages(): Map { - const messages = this.#receivingQueue; - this.#receivingQueue = new Map(); - return messages; - } - - signalStopWorker() { - this.#backgroundWorker.abortController.abort(); - } - - /** - * Handle outgoing messages: encrypt and send - */ - async #handleOutgoingMessages(): Promise { - for (const [chatId, contents] of this.#sendingQueue.entries()) { - while (true) { - const content = contents.shift(); - if (!content) { - break; - } - await this.#handleOutgoingMessage(chatId, content); - } - this.#sendingQueue.delete(chatId); - } - } - - async #handleOutgoingMessage(chatIdStr: string, content: Uint8Array) { - const MAX_RETRIES = 50; - const TIMEOUT_MS = 1000; - - const nonce = randomNonce(); - for (let i = 0; i < MAX_RETRIES; i++) { - try { - const encrypted = await this.#keyManager.encryptNow( - chatIdStr, - getMyPrincipal(), - nonce, - content - ); - await sendMessage( - getActor(), - chatIdFromString(chatIdStr), - encrypted.vetKeyEpoch, - encrypted.symmetricRatchetEpoch, - nonce, - encrypted.encryptedBytes - ); - break; - } catch (e) { - console.info('#handleOutgoingMessage: VetKeyEpochError', e); - if (e instanceof VetKeyEpochError) { - const ratchetState = - await this.#ratchetInitializationService.initializeRatchetStateAndReshareAndCacheIfNeeded( - chatIdFromString(chatIdStr), - e.requiredVetKeyEpoch - ); - this.#keyManager.inductSymmetricRatchetState( - chatIdStr, - e.requiredVetKeyEpoch, - ratchetState - ); - } else if (e instanceof SymmetricRatchetEpochError) { - console.log('#handleOutgoingMessage: Symmetric ratchet epoch error', e); - } else { - // Errors in sending are non-fatal. - console.error('#handleOutgoingMessage: Unknown error', e); - } - await new Promise((resolve) => setTimeout(resolve, TIMEOUT_MS)); - } - } - } - - /** - * Poll for new messages from canister - */ - async #pollForNewMessages(): Promise { - if (auth.state.label !== 'initialized') return; - - // Get chat IDs and check for new messages - const chatIds = await canisterAPI.getChatIdsAndCurrentNumbersOfMessages(getActor()); - - const summary = chatIdsNumMessagesToSummary(chatIds); - console.log('fetched ' + chatIds.length + ' chats: ' + summary); - - for (const { chatId, numMessages } of chatIds) { - if (!this.#keyManager.doesChatHaveKeys(chatIdToString(chatId))) { - console.log( - '#pollForNewMessages: chatId', - chatIdToString(chatId), - 'does not have keys, initializing' - ); - const latestVetKeyEpoch = ( - await canisterAPI.getLatestVetKeyEpochMetadata(getActor(), chatId) - ).epoch_id; - const ratchetState = - await this.#ratchetInitializationService.initializeRatchetStateAndReshareAndCacheIfNeeded( - chatId, - latestVetKeyEpoch - ); - this.#keyManager.inductSymmetricRatchetState( - chatIdToString(chatId), - latestVetKeyEpoch, - ratchetState - ); - } - - const currentNumberOfFetchedMessages = - this.#chatIdToCurrentNumberOfFetchedMessages.get(chatIdToString(chatId)) ?? 0n; - this.#chatIdToCurrentNumberOfRemoteMessages.set(chatIdToString(chatId), numMessages); - - console.log( - `#pollForNewMessages: new messages for chatId: ${chatIdToString(chatId)}, currentNumberOfFetchedMessages: ${currentNumberOfFetchedMessages}, numMessages: ${numMessages}` - ); - - // Get messages starting from the last known message ID - const startId = 0n + currentNumberOfFetchedMessages; - - try { - const messages = await canisterAPI.fetchEncryptedMessages( - getActor(), - chatId, - startId, - undefined - ); - - console.log( - `#pollForNewMessages: fetched ${messages.length} messages for chatId ${chatIdToString( - chatId - )}, currentNumberOfFetchedMessages: ${currentNumberOfFetchedMessages}, numMessages: ${numMessages}, new fetched messages count: ${currentNumberOfFetchedMessages + BigInt(messages.length)}` - ); - - console.log( - '#pollForNewMessages: old map this.#chatIdToCurrentNumberOfFetchedMessages ', - this.#chatIdToCurrentNumberOfFetchedMessages - ); - - this.#chatIdToCurrentNumberOfFetchedMessages.set( - chatIdToString(chatId), - currentNumberOfFetchedMessages + BigInt(messages.length) - ); - - console.log( - '#pollForNewMessages: new map this.#chatIdToCurrentNumberOfFetchedMessages ', - this.#chatIdToCurrentNumberOfFetchedMessages - ); - - this.#receivingQueueToDecrypt.set(chatIdToString(chatId), [ - ...(this.#receivingQueueToDecrypt.get(chatIdToString(chatId)) || []), - ...messages - ]); - } catch (error) { - // Polling errors are non-fatal if some messages are too big to receive several at once, - // and polling just one message works - console.info( - 'Failed to poll for new messages, trying again to pull just one message...', - error - ); - const messages = await canisterAPI.fetchEncryptedMessages(getActor(), chatId, startId, 1n); - - this.#chatIdToCurrentNumberOfFetchedMessages.set( - chatIdToString(chatId), - currentNumberOfFetchedMessages + BigInt(messages.length) - ); - - this.#receivingQueueToDecrypt.set(chatIdToString(chatId), [ - ...(this.#receivingQueueToDecrypt.get(chatIdToString(chatId)) || []), - ...messages - ]); - } - } - } - - /** - * Decrypt received messages and put into receiving queue - */ - async #decryptReceivedMessages(): Promise { - if (this.#receivingQueueToDecrypt.size !== 0) { - console.log( - '#decryptReceivedMessages: decrypting', - this.#receivingQueueToDecrypt.size, - 'messages' - ); - } - for (const [chatIdStr, encryptedMessages] of this.#receivingQueueToDecrypt.entries()) { - for (const encryptedMessage of encryptedMessages) { - const decrypted = await this.#decryptMessage(chatIdStr, encryptedMessage); - this.#receivingQueueToDecrypt.get(chatIdStr)?.shift(); - this.#receivingQueue.set(chatIdStr, [ - ...(this.#receivingQueue.get(chatIdStr) || []), - decrypted - ]); - } - this.#receivingQueueToDecrypt.delete(chatIdStr); - } - } - - async #decryptMessage(chatIdStr: string, encryptedMessage: EncryptedMessage): Promise { - console.log( - '#decryptMessage: decrypting', - encryptedMessage.metadata.chat_message_id.toString(), - 'for chatId', - chatIdStr - ); - for (let i = 0; i < 2; i++) { - try { - const decrypted = await this.#keyManager.decryptAtTimeAndEvolveIfNeeded( - chatIdStr, - encryptedMessage.metadata.sender, - encryptedMessage.metadata.nonce, - encryptedMessage.metadata.vetkey_epoch, - new Uint8Array(encryptedMessage.content), - new Date(Number(encryptedMessage.metadata.timestamp / 1_000_000n)) - ); - - return this.#parseMessage(chatIdStr, encryptedMessage.metadata, decrypted); - } catch (error) { - if ( - i === 0 && - !this.#keyManager.doesChatHaveRatchetStateForEpoch( - chatIdStr, - encryptedMessage.metadata.vetkey_epoch - ) - ) { - console.info( - `#decryptMessage: Failed to decrypt message ${encryptedMessage.metadata.chat_message_id.toString()}, trying again... Caught error: ${error instanceof Error ? error.message : 'Unknown error'}` - ); - const ratchetState = - await this.#ratchetInitializationService.initializeRatchetStateAndReshareAndCacheIfNeeded( - chatIdFromString(chatIdStr), - encryptedMessage.metadata.vetkey_epoch - ); - this.#keyManager.inductSymmetricRatchetState( - chatIdStr, - encryptedMessage.metadata.vetkey_epoch, - ratchetState - ); - } else { - throw error; - } - } - } - - throw Error('unreachable code'); - } - - #parseMessage( - chatIdStr: string, - metadata: EncryptedMessageMetadata, - decrypted: Uint8Array - ): Message { - const messageContent = cbor.decode(decrypted) as MessageContent; - - return { - messageId: metadata.chat_message_id.toString(), - chatId: chatIdStr, - senderId: metadata.sender.toText(), - content: messageContent.textContent, - timestamp: new Date(Number(metadata.timestamp / 1_000_000n)), - fileData: messageContent.fileData, - vetkeyEpoch: Number(metadata.vetkey_epoch), - symmetricRatchetEpoch: Number(metadata.symmetric_key_epoch) - }; - } -} - -class BackgroundWorker { - abortController: AbortController; - - constructor() { - this.abortController = new AbortController(); - } - - async start( - receiverFunction: () => Promise, - senderFunction: () => Promise - ): Promise { - const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms)); - - const run = async (fn: () => Promise) => { - while (!this.abortController.signal.aborted) { - try { - await fn(); - } catch (error) { - console.error('Background worker error:', error); - } - if (this.abortController.signal.aborted) break; - await sleep(500); - } - }; - - const tasks = [run(receiverFunction), run(senderFunction)]; - - if (this.abortController.signal.aborted) { - await Promise.all(tasks); - return; - } - - // Resolve after an abort signal and after both loops finish - await new Promise((resolve) => { - this.abortController.signal.addEventListener('abort', () => resolve(), { once: true }); - }); - - await Promise.all(tasks); - } -} - -async function sendMessage( - actor: ActorSubclass<_SERVICE>, - chatId: ChatId, - vetKeyEpoch: bigint, - symmetricRatchetEpoch: bigint, - nonce: bigint, - encryptedBytes: Uint8Array -) { - // Create UserMessage for the canister - const userMessage = { - vetkey_epoch: vetKeyEpoch, - content: encryptedBytes, - symmetric_key_epoch: symmetricRatchetEpoch, - nonce: nonce - }; - - // Send to canister using the appropriate method based on chat type - try { - if ('Direct' in chatId) { - const receiver = - getMyPrincipal().toText() === chatId.Direct[0].toText() - ? chatId.Direct[1] - : chatId.Direct[0]; - await canisterAPI.sendDirectMessage(actor, receiver, userMessage); - } else { - await canisterAPI.sendGroupMessage(actor, chatId.Group, userMessage); - } - } catch (e) { - if (e instanceof Error && e.message.toLowerCase().includes('wrong vetkey epoch')) { - throw new VetKeyEpochError( - e.message, - (await canisterAPI.getLatestVetKeyEpochMetadata(actor, chatId)).epoch_id - ); - } else if ( - e instanceof Error && - e.message.toLowerCase().includes('wrong symmetric ratchet epoch') - ) { - throw new SymmetricRatchetEpochError(e.message); - } else { - throw e; - } - } -} diff --git a/rust/vetkeys/encrypted_chat/frontend/src/lib/services/keyStorage.ts b/rust/vetkeys/encrypted_chat/frontend/src/lib/services/keyStorage.ts deleted file mode 100644 index 06ee28f42..000000000 --- a/rust/vetkeys/encrypted_chat/frontend/src/lib/services/keyStorage.ts +++ /dev/null @@ -1,89 +0,0 @@ -import { - SymmetricRatchetState, - type StorableSymmetricRatchetState as StorableSymmetricRatchetState -} from '$lib/crypto/symmetricRatchet'; -import { storagePrefixes } from '../types'; -import { get, keys, set } from 'idb-keyval'; - -// IndexedDB storage service for persistent key data -export class KeyStorageService { - async getSymmetricRatchetState( - chatIdStr: string, - vetKeyEpochStr: string - ): Promise { - console.log( - `KeyStorageService: Getting key state for chat ${chatIdStr} vetkeyEpoch ${vetKeyEpochStr}` - ); - const stateRecord = (await get([ - storagePrefixes.CHAT_EPOCH_KEY_PREFIX, - chatIdStr, - vetKeyEpochStr - ])) as StorableSymmetricRatchetState; - if (!stateRecord) { - return undefined; - } - console.log( - `KeyStorageService.getSymmetricRatchetState: Got symmetric ratchet state for chat ${chatIdStr} vetkeyEpoch ${vetKeyEpochStr}: state`, - stateRecord - ); - return new SymmetricRatchetState( - stateRecord.cryptoKey, - stateRecord.symmetricRatchetEpoch, - stateRecord.creationTime, - stateRecord.rotationDuration - ); - } - - async saveSymmetricRatchetState( - chatIdStr: string, - vetKeyEpochStr: string, - state: SymmetricRatchetState - ) { - console.log( - `KeyStorageService: Saving key state for chat ${chatIdStr} vetkeyEpoch ${vetKeyEpochStr}: state`, - state - ); - await set( - [storagePrefixes.CHAT_EPOCH_KEY_PREFIX, chatIdStr, vetKeyEpochStr], - state.toStorable() - ); - } - - async getAllSymmetricRatchetStates(): Promise< - { chatIdStr: string; vetKeyEpoch: bigint; state: SymmetricRatchetState }[] - > { - console.log(`KeyStorageService: Getting all symmetric key states`); - const allKeys = await keys(); - const symmetricKeyStates: { - chatIdStr: string; - vetKeyEpoch: bigint; - state: SymmetricRatchetState; - }[] = []; - for (const key of allKeys) { - console.log(`KeyStorageService: getAllSymmetricRatchetStates key`, key); - if (Array.isArray(key) && key[0] === storagePrefixes.CHAT_EPOCH_KEY_PREFIX) { - const state = await this.getSymmetricRatchetState(key[1] as string, key[2] as string); - if (state) { - symmetricKeyStates.push({ - chatIdStr: key[1] as string, - vetKeyEpoch: BigInt(key[2] as string), - state - }); - } - } - } - return symmetricKeyStates; - } - - async saveIbeDecryptionKey(keyBytes: Uint8Array) { - console.log(`KeyStorageService: Saving IBE decryption key`); - await set([storagePrefixes.CHAT_IBE_DECRYPTION_KEY_PREFIX], keyBytes); - } - - async getIbeDecryptionKey(): Promise { - console.log(`KeyStorageService: Getting IBE decryption key`); - return await get([storagePrefixes.CHAT_IBE_DECRYPTION_KEY_PREFIX]); - } -} - -export const keyStorageService = new KeyStorageService(); diff --git a/rust/vetkeys/encrypted_chat/frontend/src/lib/services/ratchetInitializationService.ts b/rust/vetkeys/encrypted_chat/frontend/src/lib/services/ratchetInitializationService.ts deleted file mode 100644 index 14c31e12d..000000000 --- a/rust/vetkeys/encrypted_chat/frontend/src/lib/services/ratchetInitializationService.ts +++ /dev/null @@ -1,222 +0,0 @@ -import { deriveRootKeyBytes, SymmetricRatchetState } from '$lib/crypto/symmetricRatchet'; -import type { ChatId } from '../../declarations/encrypted_chat/encrypted_chat.did'; -import { getActor, getMyPrincipal } from '$lib/stores/auth.svelte'; -import { stringifyBigInt, chatIdToString } from '$lib/utils'; -import { canisterAPI } from './canisterApi'; -import { keyStorageService } from './keyStorage'; -import { EncryptedCanisterCacheService } from './encryptedCanisterCacheService'; -import { VetKeyResharingService } from './vetKeyResharingService'; - -export class RatchetInitializationService { - #vetKeyResharingService: VetKeyResharingService; - #encryptedCanisterCacheService: EncryptedCanisterCacheService; - - constructor() { - this.#vetKeyResharingService = new VetKeyResharingService(); - this.#encryptedCanisterCacheService = new EncryptedCanisterCacheService(); - } - - async initializeRatchetStateAndReshareAndCacheIfNeeded( - chatId: ChatId, - vetKeyEpoch: bigint - ): Promise { - const metadata = await canisterAPI.getVetKeyEpochMetadata(getActor(), chatId, vetKeyEpoch); - const creationTime = new Date(Number(metadata.creation_timestamp / 1_000_000n)); - const rotationDuration = new Date( - Number(metadata.symmetric_key_rotation_duration / 1_000_000n) - ); - console.log( - `RatchetInitializationService.initializeRatchetStateAndReshareAndCacheIfNeeded: Initializing ratchet state for chat ${chatIdToString(chatId)} and vetKey epoch ${vetKeyEpoch.toString()} with creation time ${creationTime.toUTCString()} and rotation duration ${rotationDuration.toUTCString()}` - ); - - try { - return await this.cryptoKeyStateFromLocalStorage(chatId, vetKeyEpoch); - } catch (error) { - console.info( - `User doesn't have key in persistent storage for chat ${chatIdToString(chatId)} and vetKey epoch ${vetKeyEpoch.toString()}: `, - error - ); - } - - try { - const keyState = await this.cryptoKeyStateFromRemoteCache(chatId, vetKeyEpoch); - const symmetricRatchetState = new SymmetricRatchetState( - keyState.key, - keyState.symmetricKeyEpoch, - creationTime, - rotationDuration - ); - - keyStorageService - .saveSymmetricRatchetState( - chatIdToString(chatId), - vetKeyEpoch.toString(), - symmetricRatchetState - ) - .catch((error) => { - console.error( - `Failed to save key state for chat ${chatIdToString(chatId)} vetkeyEpoch ${vetKeyEpoch.toString()}: `, - error - ); - }); - return symmetricRatchetState; - } catch (error) { - console.info( - `User doesn't have key in remote cache for chat ${chatIdToString(chatId)} and vetKey epoch ${vetKeyEpoch.toString()}: `, - error - ); - } - - try { - const keyState = await this.cryptoKeyStateFromResharedVetKey(chatId, vetKeyEpoch); - return new SymmetricRatchetState( - keyState.key, - keyState.symmetricKeyEpoch, - creationTime, - rotationDuration - ); - } catch (error) { - console.info('Failed to fetch reshared IBE encrypted vetkey: ', error); - } - - try { - const keyState = await this.fetchAndReshareAndCacheVetKey(chatId, vetKeyEpoch); - const symmetricRatchetState = new SymmetricRatchetState( - keyState.key, - keyState.symmetricKeyEpoch, - creationTime, - rotationDuration - ); - keyStorageService - .saveSymmetricRatchetState( - chatIdToString(chatId), - vetKeyEpoch.toString(), - symmetricRatchetState - ) - .catch((error) => { - console.error( - `Failed to save key state for chat ${chatIdToString(chatId)} vetkeyEpoch ${vetKeyEpoch.toString()}: `, - error - ); - }); - return symmetricRatchetState; - } catch (error) { - console.info('Failed to fetch vetkey: ', error); - } - - throw new Error('Failed to initialize ratchet state'); - } - - async cryptoKeyStateFromLocalStorage( - chatId: ChatId, - vetKeyEpoch: bigint - ): Promise { - return keyStorageService - .getSymmetricRatchetState(chatIdToString(chatId), vetKeyEpoch.toString()) - .then((keyState) => { - if (keyState) { - console.log('Key state found in key storage: ', keyState); - return keyState; - } else { - console.log( - 'Key state not found in key storage: ', - chatIdToString(chatId), - vetKeyEpoch.toString() - ); - throw new Error('Key state not found in key storage'); - } - }); - } - - cryptoKeyStateFromRemoteCache( - chatId: ChatId, - vetKeyEpoch: bigint - ): Promise<{ key: CryptoKey; symmetricKeyEpoch: bigint }> { - return this.#encryptedCanisterCacheService - .fetchAndDecryptFor(chatId, vetKeyEpoch) - .then((epochKeyState) => { - return importKeyStateFromBytes(epochKeyState); - }); - } - - cryptoKeyStateFromResharedVetKey( - chatId: ChatId, - vetKeyEpoch: bigint - ): Promise<{ key: CryptoKey; symmetricKeyEpoch: bigint }> { - return this.#vetKeyResharingService - .fetchResharedIbeEncryptedVetKey(chatId, vetKeyEpoch) - .then((resharedVetKey) => { - console.log('successfully fetched reshared IBE encrypted vetkey: ', resharedVetKey); - return importKeyStateFromBytes( - deriveRootKeyAndDispatchCaching(chatId, vetKeyEpoch, resharedVetKey) - ); - }); - } - - async fetchAndReshareAndCacheVetKey( - chatId: ChatId, - vetKeyEpoch: bigint - ): Promise<{ key: CryptoKey; symmetricKeyEpoch: bigint }> { - const vetKey = await canisterAPI.getVetKey(getActor(), chatId, vetKeyEpoch); - while (true) { - console.log('waiting for vetKey epoch metadata in a loop'); - - const meta = await canisterAPI.getVetKeyEpochMetadata(getActor(), chatId, vetKeyEpoch); - - const otherParticipants = meta.participants.filter( - (p) => p.toString() !== getMyPrincipal().toString() - ); - - this.#vetKeyResharingService - .reshareIbeEncryptedVetKeys(chatId, vetKeyEpoch, otherParticipants, vetKey.signatureBytes()) - .catch((error) => { - console.error( - `Failed to reshare IBE encrypted vetkeys for chat ${chatIdToString(chatId)} vetkeyEpoch ${meta.epoch_id.toString()}: `, - error - ); - }); - - return await importKeyStateFromBytes( - deriveRootKeyAndDispatchCaching(chatId, vetKeyEpoch, vetKey.signatureBytes()) - ); - } - } -} - -function deriveRootKeyAndDispatchCaching( - chatId: ChatId, - vetKeyEpoch: bigint, - vetKeyBytes: Uint8Array -): { keyBytes: Uint8Array; symmetricKeyEpoch: bigint } { - const rootKey = deriveRootKeyBytes(vetKeyBytes); - console.log( - `Computed rootKey=${stringifyBigInt(rootKey)} from vetKey=${stringifyBigInt(vetKeyBytes)}` - ); - - console.log('starting to store the root key in cache: ', rootKey); - const vetKeyEncryptedCache = new EncryptedCanisterCacheService(); - const keyState = { keyBytes: rootKey, symmetricKeyEpoch: 0n }; - // await this future in background - vetKeyEncryptedCache.encryptAndStoreFor(chatId, vetKeyEpoch, keyState).catch((error) => { - console.error( - `Failed to store root key in cache for chat ${chatIdToString(chatId)} vetkeyEpoch ${vetKeyEpoch.toString()}: `, - error - ); - }); - return keyState; -} - -export async function importKeyStateFromBytes(params: { - keyBytes: Uint8Array; - symmetricKeyEpoch: bigint; -}): Promise<{ key: CryptoKey; symmetricKeyEpoch: bigint }> { - const exportable = false; - const key = await globalThis.crypto.subtle.importKey( - 'raw', - new Uint8Array(params.keyBytes), - 'HKDF', - exportable, - ['deriveKey', 'deriveBits'] - ); - return { key, symmetricKeyEpoch: params.symmetricKeyEpoch }; -} diff --git a/rust/vetkeys/encrypted_chat/frontend/src/lib/services/vetKeyResharingService.ts b/rust/vetkeys/encrypted_chat/frontend/src/lib/services/vetKeyResharingService.ts deleted file mode 100644 index 5e486b5ac..000000000 --- a/rust/vetkeys/encrypted_chat/frontend/src/lib/services/vetKeyResharingService.ts +++ /dev/null @@ -1,122 +0,0 @@ -import { chatIdToString } from '$lib/utils'; -import { - DerivedPublicKey, - IbeIdentity, - IbeSeed, - IbeCiphertext, - TransportSecretKey, - EncryptedVetKey -} from '@dfinity/vetkeys'; -import type { ChatId } from '../../declarations/encrypted_chat/encrypted_chat.did'; -import { keyStorageService } from './keyStorage'; -import type { Principal } from '@dfinity/principal'; -import { getActor, getMyPrincipal } from '$lib/stores/auth.svelte'; - -export class VetKeyResharingService { - constructor() {} - - async reshareIbeEncryptedVetKeys( - chatId: ChatId, - vetkeyEpoch: bigint, - otherParticipants: Principal[], - vetKeyBytes: Uint8Array - ): Promise { - if (otherParticipants.length > 0) { - console.log( - 'reshareIbeEncryptedVetkeys: ', - chatId, - vetkeyEpoch, - otherParticipants, - vetKeyBytes - ); - // try to reshare with other participants - return await Promise.all( - otherParticipants.map(async (p) => { - const ibePublicKey = DerivedPublicKey.deserialize( - new Uint8Array(await getActor().get_vetkey_resharing_ibe_encryption_key(p)) - ); - const ibeIdentity = IbeIdentity.fromBytes(new Uint8Array()); - const ibeSeed = IbeSeed.random(); - const ibeCiphertext = IbeCiphertext.encrypt( - ibePublicKey, - ibeIdentity, - vetKeyBytes, - ibeSeed - ); - const ibeCiphertextBytes = ibeCiphertext.serialize(); - const result: [Principal, Uint8Array] = [p, ibeCiphertextBytes]; - return result; - }) - ).then(async (ibeEncryptedVetKeysPromise) => { - await getActor() - .reshare_ibe_encrypted_vetkeys(chatId, vetkeyEpoch, ibeEncryptedVetKeysPromise) - .then((result) => { - if ('Ok' in result) { - console.log( - `Successfully resharded IBE encrypted vetkeys for chat ${chatIdToString(chatId)} vetkeyEpoch ${vetkeyEpoch.toString()}` - ); - } else { - console.info( - `Failed to reshare IBE encrypted vetkeys for chat ${chatIdToString(chatId)} vetkeyEpoch ${vetkeyEpoch.toString()}: `, - result.Err - ); - } - }); - }); - } else { - console.log('no other participants to reshare vetKey with'); - } - } - - async fetchResharedIbeEncryptedVetKey(chatId: ChatId, vetkeyEpoch: bigint): Promise { - console.log('fetchResharedIbeEncryptedVetKeys: ', chatId, vetkeyEpoch, getMyPrincipal()); - const tsk = TransportSecretKey.random(); - const myResharedIbeEncryptedVetkey = await getActor().get_my_reshared_ibe_encrypted_vetkey( - chatId, - vetkeyEpoch - ); - if ('Err' in myResharedIbeEncryptedVetkey) { - throw new Error( - 'Failed to get my reshared IBE encrypted vetkey: ' + myResharedIbeEncryptedVetkey.Err - ); - } else if (myResharedIbeEncryptedVetkey.Ok.length === 0) { - throw new Error('Failed to get my reshared IBE encrypted vetkey: no reshared vetkey'); - } - const ibeCiphertext = IbeCiphertext.deserialize( - new Uint8Array(myResharedIbeEncryptedVetkey.Ok[0]) - ); - - const publicIbeKeyBytes = - await getActor().get_vetkey_resharing_ibe_encryption_key(getMyPrincipal()); - const publicIbeKey = DerivedPublicKey.deserialize(new Uint8Array(publicIbeKeyBytes)); - - const maybeIbeDecryptionKeyFromStorage = await keyStorageService.getIbeDecryptionKey(); - // TODO:cache this key - const privateEncryptedIbeKeyBytes = - maybeIbeDecryptionKeyFromStorage ?? - new Uint8Array( - await getActor().get_vetkey_resharing_ibe_decryption_key(tsk.publicKeyBytes()) - ); - if (!maybeIbeDecryptionKeyFromStorage) { - console.log( - `Saving IBE decryption key for chat ${chatIdToString(chatId)} vetkeyEpoch ${vetkeyEpoch.toString()}` - ); - keyStorageService - .saveIbeDecryptionKey(new Uint8Array(privateEncryptedIbeKeyBytes)) - .catch((error) => { - console.error( - `Failed to save IBE decryption key for chat ${chatIdToString(chatId)} vetkeyEpoch ${vetkeyEpoch.toString()}: `, - error - ); - }); - } - - const encryptedVetKey = EncryptedVetKey.deserialize( - new Uint8Array(privateEncryptedIbeKeyBytes) - ); - const privateIbeKey = encryptedVetKey.decryptAndVerify(tsk, publicIbeKey, new Uint8Array()); - - const ibePlaintext = ibeCiphertext.decrypt(privateIbeKey); - return ibePlaintext; - } -} diff --git a/rust/vetkeys/encrypted_chat/frontend/src/lib/stores/auth.svelte.ts b/rust/vetkeys/encrypted_chat/frontend/src/lib/stores/auth.svelte.ts deleted file mode 100644 index 820620932..000000000 --- a/rust/vetkeys/encrypted_chat/frontend/src/lib/stores/auth.svelte.ts +++ /dev/null @@ -1,131 +0,0 @@ -import { chatStorageService } from '$lib/services/chatStorage'; -import { HttpAgent, type ActorSubclass } from '@dfinity/agent'; -import { AuthClient } from '@dfinity/auth-client'; -import type { Principal } from '@dfinity/principal'; -import type { _SERVICE } from '../../declarations/encrypted_chat/encrypted_chat.did'; -import { createActor } from '../../declarations/encrypted_chat'; -import fetch from 'isomorphic-fetch'; -import { DFX_NETWORK, CANISTER_ID_ENCRYPTED_CHAT } from '$env/static/public'; - -if (import.meta.env.SSR || typeof window === 'undefined') { - const { - indexedDB, - IDBKeyRange, - IDBRequest, - IDBDatabase, - IDBTransaction, - IDBCursor, - IDBIndex, - IDBObjectStore, - IDBOpenDBRequest - } = await import('fake-indexeddb'); - globalThis.indexedDB = indexedDB; - globalThis.IDBKeyRange = IDBKeyRange; - globalThis.IDBDatabase = IDBDatabase; - globalThis.IDBTransaction = IDBTransaction; - globalThis.IDBRequest = IDBRequest; - globalThis.IDBCursor = IDBCursor; - globalThis.IDBIndex = IDBIndex; - globalThis.IDBObjectStore = IDBObjectStore; - globalThis.IDBOpenDBRequest = IDBOpenDBRequest; -} - -export type AuthState = - | { - label: 'initializing-auth'; - } - | { - label: 'anonymous'; - client: AuthClient; - } - | { - label: 'initialized'; - client: AuthClient; - } - | { - label: 'error'; - error: string; - }; - -export const auth = $state<{ state: AuthState }>({ - state: { label: 'initializing-auth' } -}); - -async function initAuth() { - const client = await AuthClient.create(); - if (await client.isAuthenticated()) { - auth.state = { - label: 'initialized', - client - }; - } else { - auth.state = { - label: 'anonymous', - client - }; - } -} - -void initAuth(); - -export async function login() { - if (auth.state.label === 'anonymous') { - const client = auth.state.client; - await client.login({ - maxTimeToLive: BigInt(8 * 3600) * BigInt(1_000_000_000), // 8 hours - identityProvider: - DFX_NETWORK === 'ic' - ? 'https://identity.ic0.app/#authorize' - : `http://rdmx6-jaaaa-aaaaa-aaadq-cai.localhost:4943/#authorize`, - onSuccess: () => { - authenticate(client); - }, - onError: (e) => console.error('Failed to authenticate with internet identity: ' + e) - }); - } -} - -function authenticate(client: AuthClient) { - auth.state = { - label: 'initialized', - client - }; -} - -export async function logout() { - if (auth.state.label === 'initialized') { - await auth.state.client.logout(); - auth.state = { - label: 'anonymous', - client: auth.state.client - }; - await chatStorageService.discardCacheCompletely(); - } -} - -export function getMyPrincipal(): Principal { - if (auth.state.label !== 'initialized') throw new Error('Unexpectedly not authenticated'); - if (!auth.state.client.getIdentity().getPrincipal()) { - console.error('Unexpectedly not authenticated: undefined principal', auth.state.client.getIdentity().getPrincipal()); - } - return auth.state.client.getIdentity().getPrincipal(); -} - -export function getActor(): ActorSubclass<_SERVICE> { - if (auth.state.label === 'initialized') { - const host = DFX_NETWORK === 'ic' ? 'https://ic0.app' : 'http://localhost:4943'; - const shouldFetchRootKey = DFX_NETWORK !== 'ic'; - const agent = HttpAgent.createSync({ - identity: auth.state.client.getIdentity(), - fetch: fetch, - host, - shouldFetchRootKey - }); - if (!CANISTER_ID_ENCRYPTED_CHAT) { - throw new Error('CANISTER_ID_ENCRYPTED_CHAT is not set'); - } - return createActor(CANISTER_ID_ENCRYPTED_CHAT, { agent }); - } else { - throw new Error('Not authenticated'); - } -} diff --git a/rust/vetkeys/encrypted_chat/frontend/src/lib/stores/chat.svelte.ts b/rust/vetkeys/encrypted_chat/frontend/src/lib/stores/chat.svelte.ts deleted file mode 100644 index acaa7f749..000000000 --- a/rust/vetkeys/encrypted_chat/frontend/src/lib/stores/chat.svelte.ts +++ /dev/null @@ -1,486 +0,0 @@ -import { - type Chat, - type Message, - type UserConfig, - type Notification, - type SymmetricRatchetStats -} from '../types'; -import { canisterAPI } from '../services/canisterApi'; -import { chatStorageService } from '../services/chatStorage'; -import { SvelteDate } from 'svelte/reactivity'; -import { auth, getActor, getMyPrincipal } from '$lib/stores/auth.svelte'; -import type { - ChatId, - GroupModification -} from '../../declarations/encrypted_chat/encrypted_chat.did'; -import { Principal } from '@dfinity/principal'; -import { chatIdFromString, chatIdToString } from '$lib/utils'; -import { EncryptedMessagingService } from '$lib/services/encryptedMessagingService'; -import * as cbor from 'cbor-x'; -import { KeyStorageService } from '$lib/services/keyStorage'; - -export const chats = $state<{ state: Chat[] }>({ state: [] }); -export const selectedChatId = $state<{ state: ChatId | null }>({ state: null }); -export const userConfig = $state<{ state: UserConfig | null }>({ state: null }); -export const notifications = $state<{ state: Notification[] }>({ state: [] }); -export const isLoading = $state({ state: false }); -export const isBlocked = $state({ state: false }); -export const availableChats = $state({ state: [] }); -export const messages = $state<{ state: Record }>({ state: {} }); - -const encryptedMessagingService = new EncryptedMessagingService(); - -export function initVetKeyReactions() { - $effect.root(() => { - $effect(() => { - console.log('Running chat saver $effect...'); - if (auth.state.label !== 'initialized') return; - - for (const chat of chats.state) { - chatStorageService.saveChat(chat).catch((error) => { - console.error('Failed to save chat:', error); - }); - } - - for (const [chatIdStr, messagesArray] of Object.entries(messages.state)) { - for (const message of messagesArray) { - void chatStorageService.containsMessage(chatIdStr, message.messageId).then((exists) => { - if (!exists) { - chatStorageService.saveMessage(message).catch((error) => { - console.error('Failed to save message:', error); - }); - } - }); - } - } - }); - }); -} - -export const chatUIActions = { - async initialize() { - console.log('chatActions.initialize'); - isLoading.state = true; - - try { - // Load user config - let config = await chatStorageService.getUserConfig(); - if (!config) { - config = chatStorageService.getMyUserConfig(); - await chatStorageService.saveUserConfig(config); - } - userConfig.state = config; - - // Load chats - const allChats = await chatStorageService.getAllChats(); - - console.log('initialize: allChats', allChats); - - const allMessages: Record = {}; - // Load messages - for (const chat of allChats) { - const chatMessages = await chatStorageService.getMessages(chat.idStr); - console.log( - 'initialize: adding ', - chatMessages.length, - ' messages from indexedDB for chat ', - chat.idStr - ); - if (chatMessages.length !== 0) { - encryptedMessagingService.skipMessagesAvailableLocally( - chatIdFromString(chat.idStr), - BigInt(chatMessages[chatMessages.length - 1].messageId) + 1n - ); - } - allMessages[chat.idStr] = [...(allMessages[chat.idStr] ?? []), ...chatMessages]; - } - - // set the last message for each chat - allChats.forEach((chat) => { - const lastMessage = allMessages[chat.idStr][allMessages[chat.idStr].length - 1]; - chat.lastMessage = lastMessage; - }); - console.log('initialize: allMessages', allMessages); - console.log('initialize: allChats', allChats); - chats.state = allChats; - messages.state = allMessages; - const symmetricRatchetStates = await new KeyStorageService().getAllSymmetricRatchetStates(); - for (const { chatIdStr, vetKeyEpoch, state } of symmetricRatchetStates) { - console.log( - 'initialize: inducting symmetric ratchet state for chatId', - chatIdStr, - 'vetKeyEpoch', - vetKeyEpoch, - 'symmetricRatchetState', - state - ); - encryptedMessagingService.inductSymmetricRatchetState(chatIdStr, vetKeyEpoch, state); - } - - encryptedMessagingService.start(); - - await this.refreshChats(); - - // Set up periodic cleanup - //setInterval(() => { - // TODO: cleanup disappearing messages - // }, 60000); - } catch (error) { - console.error('Failed to initialize chat:', error); - chatUIActions.addNotification({ - type: 'error', - title: 'Initialization Error', - message: 'Failed to load chat data. Please refresh the page.', - isDismissible: true - }); - } finally { - isLoading.state = false; - } - }, - - async refreshChats() { - console.log('refreshChats'); - const currentChatIds = encryptedMessagingService.getCurrentChatIds(); - - const chatsToRemoveFromUi = chats.state.filter( - (c) => !currentChatIds.find((chatId) => chatIdToString(chatId) === c.idStr) - ); - if (chatsToRemoveFromUi.length > 0) { - console.log( - 'refreshChats: removing chats ', - chatsToRemoveFromUi.map((c) => c.idStr), - ' from chats.state ', - chats.state.map((c) => c.idStr), - ' because in currentChatIds from encryptedMessagingService we have ', - currentChatIds.map((c) => chatIdToString(c)) - ); - } - - const chatsToAddToUi = currentChatIds.filter( - (chatId) => !chats.state.find((chat) => chat.idStr === chatIdToString(chatId)) - ); - - const vetKeyEpochMetaData = []; - - for (const chatId of chatsToAddToUi) { - const chatIdStr = chatIdToString(chatId); - console.log('refreshChats: adding chat ', chatIdStr); - vetKeyEpochMetaData.push(await canisterAPI.getLatestVetKeyEpochMetadata(getActor(), chatId)); - } - - const newChats: Chat[] = []; - - for (let i = 0; i < chatsToAddToUi.length; i++) { - const chatId = chatsToAddToUi[i]; - const chatIdStr = chatIdToString(chatId); - console.log('refreshChats: adding chat ', chatIdStr); - - if (chats.state.find((c) => c.idStr === chatIdStr)) { - continue; - } - - const isGroup = 'Group' in chatId; - - const participants = vetKeyEpochMetaData[i].participants; - if (!participants) { - console.error('Failed to get participants for chat:', chatId); - continue; - } - const myPrincipalText = getMyPrincipal().toText(); - const name = isGroup - ? `Group: ${shortenId(String(chatId.Group.toString()))}` - : participants[0].toString() === participants[1].toString() - ? 'Note to Self' - : `Direct: ${participants.find((p) => p.toString() !== myPrincipalText)?.toString()}`; - const now = new SvelteDate(); - - const chat: Chat = { - idStr: chatIdStr, - name, - type: isGroup ? 'group' : 'direct', - participants: participants.map((p) => ({ - principal: p, - name: myPrincipalText === p.toText() ? 'Me' : p.toText(), - avatar: '👤', - isOnline: true - })), - lastMessage: undefined, - lastActivity: now, - isReady: true, - isUpdating: false, - disappearingMessagesDuration: 0, - keyRotationStatus: buildDummyRotationStatus(), - vetKeyEpoch: Number(vetKeyEpochMetaData[i].epoch_id), - symmetricRatchetEpoch: 0, - unreadCount: 0, - avatar: isGroup ? '👥' : '👤' - }; - newChats.push(chat); - } - - chats.state = [ - ...chats.state.filter((c) => - currentChatIds.find((chatId) => chatIdToString(chatId) === c.idStr) - ), - ...newChats.filter((c) => !chats.state.find((c2) => c2.idStr === c.idStr)) - ]; - - // Initialize empty messages cache; we lazy-load on demand - for (const chat of newChats) { - if (!messages.state[chat.idStr]) messages.state[chat.idStr] = []; - } - - if (chatsToRemoveFromUi.length > 0) { - for (const chat of chatsToRemoveFromUi) { - await chatStorageService.deleteChat(chat.idStr); - } - } - }, - - selectChat(chatId: ChatId) { - selectedChatId.state = chatId; - console.log('selectChat: selected chat ', chatIdToString(chatId)); - - // Mark as read - const index = chats.state.findIndex((c) => c.idStr === chatIdToString(chatId)); - if (index >= 0) { - chats.state[index].unreadCount = 0; - } else { - console.error('selectChat: chat not found: ', chatIdToString(chatId)); - } - }, - - async loadChatMessages() { - const newMessages = encryptedMessagingService.takeReceivedMessages(); - console.log( - 'loadChatMessages: encryptedMessagingService.takeReceivedMessages() ', - newMessages.size, - 'messages' - ); - for (const [chatIdStr, messagesArray] of newMessages.entries()) { - const chat = chats.state.find((c) => c.idStr === chatIdStr); - if (!chat) { - console.error('Bug in loadChatMessages: chat not found for a new message: ', chatIdStr); - continue; - } - - for (const m of messagesArray) await chatStorageService.saveMessage(m); - - const chatMessages = [...messages.state[chatIdStr], ...messagesArray]; - - const newMessagesState = { - ...messages.state, - [chatIdStr]: chatMessages - }; - - messages.state = newMessagesState; - - // Check if this chat is currently selected - const isCurrentlySelected = - selectedChatId.state && chatIdToString(selectedChatId.state) === chatIdStr; - - chats.state = chats.state.map((c) => - c.idStr === chatIdStr - ? { - ...c, - // Don't increment unread count if chat is currently selected - unreadCount: isCurrentlySelected ? 0 : c.unreadCount + messagesArray.length, - lastMessage: messagesArray[messagesArray.length - 1] - } - : c - ); - } - }, - - enqueueEncryptAndSendMessage( - chatId: ChatId, - textContent: string, - fileData?: { name: string; size: number; type: string; data: ArrayBuffer } - ) { - const messageContent = cbor.encode({ textContent, fileData }) as Uint8Array; - encryptedMessagingService.enqueueSendMessage(chatId, messageContent); - }, - - rotateKeys(chatId: string) { - try { - // Mark chat as updating - chats.state = chats.state.map((chat) => - chat.idStr === chatId ? { ...chat, isUpdating: true } : chat - ); - - const stats: SymmetricRatchetStats = canisterAPI.getRatchetStats(); - - // Update chat with new key status (dummy update) - chats.state = chats.state.map((chat) => - chat.idStr === chatId - ? { - ...chat, - isUpdating: false, - keyRotationStatus: { - lastRotation: stats.lastRotation, - nextRotation: stats.nextScheduledRotation, - isRotationNeeded: false, - currentEpoch: stats.currentEpoch - }, - ratchetEpoch: stats.currentEpoch - } - : chat - ); - - chatUIActions.addNotification({ - type: 'success', - title: 'Keys Rotated', - message: 'Chat encryption keys have been successfully rotated.', - isDismissible: true, - duration: 3000 - }); - } catch (error) { - console.error('Failed to rotate keys:', error); - - // Mark chat as not updating - chats.state = chats.state.map((chat) => - chat.idStr === chatId ? { ...chat, isUpdating: false } : chat - ); - - chatUIActions.addNotification({ - type: 'error', - title: 'Key Rotation Failed', - message: 'Failed to rotate encryption keys. Please try again.', - isDismissible: true - }); - } - }, - - async updateUserConfig(config: Partial) { - if (!userConfig.state) return; - - const newConfig = { ...userConfig.state, ...config }; - userConfig.state = newConfig; - await chatStorageService.saveUserConfig(newConfig); - - // Trigger cache cleanup if retention days changed - if (config.cacheRetentionDays !== undefined) { - await chatStorageService.cleanupUserCache(config.cacheRetentionDays); - } - }, - - addNotification(notification: Omit) { - const id = `notification-${Date.now()}-${Math.random()}`; - const newNotification: Notification = { ...notification, id }; - notifications.state = [...notifications.state, newNotification]; - - // Auto-dismiss if duration is set - if (notification.duration) { - setTimeout(() => { - chatUIActions.dismissNotification(id); - }, notification.duration); - } - }, - - dismissNotification(id: string) { - notifications.state = notifications.state.filter((n) => n.id !== id); - }, - - async createDirectChat( - receiverPrincipalText: string, - rotationMinutes: number, - expirationMinutes: number - ) { - try { - const receiver = Principal.fromText(receiverPrincipalText.trim()); - await canisterAPI.createDirectChat( - getActor(), - receiver, - BigInt(Math.max(0, Math.trunc(rotationMinutes))), - BigInt(Math.max(0, Math.trunc(expirationMinutes))) - ); - chatUIActions.addNotification({ - type: 'success', - title: 'Chat Created', - message: 'Direct chat created successfully.', - isDismissible: true, - duration: 3000 - }); - await chatUIActions.refreshChats(); - } catch (error) { - console.error('Failed to create direct chat:', error); - chatUIActions.addNotification({ - type: 'error', - title: 'Create Chat Failed', - message: 'Failed to create direct chat. Check the Principal and try again.', - isDismissible: true - }); - } - }, - - async createGroupChat( - participantPrincipalTexts: string[], - rotationMinutes: number, - expirationMinutes: number - ) { - try { - const participants = participantPrincipalTexts - .map((t) => t.trim()) - .filter(Boolean) - .map((t) => Principal.fromText(t)); - const meta = await canisterAPI.createGroupChat( - getActor(), - participants, - BigInt(Math.max(0, Math.trunc(rotationMinutes))), - BigInt(Math.max(0, Math.trunc(expirationMinutes))) - ); - chatUIActions.addNotification({ - type: 'success', - title: 'Group Created', - message: `Group chat #${meta.chat_id.toString()} created successfully.`, - isDismissible: true, - duration: 3000 - }); - await chatUIActions.refreshChats(); - } catch (error) { - console.error('Failed to create group chat:', error); - chatUIActions.addNotification({ - type: 'error', - title: 'Create Group Failed', - message: 'Failed to create group chat. Check the Principals and try again.', - isDismissible: true - }); - } - }, - - async updateGroupMembers(chatId: ChatId, addUsers: Principal[], removeUsers: Principal[]) { - if ('Direct' in chatId) { - throw new Error('updateGroupMembers: chatId is a direct chat'); - } - const modification: GroupModification = { - remove_participants: removeUsers, - add_participants: addUsers - }; - const result = await getActor().modify_group_chat_participants(chatId.Group, modification); - if ('Ok' in result) { - console.log( - `Group ${chatIdToString(chatId)} updated: +${addUsers.length}, -${removeUsers.length}` - ); - } else { - throw new Error(result.Err); - } - } -}; - -function shortenId(id: string): string { - return id.length > 8 ? `${id.slice(0, 6)}…${id.slice(-2)}` : id; -} - -function buildDummyRotationStatus() { - return { - lastRotation: new SvelteDate(1000000000000000), - nextRotation: new SvelteDate(2000000000000000), - isRotationNeeded: false, - currentEpoch: 0 - }; -} - -// Initialize on module load (browser only) -if (typeof window !== 'undefined') { - void chatUIActions.initialize(); -} diff --git a/rust/vetkeys/encrypted_chat/frontend/src/lib/types/index.ts b/rust/vetkeys/encrypted_chat/frontend/src/lib/types/index.ts deleted file mode 100644 index 618be057a..000000000 --- a/rust/vetkeys/encrypted_chat/frontend/src/lib/types/index.ts +++ /dev/null @@ -1,130 +0,0 @@ -import { Principal } from '@dfinity/principal'; - -export interface User { - principal: Principal; - name: string; - avatar?: string; - isOnline: boolean; - lastSeen?: Date; -} - -export interface Message { - messageId: string; - chatId: string; - senderId: string; - content: string; - timestamp: Date; - fileData?: { - name: string; - size: number; - type: string; - data: Uint8Array; - }; - vetkeyEpoch: number; - symmetricRatchetEpoch: number; -} - -export interface Chat { - idStr: string; - name: string; - type: 'direct' | 'group'; - // Participants are required by UI components like ChatHeader/ChatListItem - participants: User[]; - lastMessage?: Message; - lastActivity: Date; - isReady: boolean; - isUpdating: boolean; - disappearingMessagesDuration: number; // in days, 0 = never - keyRotationStatus: VetKeyRotationStatus; - vetKeyEpoch: number; - symmetricRatchetEpoch: number; - unreadCount: number; - avatar?: string; -} - -export interface DirectChat extends Chat { - type: 'direct'; - otherParticipant: User; -} - -export interface GroupChat extends Chat { - type: 'group'; - otherParticipants: User[]; -} - -export interface VetKeyRotationStatus { - lastRotation: Date; - currentEpoch: number; -} - -export interface SymmetricRatchetStats { - currentEpoch: number; - messagesInCurrentEpoch: number; - lastRotation: Date; - nextScheduledRotation: Date; -} - -export interface UserConfig { - cacheRetentionDays: number; - userId: string; - userName: string; - userAvatar?: string; -} - -export interface ChatStatus { - isReady: boolean; - isUpdating: boolean; - lastSync: Date; - additionalInfo?: string; -} - -export interface FileUpload { - file: File; - preview?: string; - isValid: boolean; - error?: string; -} - -export type ChatType = 'direct' | 'group'; -export type MessageType = 'text' | 'file' | 'image'; -export type NotificationType = 'info' | 'warning' | 'error' | 'success'; - -export interface Notification { - id: string; - type: NotificationType; - title: string; - message: string; - isDismissible: boolean; - duration?: number; // auto-dismiss after ms, undefined = manual dismiss -} - -export class StoragePrefixesClass { - public readonly MESSAGE_PREFIX: string = 'messages'; - public readonly CONFIG_KEY: string = 'user_config'; - public readonly DISCLAIMER_KEY: string = 'disclaimer_dismissed'; - public readonly CHAT_PREFIX: string = 'chat'; - - public readonly CHAT_EPOCH_KEY_PREFIX: string = 'chat_epoch_keys'; - public readonly CHAT_IBE_DECRYPTION_KEY_PREFIX: string = 'chat_ibe_decryption_key'; -} - -export const storagePrefixes = new StoragePrefixesClass(); - -export class VetKeyEpochError extends Error { - requiredVetKeyEpoch: bigint; - - constructor(message: string, requiredVetKeyEpoch: bigint) { - super(message); - this.name = 'VetKeyEpochError'; - this.requiredVetKeyEpoch = requiredVetKeyEpoch; - Object.setPrototypeOf(this, VetKeyEpochError.prototype); - } -} - -export class SymmetricRatchetEpochError extends Error { - constructor(message: string) { - super(message); - this.name = 'SymmetricRatchetEpochError'; - Object.setPrototypeOf(this, SymmetricRatchetEpochError.prototype); - } -} diff --git a/rust/vetkeys/encrypted_chat/frontend/src/lib/utils/index.ts b/rust/vetkeys/encrypted_chat/frontend/src/lib/utils/index.ts deleted file mode 100644 index b2281684b..000000000 --- a/rust/vetkeys/encrypted_chat/frontend/src/lib/utils/index.ts +++ /dev/null @@ -1,117 +0,0 @@ -import type { ChatId } from '../../declarations/encrypted_chat/encrypted_chat.did'; -import { Principal } from '@dfinity/principal'; - -export function chatIdToString(chatId: ChatId): string { - if ('Group' in chatId) return `group/${chatId.Group.toString()}`; - const [a, b] = chatId.Direct; - return `direct/${a.toString()}/${b.toString()}`; -} - -export function chatIdFromString(str: string): ChatId { - if (typeof str !== 'string') throw new Error('chatIdStr is not a string but ' + typeof str); - if (str.startsWith('group/')) return { Group: BigInt(str.slice(6)) }; - const [a, b] = str.split('/').slice(1); - return { Direct: [Principal.fromText(a), Principal.fromText(b)] }; -} - -export function chatIdVetKeyEpochToString(chatId: ChatId, vetKeyEpoch: bigint): string { - return chatIdToString(chatId) + '/' + vetKeyEpoch.toString(); -} - -export function chatIdVetKeyEpochFromString(str: string): { chatId: ChatId; vetKeyEpoch: bigint } { - const vetKeyEpochStr = str.split('/').pop()!; - const chatIdStr = str.slice(0, str.lastIndexOf(`/${vetKeyEpochStr}`)); - return { chatId: chatIdFromString(chatIdStr), vetKeyEpoch: BigInt(vetKeyEpochStr) }; -} - -// eslint-disable-next-line @typescript-eslint/no-explicit-any -export function stringifyBigInt(value: any): string { - return JSON.stringify(value, (_key, value) => { - if (typeof value === 'bigint') { - return value.toString(); - } - // eslint-disable-next-line @typescript-eslint/no-unsafe-return - return value; - }); -} - -export function uBigIntTo8ByteUint8ArrayBigEndian(value: bigint): Uint8Array { - if (value < 0n) throw new RangeError('Accpts only bigint n >= 0'); - - const bytes = new Uint8Array(8); - for (let i = 0; i < 8; i++) { - bytes[i] = Number((value >> BigInt(i * 8)) & 0xffn); - } - return bytes; -} - -export function u8ByteUint8ArrayBigEndianToUBigInt(bytes: Uint8Array): bigint { - if (bytes.length !== 8) throw new Error('Expected 8 bytes'); - let value = 0n; - for (let i = 0; i < 8; i++) { - value += BigInt(bytes[i]) << BigInt(i * 8); - } - return value; -} - -export function sizePrefixedBytesFromString(text: string): Uint8Array { - const bytes = new TextEncoder().encode(text); - if (bytes.length > 255) { - throw new Error('Text is too long'); - } - const size = new Uint8Array(1); - size[0] = bytes.length & 0xff; - return new Uint8Array([...size, ...bytes]); -} - -export function chatIdsNumMessagesToSummary( - args: { chatId: ChatId; numMessages: bigint }[] -): string { - return args.reduce((acc, { chatId, numMessages }) => { - if ('Direct' in chatId) { - return ( - acc + - chatId.Direct[0].toText() + - ' ' + - chatId.Direct[1].toText() + - ' #' + - numMessages.toString() - ); - } else { - return ( - acc + - (acc.length > 0 ? ' | ' : '') + - chatId.Group.toString() + - ' #' + - numMessages.toString() - ); - } - }, ''); -} - -export function randomNonce(): bigint { - const buf = new Uint8Array(8); - globalThis.crypto.getRandomValues(buf); - let nonce = 0n; - for (const b of buf) nonce = (nonce << 8n) | BigInt(b); - return nonce; -} - -export function toHex(bytes: Uint8Array): string { - const hex: string[] = []; - for (let i = 0; i < bytes.length; i++) { - const v = bytes[i].toString(16); - hex[i] = v.length === 1 ? '0' + v : v; - } - return hex.join(''); -} - -export function fromHex(hex: string): Uint8Array { - if (hex.length % 2 !== 0) throw new Error('Invalid hex string'); - const len = hex.length / 2; - const bytes = new Uint8Array(len); - for (let i = 0; i < len; i++) { - bytes[i] = parseInt(hex.substr(i * 2, 2), 16); - } - return bytes; -} diff --git a/rust/vetkeys/encrypted_chat/frontend/src/routes/+layout.svelte b/rust/vetkeys/encrypted_chat/frontend/src/routes/+layout.svelte deleted file mode 100644 index 41856db62..000000000 --- a/rust/vetkeys/encrypted_chat/frontend/src/routes/+layout.svelte +++ /dev/null @@ -1,28 +0,0 @@ - - - - - - -
- - - - - - {@render children?.()} -
- - diff --git a/rust/vetkeys/encrypted_chat/frontend/src/routes/+layout.ts b/rust/vetkeys/encrypted_chat/frontend/src/routes/+layout.ts deleted file mode 100644 index 189f71e2e..000000000 --- a/rust/vetkeys/encrypted_chat/frontend/src/routes/+layout.ts +++ /dev/null @@ -1 +0,0 @@ -export const prerender = true; diff --git a/rust/vetkeys/encrypted_chat/frontend/src/routes/+page.svelte b/rust/vetkeys/encrypted_chat/frontend/src/routes/+page.svelte deleted file mode 100644 index de7c5733b..000000000 --- a/rust/vetkeys/encrypted_chat/frontend/src/routes/+page.svelte +++ /dev/null @@ -1,121 +0,0 @@ - - - - Encrypted Chat using vetKeys - - - -{#if auth.state.label !== 'initialized'} - -{:else if isLoading.state} - -
-
-
-

- Loading vetKeys Chat -

-

- Initializing secure communication... -

-
-
-
-
-
-
-
-{:else} - -
- -
- -
- - -
- -
-
-{/if} - - diff --git a/rust/vetkeys/encrypted_chat/frontend/svelte.config.js b/rust/vetkeys/encrypted_chat/frontend/svelte.config.js deleted file mode 100644 index 0cf8ed3f6..000000000 --- a/rust/vetkeys/encrypted_chat/frontend/svelte.config.js +++ /dev/null @@ -1,29 +0,0 @@ -import { vitePreprocess } from '@sveltejs/vite-plugin-svelte'; -import adapter from '@sveltejs/adapter-static'; - -const config = { - preprocess: vitePreprocess(), - kit: { - adapter: adapter({ - pages: 'dist', - assets: 'dist', - fallback: null, - precompress: true - }), - prerender: { - entries: ['*'] // ensures all routes are prerendered - }, - output: { - bundleStrategy: 'single' - }, - env: { - publicPrefix: '' - } - }, - compilerOptions: { - experimental: { - async: true - } - } -}; -export default config; diff --git a/rust/vetkeys/encrypted_chat/frontend/tailwind.config.js b/rust/vetkeys/encrypted_chat/frontend/tailwind.config.js deleted file mode 100644 index 30966375d..000000000 --- a/rust/vetkeys/encrypted_chat/frontend/tailwind.config.js +++ /dev/null @@ -1,23 +0,0 @@ -import { skeleton } from '@skeletonlabs/tw-plugin'; - -/** @type {import('tailwindcss').Config} */ -export default { - content: [ - './src/**/*.{html,js,svelte,ts}', - './node_modules/@skeletonlabs/skeleton/**/*.{html,js,svelte,ts}' - ], - theme: { - extend: { - fontFamily: { - sans: ['Inter', 'system-ui', 'sans-serif'] - } - } - }, - plugins: [ - skeleton({ - themes: { - preset: ['modern'] - } - }) - ] -}; diff --git a/rust/vetkeys/encrypted_chat/frontend/tsconfig.json b/rust/vetkeys/encrypted_chat/frontend/tsconfig.json deleted file mode 100644 index 95faaf66e..000000000 --- a/rust/vetkeys/encrypted_chat/frontend/tsconfig.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "extends": "./.svelte-kit/tsconfig.json", - "compilerOptions": { - "allowJs": true, - "checkJs": true, - "esModuleInterop": true, - "forceConsistentCasingInFileNames": true, - "resolveJsonModule": true, - "skipLibCheck": true, - "strict": true, - "moduleResolution": "bundler" - } - // Path aliases are handled by https://svelte.dev/docs/kit/configuration#alias - // except $lib which is handled by https://svelte.dev/docs/kit/configuration#files - // - // If you want to overwrite includes/excludes, make sure to copy over the relevant includes/excludes - // from the referenced tsconfig.json - TypeScript does not merge them in -} diff --git a/rust/vetkeys/encrypted_chat/frontend/vite.config.ts b/rust/vetkeys/encrypted_chat/frontend/vite.config.ts deleted file mode 100644 index 24886d002..000000000 --- a/rust/vetkeys/encrypted_chat/frontend/vite.config.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { sveltekit } from '@sveltejs/kit/vite'; -import { defineConfig } from 'vite'; -import tailwindcss from '@tailwindcss/vite'; -import environment from 'vite-plugin-environment'; -import typescript from '@rollup/plugin-typescript'; - -export default defineConfig({ - plugins: [ - typescript(), - tailwindcss(), - sveltekit(), - environment('all', { prefix: 'CANISTER_' }), - environment('all', { prefix: 'DFX_' }) - ], - server: { - proxy: { - '/api': { - target: 'http://localhost:4943', - changeOrigin: true - } - } - }, - build: { - sourcemap: true, - } -}); diff --git a/rust/vetkeys/encrypted_chat/rust/Cargo.toml b/rust/vetkeys/encrypted_chat/rust/Cargo.toml deleted file mode 100644 index d1e49e317..000000000 --- a/rust/vetkeys/encrypted_chat/rust/Cargo.toml +++ /dev/null @@ -1,3 +0,0 @@ -[workspace] -members = ["backend"] -resolver = "2" diff --git a/rust/vetkeys/encrypted_chat/rust/backend/Cargo.toml b/rust/vetkeys/encrypted_chat/rust/backend/Cargo.toml deleted file mode 100644 index 10ceb23f3..000000000 --- a/rust/vetkeys/encrypted_chat/rust/backend/Cargo.toml +++ /dev/null @@ -1,32 +0,0 @@ -[package] -name = "ic-vetkeys-example-encrypted-chat-backend" -authors = ["DFINITY Stiftung"] -version = "0.1.0" -edition = "2021" -license = "Apache-2.0" -description = "Encrypted Chat using vetKeys" -repository = "https://github.com/dfinity/vetkeys" -rust-version = "1.85.0" - -[lib] -path = "src/lib.rs" -crate-type = ["cdylib", "lib"] - -[dependencies] -candid = "0.10.2" -ic-cdk = "0.18.5" -ic-cdk-macros = "0.18.5" -ic-dummy-getrandom-for-wasm = "0.1.0" -ic-stable-structures = "0.7.0" -ic-cdk-timers = "0.12.2" -ic-vetkeys = "0.4.0" -serde = "1.0.217" -serde_bytes = "0.11.15" -serde_cbor = "0.11.2" -serde_with = "3.11.0" -sha2 = "0.10.7" - -[dev-dependencies] -pocket-ic = "9.0.2" -rand_chacha = "0.9.0" -rand = "0.9.2" diff --git a/rust/vetkeys/encrypted_chat/rust/backend/Makefile b/rust/vetkeys/encrypted_chat/rust/backend/Makefile deleted file mode 100644 index a8ffe99b7..000000000 --- a/rust/vetkeys/encrypted_chat/rust/backend/Makefile +++ /dev/null @@ -1,15 +0,0 @@ -.PHONY: compile-wasm -.SILENT: compile-wasm -compile-wasm: - cargo build --release --target wasm32-unknown-unknown - -.PHONY: extract-candid -.SILENT: extract-candid -extract-candid: compile-wasm - candid-extractor ../target/wasm32-unknown-unknown/release/ic_vetkeys_example_encrypted_chat_backend.wasm > backend.did - -.PHONY: clean -.SILENT: clean -clean: - cargo clean - rm -rf ../.dfx \ No newline at end of file diff --git a/rust/vetkeys/encrypted_chat/rust/backend/backend.did b/rust/vetkeys/encrypted_chat/rust/backend/backend.did deleted file mode 100644 index 968830f21..000000000 --- a/rust/vetkeys/encrypted_chat/rust/backend/backend.did +++ /dev/null @@ -1,86 +0,0 @@ -type ChatId = variant { - Group : nat64; - Direct : record { principal; principal }; -}; -type EncryptedMessage = record { - content : blob; - metadata : EncryptedMessageMetadata; -}; -type EncryptedMessageMetadata = record { - vetkey_epoch : nat64; - sender : principal; - symmetric_key_epoch : nat64; - chat_message_id : nat64; - nonce : nat64; - timestamp : nat64; -}; -type GroupChatMetadata = record { creation_timestamp : nat64; chat_id : nat64 }; -type GroupModification = record { - remove_participants : vec principal; - add_participants : vec principal; -}; -type Result = variant { Ok : nat64; Err : text }; -type Result_1 = variant { Ok : GroupChatMetadata; Err : text }; -type Result_2 = variant { Ok : blob; Err : text }; -type Result_3 = variant { Ok : VetKeyEpochMetadata; Err : text }; -type Result_4 = variant { Ok : opt blob; Err : text }; -type Result_5 = variant { Ok; Err : text }; -type UserMessage = record { - vetkey_epoch : nat64; - content : blob; - symmetric_key_epoch : nat64; - nonce : nat64; -}; -type VetKeyEpochMetadata = record { - symmetric_key_rotation_duration : nat64; - participants : vec principal; - messages_start_with_id : nat64; - creation_timestamp : nat64; - epoch_id : nat64; -}; -service : (text) -> { - chat_public_key : (ChatId, nat64) -> (blob); - create_direct_chat : (principal, nat64, nat64) -> (Result); - create_group_chat : (vec principal, nat64, nat64) -> (Result_1); - // Derives a vetKey for an existing chat or creates a new one if the chat does not exist. - // - // # Arguments - // * `chat_id`: The chat to derive a vetKey for. - // * `opt_vetkey_epoch`: The vetKey epoch to derive a vetKey for. If `None`, a new epoch is created. - // * `transport_key`: The transport key to derive a vetKey for. - // - // # Errors - // * If the vetKey epoch has expired. - // * If the user does not have access to the chat or vetKey epoch. - // * If the user has already cached the key. - derive_chat_vetkey : (ChatId, opt nat64, blob) -> (Result_2); - get_encrypted_vetkey_for_my_cache_storage : (blob) -> (blob); - get_latest_chat_vetkey_epoch_metadata : (ChatId) -> (Result_3) query; - // Returns messages for a chat starting from a given message id. - // - // # Arguments - // * `chat_id`: The chat to get messages for. - // * `message_id`: The message id to start from. - // * `limit`: The maximum number of messages to return. - // - // # Notes - // * Does not fail if the chat does not exist or the user has no access -- returns empty vector instead. - get_messages : (ChatId, nat64, opt nat32) -> (vec EncryptedMessage) query; - get_my_chat_ids : () -> (vec record { ChatId; nat64 }) query; - get_my_reshared_ibe_encrypted_vetkey : (ChatId, nat64) -> (Result_4); - get_my_symmetric_key_cache : (ChatId, nat64) -> (Result_4); - get_vetkey_epoch_metadata : (ChatId, nat64) -> (Result_3) query; - get_vetkey_resharing_ibe_decryption_key : (blob) -> (blob); - get_vetkey_resharing_ibe_encryption_key : (principal) -> (blob); - get_vetkey_verification_key_for_my_cache_storage : () -> (blob); - modify_group_chat_participants : (nat64, GroupModification) -> (Result); - reshare_ibe_encrypted_vetkeys : ( - ChatId, - nat64, - vec record { principal; blob }, - ) -> (Result_5); - rotate_chat_vetkey : (ChatId) -> (Result); - send_direct_message : (UserMessage, principal) -> (Result); - send_group_message : (UserMessage, nat64) -> (Result); - update_my_symmetric_key_cache : (ChatId, nat64, blob) -> (Result_5); -} diff --git a/rust/vetkeys/encrypted_chat/rust/backend/src/lib.rs b/rust/vetkeys/encrypted_chat/rust/backend/src/lib.rs deleted file mode 100644 index 3009b3827..000000000 --- a/rust/vetkeys/encrypted_chat/rust/backend/src/lib.rs +++ /dev/null @@ -1,1188 +0,0 @@ -use candid::Principal; -use ic_cdk::management_canister::{VetKDCurve, VetKDDeriveKeyArgs, VetKDKeyId, VetKDPublicKeyArgs}; -use ic_stable_structures::memory_manager::{MemoryId, MemoryManager, VirtualMemory}; -use ic_stable_structures::{ - BTreeMap as StableBTreeMap, Cell as StableCell, DefaultMemoryImpl, Storable, -}; -use ic_vetkeys::encrypted_maps::EncryptedMaps; -use ic_vetkeys::types::AccessRights; -use sha2::Digest; -use std::borrow::Cow; -use std::cell::RefCell; - -pub mod types; -use types::*; - -type Memory = VirtualMemory; - -const NANOSECONDS_IN_MINUTE: u64 = 60_000_000_000; - -pub static DOMAIN_SEPARATOR_VETKEY_ROTATION: &str = "vetkeys-example-encrypted-chat-rotation"; -pub static DOMAIN_SEPARATOR_USER_CACHE: &str = "vetkeys-example-encrypted-chat-user-cache"; -pub static DOMAIN_SEPARATOR_VETKEY_RESHARING: &str = - "vetkeys-example-encrypted-chat-vetkey-resharing"; - -thread_local! { - static MEMORY_MANAGER: RefCell> = - RefCell::new(MemoryManager::init(DefaultMemoryImpl::default())); - - static DIRECT_CHAT_MESSAGES: RefCell> = RefCell::new(StableBTreeMap::init( - MEMORY_MANAGER.with(|m| m.borrow().get(MemoryId::new(0))), - )); - - static GROUP_CHAT_MESSAGES: RefCell> = RefCell::new(StableBTreeMap::init( - MEMORY_MANAGER.with(|m| m.borrow().get(MemoryId::new(1))), - )); - - static GROUP_CHATS: RefCell> = RefCell::new(StableBTreeMap::init( - MEMORY_MANAGER.with(|m| m.borrow().get(MemoryId::new(2))), - )); - - static CHAT_TO_MESSAGE_COUNTERS: RefCell> = RefCell::new(StableBTreeMap::init( - MEMORY_MANAGER.with(|m| m.borrow().get(MemoryId::new(3))), - )); - - static SET_CHAT_AND_SENDER_AND_USER_MESSAGE_ID: RefCell> = RefCell::new(StableBTreeMap::init( - MEMORY_MANAGER.with(|m| m.borrow().get(MemoryId::new(4))), - )); - - static CHAT_TO_VETKEYS_METADATA: RefCell> = RefCell::new(StableBTreeMap::init( - MEMORY_MANAGER.with(|m| m.borrow().get(MemoryId::new(5))), - )); - - static CHAT_TO_MESSAGE_EXPIRY_SETTING: RefCell> = RefCell::new(StableBTreeMap::init( - MEMORY_MANAGER.with(|m| m.borrow().get(MemoryId::new(6))), - )); - - static EXPIRING_MESSAGES: RefCell> = RefCell::new(StableBTreeMap::init( - MEMORY_MANAGER.with(|m| m.borrow().get(MemoryId::new(7))), - )); - - static EXPIRING_VETKEY_EPOCHS_CACHES: RefCell> = RefCell::new(StableBTreeMap::init( - MEMORY_MANAGER.with(|m| m.borrow().get(MemoryId::new(8))), - )); - - static USER_TO_CHAT_MAP: RefCell> = RefCell::new(StableBTreeMap::init( - MEMORY_MANAGER.with(|m| m.borrow().get(MemoryId::new(9))), - )); - - static RESHARED_VETKEYS: RefCell> = RefCell::new(StableBTreeMap::init( - MEMORY_MANAGER.with(|m| m.borrow().get(MemoryId::new(11))), - )); - - // Store symmetric key cache in encrypted maps. On a high level, store the cache in: - // - // map = ENCRYPTED_MAPS[(caller, "encrypted_chat_cache")] - // map[SHA256(chat_id || vetkey_epoch_id)] = cache - // - // The reason for not storing that data directly is that in encrypted maps, the key is limited to 32 bytes, which is a conservative constant due to the fact that stable structures cannot currently store unbounded data in tuples. - static ENCRYPTED_MAPS: RefCell>> = const { RefCell::new(None) }; - - static VETKD_KEY_NAME: RefCell> = - RefCell::new(StableCell::init(MEMORY_MANAGER.with(|m| m.borrow().get(MemoryId::new(12))), String::new())); -} - -#[ic_cdk::init] -fn init(key_name: String) { - VETKD_KEY_NAME.with(|name| { - name.borrow_mut().set(key_name); - }); - - ENCRYPTED_MAPS.with_borrow_mut(|maps| { - let x = EncryptedMaps::init( - DOMAIN_SEPARATOR_USER_CACHE, - key_id(), - MEMORY_MANAGER.with(|m| m.borrow().get(MemoryId::new(13))), - MEMORY_MANAGER.with(|m| m.borrow().get(MemoryId::new(14))), - MEMORY_MANAGER.with(|m| m.borrow().get(MemoryId::new(15))), - MEMORY_MANAGER.with(|m| m.borrow().get(MemoryId::new(16))), - ); - *maps = Some(x); - }); - - start_expired_cleanup_timer_job_with_interval(24 * 3600); -} - -#[ic_cdk::post_upgrade] -fn post_upgrade(key_name: String) { - init(key_name); -} - -#[ic_cdk::update] -fn create_direct_chat( - receiver: Principal, - symmetric_key_rotation_duration_minutes: Time, - message_expiry_time_minutes: Time, -) -> Result { - let caller = ic_cdk::api::msg_caller(); - let chat_id = ChatId::Direct(DirectChatId::new((caller, receiver))); - - if latest_vetkey_epoch_id(chat_id).is_some() { - return Err(format!("Chat {chat_id:?} already exists")); - } - - let now = Time(ic_cdk::api::time()); - const NANOSECONDS_IN_MINUTE: u64 = 60_000_000_000; - - let symmetric_key_rotation_duration = Time( - symmetric_key_rotation_duration_minutes - .0 - .checked_mul(NANOSECONDS_IN_MINUTE) - .ok_or("Overflow: too symmetric key rotation time".to_string())?, - ); - - let todo_remove_1 = CHAT_TO_VETKEYS_METADATA.with_borrow_mut(|metadata| { - let vetkey_epoch_metadata = VetKeyEpochMetadata { - epoch_id: VetKeyEpochId(0), - participants: vec![caller, receiver], - creation_timestamp: now, - symmetric_key_rotation_duration, - messages_start_with_id: ChatMessageId(0), - }; - metadata.insert((chat_id, now), vetkey_epoch_metadata.clone()) - }); - assert!(todo_remove_1.is_none()); - - let todo_remove_2 = CHAT_TO_MESSAGE_COUNTERS - .with_borrow_mut(|counters| counters.insert(chat_id, ChatMessageId(0))); - assert!(todo_remove_2.is_none()); - - USER_TO_CHAT_MAP.with_borrow_mut(|map| { - let todo_remove_3 = map.insert((caller, chat_id, VetKeyEpochId(0)), ()); - assert!(todo_remove_3.is_none()); - let todo_remove_4 = map.insert((receiver, chat_id, VetKeyEpochId(0)), ()); - if caller != receiver { - assert!(todo_remove_4.is_none()); - } - }); - - let expiry_time_nanos = Time( - message_expiry_time_minutes - .0 - .checked_mul(NANOSECONDS_IN_MINUTE) - .ok_or("Overflow: too large expiry time".to_string())?, - ); - - let todo_remove = CHAT_TO_MESSAGE_EXPIRY_SETTING - .with_borrow_mut(|expiry_settings| expiry_settings.insert(chat_id, expiry_time_nanos)); - - assert!(todo_remove.is_none()); - - Ok(now) -} - -#[ic_cdk::update] -fn create_group_chat( - other_participants: Vec, - symmetric_key_rotation_duration_minutes: Time, - message_expiry_time_minutes: Time, -) -> Result { - let caller = ic_cdk::api::msg_caller(); - let now = Time(ic_cdk::api::time()); - - let chat_id_u64 = - GROUP_CHATS.with_borrow(|chats| chats.last_key_value().map(|kv| kv.0 .0 + 1).unwrap_or(0)); - let group_chat_id = GroupChatId(chat_id_u64); - - let group_chat_metadata = GroupChatMetadata { - chat_id: group_chat_id, - creation_timestamp: now, - }; - - GROUP_CHATS.with_borrow_mut(|chats| { - let todo_remove = chats.insert(group_chat_id, group_chat_metadata); - assert!(todo_remove.is_none()); - }); - - let chat_id = ChatId::Group(group_chat_id); - - let mut participants: Vec<_> = [caller].into_iter().chain(other_participants).collect(); - - participants.sort(); - - // ignore duplicates - participants.dedup(); - - let symmetric_key_rotation_duration = Time( - symmetric_key_rotation_duration_minutes - .0 - .checked_mul(NANOSECONDS_IN_MINUTE) - .ok_or("Overflow: too symmetric key rotation time".to_string())?, - ); - - let todo_remove_1 = CHAT_TO_VETKEYS_METADATA.with_borrow_mut(|metadata| { - let vetkey_epoch_metadata = VetKeyEpochMetadata { - epoch_id: VetKeyEpochId(0), - participants: participants.clone(), - creation_timestamp: now, - symmetric_key_rotation_duration, - messages_start_with_id: ChatMessageId(0), - }; - metadata.insert((chat_id, now), vetkey_epoch_metadata.clone()) - }); - assert!(todo_remove_1.is_none()); - - let todo_remove_2 = CHAT_TO_MESSAGE_COUNTERS - .with_borrow_mut(|counters| counters.insert(chat_id, ChatMessageId(0))); - assert!(todo_remove_2.is_none()); - - USER_TO_CHAT_MAP.with_borrow_mut(|map| { - for participant in participants.iter().copied() { - let todo_remove_3 = map.insert((participant, chat_id, VetKeyEpochId(0)), ()); - assert!(todo_remove_3.is_none()); - } - }); - - let expiry_time_nanos = Time( - message_expiry_time_minutes - .0 - .checked_mul(NANOSECONDS_IN_MINUTE) - .ok_or("Overflow: too large expiry time".to_string())?, - ); - - let todo_remove = CHAT_TO_MESSAGE_EXPIRY_SETTING - .with_borrow_mut(|expiry_settings| expiry_settings.insert(chat_id, expiry_time_nanos)); - - assert!(todo_remove.is_none()); - - Ok(group_chat_metadata) -} - -#[ic_cdk::update] -async fn chat_public_key(chat_id: ChatId, vetkey_epoch_id: VetKeyEpochId) -> serde_bytes::ByteBuf { - let request = VetKDPublicKeyArgs { - canister_id: None, - context: ratchet_context(chat_id, vetkey_epoch_id), - key_id: key_id(), - }; - - let result = ic_cdk::management_canister::vetkd_public_key(&request) - .await - .expect("call to vetkd_derive_key failed"); - - result.public_key.into() -} - -/// Derives a vetKey for an existing chat or creates a new one if the chat does not exist. -/// -/// # Arguments -/// * `chat_id`: The chat to derive a vetKey for. -/// * `opt_vetkey_epoch`: The vetKey epoch to derive a vetKey for. If `None`, a new epoch is created. -/// * `transport_key`: The transport key to derive a vetKey for. -/// -/// # Errors -/// * If the vetKey epoch has expired. -/// * If the user does not have access to the chat or vetKey epoch. -/// * If the user has already cached the key. -#[ic_cdk::update] -async fn derive_chat_vetkey( - chat_id: ChatId, - opt_vetkey_epoch_id: Option, - transport_key: serde_bytes::ByteBuf, -) -> Result { - let caller = ic_cdk::api::msg_caller(); - - let vetkey_epoch_id = opt_vetkey_epoch_id - .or_else(|| latest_vetkey_epoch_id(chat_id)) - .ok_or_else(|| format!("No chat {chat_id:?} found"))?; - - ensure_chat_and_vetkey_epoch_exist(chat_id, vetkey_epoch_id)?; - ensure_user_has_access_to_chat_at_epoch(caller, chat_id, vetkey_epoch_id)?; - ensure_user_has_no_cached_key_for_chat_and_vetkey_epoch(caller, chat_id, vetkey_epoch_id)?; - - let request = VetKDDeriveKeyArgs { - input: vec![], - context: ratchet_context(chat_id, vetkey_epoch_id), - key_id: key_id(), - transport_public_key: transport_key.into_vec(), - }; - - let result = ic_cdk::management_canister::vetkd_derive_key(&request) - .await - .expect("call to vetkd_derive_key failed"); - - Ok(result.encrypted_key.into()) -} - -#[ic_cdk::query] -fn get_latest_chat_vetkey_epoch_metadata(chat_id: ChatId) -> Result { - let caller = ic_cdk::api::msg_caller(); - - let latest_epoch_metadata = - latest_vetkey_epoch_metadata(chat_id).ok_or(format!("No chat {chat_id:?} found"))?; - ensure_chat_and_vetkey_epoch_exist(chat_id, latest_epoch_metadata.epoch_id)?; - ensure_user_has_access_to_chat_at_epoch(caller, chat_id, latest_epoch_metadata.epoch_id)?; - - Ok(latest_epoch_metadata) -} - -#[ic_cdk::query] -fn get_vetkey_epoch_metadata( - chat_id: ChatId, - vetkey_epoch_id: VetKeyEpochId, -) -> Result { - let caller = ic_cdk::api::msg_caller(); - - ensure_user_has_access_to_chat_at_epoch(caller, chat_id, vetkey_epoch_id)?; - - let epoch_metadata = CHAT_TO_VETKEYS_METADATA - .with_borrow(|metadata| { - metadata - .range(&(chat_id, Time(0))..) - .take_while(|metadata| metadata.key().0 == chat_id) - .filter(|metadata| metadata.value().epoch_id == vetkey_epoch_id) - .last() - .map(|metadata| metadata.value()) - }) - .ok_or(format!( - "No vetkey epoch {vetkey_epoch_id:?} found for chat {chat_id:?}" - ))?; - - Ok(epoch_metadata) -} - -#[ic_cdk::update] -fn rotate_chat_vetkey(chat_id: ChatId) -> Result { - let caller: Principal = ic_cdk::api::msg_caller(); - let now = Time(ic_cdk::api::time()); - - let latest_epoch_metadata = - latest_vetkey_epoch_metadata(chat_id).ok_or(format!("No chat {chat_id:?} found"))?; - ensure_user_has_access_to_chat_at_epoch(caller, chat_id, latest_epoch_metadata.epoch_id)?; - - let messages_start_with_id = CHAT_TO_MESSAGE_COUNTERS.with_borrow(|counters| { - counters - .get(&chat_id) - .expect("bug: uninitialized chat message counter") - }); - - let new_vetkey_epoch_id = CHAT_TO_VETKEYS_METADATA.with_borrow_mut(|metadata| { - let new_vetkey_epoch_id = VetKeyEpochId(latest_epoch_metadata.epoch_id.0 + 1); - let new_vetkey_epoch_metadata = VetKeyEpochMetadata { - epoch_id: new_vetkey_epoch_id, - creation_timestamp: now, - messages_start_with_id, - ..latest_epoch_metadata - }; - - for participant in new_vetkey_epoch_metadata.participants.iter().copied() { - USER_TO_CHAT_MAP.with_borrow_mut(|map| { - let todo_remove = map.insert((participant, chat_id, new_vetkey_epoch_id), ()); - assert!(todo_remove.is_none()); - }); - } - - let todo_remove = metadata.insert((chat_id, now), new_vetkey_epoch_metadata); - assert!(todo_remove.is_none()); - - clean_up_expired_vetkey_epochs(metadata, chat_id); - - new_vetkey_epoch_id - }); - - Ok(new_vetkey_epoch_id) -} - -#[ic_cdk::update] -fn send_direct_message(user_message: UserMessage, receiver: Principal) -> Result { - let caller = ic_cdk::api::msg_caller(); - let direct_chat_id = DirectChatId::new((caller, receiver)); - let chat_id = ChatId::Direct(direct_chat_id); - - ensure_chat_and_vetkey_epoch_exist(chat_id, user_message.vetkey_epoch)?; - ensure_user_has_access_to_chat_at_epoch(caller, chat_id, user_message.vetkey_epoch)?; - ensure_latest_and_correct_vetkey_and_symmetric_key_epoch( - chat_id, - user_message.vetkey_epoch, - user_message.symmetric_key_epoch, - )?; - ensure_nonce_is_unique(chat_id, user_message.nonce)?; - - let now = Time(ic_cdk::api::time()); - - let chat_message_id = CHAT_TO_MESSAGE_COUNTERS.with_borrow_mut(|counters| { - let chat_message_id = counters - .get(&chat_id) - .expect("bug: uninitialized chat message counter"); - counters.insert(chat_id, ChatMessageId(chat_message_id.0 + 1)); - chat_message_id - }); - - let stored_message = EncryptedMessage { - content: user_message.content, - metadata: EncryptedMessageMetadata { - sender: caller, - timestamp: now, - vetkey_epoch: user_message.vetkey_epoch, - symmetric_key_epoch: user_message.symmetric_key_epoch, - chat_message_id, - nonce: user_message.nonce, - }, - }; - - SET_CHAT_AND_SENDER_AND_USER_MESSAGE_ID.with_borrow_mut(|message_times| { - message_times.insert((chat_id, Sender(caller), user_message.nonce), ()); - }); - - DIRECT_CHAT_MESSAGES.with_borrow_mut(|messages| { - messages.insert((direct_chat_id, chat_message_id), stored_message.clone()); - }); - - let expiry_time = CHAT_TO_MESSAGE_EXPIRY_SETTING.with_borrow(|expiry_settings| { - expiry_settings - .get(&chat_id) - .expect("bug: uninitialized expiry setting") - }); - - EXPIRING_MESSAGES.with_borrow_mut(|expiring_messages| { - let todo_insert = - expiring_messages.insert((Time(now.0 + expiry_time.0), chat_id, chat_message_id), ()); - assert!(todo_insert.is_none()); - }); - - Ok(now) -} - -#[ic_cdk::update] -fn send_group_message( - user_message: UserMessage, - group_chat_id: GroupChatId, -) -> Result { - let caller = ic_cdk::api::msg_caller(); - let chat_id = ChatId::Group(group_chat_id); - - ensure_chat_and_vetkey_epoch_exist(chat_id, user_message.vetkey_epoch)?; - ensure_user_has_access_to_chat_at_epoch(caller, chat_id, user_message.vetkey_epoch)?; - ensure_latest_and_correct_vetkey_and_symmetric_key_epoch( - chat_id, - user_message.vetkey_epoch, - user_message.symmetric_key_epoch, - )?; - ensure_nonce_is_unique(chat_id, user_message.nonce)?; - - let now = Time(ic_cdk::api::time()); - - let chat_message_id = CHAT_TO_MESSAGE_COUNTERS.with_borrow_mut(|counters| { - let chat_message_id = counters - .get(&chat_id) - .expect("bug: uninitialized chat message counter"); - counters.insert(chat_id, ChatMessageId(chat_message_id.0 + 1)); - chat_message_id - }); - - let stored_message = EncryptedMessage { - content: user_message.content, - metadata: EncryptedMessageMetadata { - sender: caller, - timestamp: now, - vetkey_epoch: user_message.vetkey_epoch, - symmetric_key_epoch: user_message.symmetric_key_epoch, - chat_message_id, - nonce: user_message.nonce, - }, - }; - - SET_CHAT_AND_SENDER_AND_USER_MESSAGE_ID.with_borrow_mut(|message_times| { - message_times.insert((chat_id, Sender(caller), user_message.nonce), ()); - }); - - GROUP_CHAT_MESSAGES.with_borrow_mut(|messages| { - messages.insert((group_chat_id, chat_message_id), stored_message); - }); - - let expiry_time = CHAT_TO_MESSAGE_EXPIRY_SETTING.with_borrow(|expiry_settings| { - expiry_settings - .get(&chat_id) - .expect("bug: uninitialized expiry setting") - }); - - EXPIRING_MESSAGES.with_borrow_mut(|expiring_messages| { - let todo_insert = - expiring_messages.insert((Time(now.0 + expiry_time.0), chat_id, chat_message_id), ()); - assert!(todo_insert.is_none()); - }); - - Ok(now) -} - -#[ic_cdk::query] -fn get_my_chat_ids() -> Vec<(ChatId, ChatMessageId)> { - let caller = ic_cdk::api::msg_caller(); - USER_TO_CHAT_MAP.with_borrow(|map| { - CHAT_TO_MESSAGE_COUNTERS.with_borrow(|counters| { - map.keys_range((caller, ChatId::MIN_VALUE, VetKeyEpochId(0))..) - .take_while(|(user, _, _)| user == &caller) - .map(|(_, chat_id, _)| { - ( - chat_id, - ChatMessageId( - counters - .get(&chat_id) - .expect("bug: uninitialized chat message counter") - .0, - ), - ) - }) - .collect::>() - .into_iter() - .collect() - }) - }) -} - -/// Returns messages for a chat starting from a given message id. -/// -/// # Arguments -/// * `chat_id`: The chat to get messages for. -/// * `message_id`: The message id to start from. -/// * `limit`: The maximum number of messages to return. -/// -/// # Notes -/// * Does not fail if the chat does not exist or the user has no access -- returns empty vector instead. -#[ic_cdk::query] -fn get_messages( - chat_id: ChatId, - message_id: ChatMessageId, - limit: Option, -) -> Vec { - let caller = ic_cdk::api::msg_caller(); - - match chat_id { - ChatId::Direct(direct_chat) => DIRECT_CHAT_MESSAGES.with_borrow(|messages| { - if direct_chat.0 == caller || direct_chat.1 == caller { - messages - .range(&(direct_chat, message_id)..) - .take_while(|kv| kv.key().0 == direct_chat) - .map(|kv| kv.value()) - .filter(|message| { - ensure_user_has_access_to_chat_at_epoch( - caller, - chat_id, - message.metadata.vetkey_epoch, - ) - .is_ok() - }) - .take(limit.unwrap_or(u32::MAX) as usize) - .collect() - } else { - vec![] - } - }), - ChatId::Group(group_chat) => GROUP_CHAT_MESSAGES.with_borrow(|messages| { - messages - .range(&(group_chat, message_id)..) - .take_while(|kv| kv.key().0 == group_chat) - .map(|kv| kv.value()) - .filter(|message| { - ensure_user_has_access_to_chat_at_epoch( - caller, - chat_id, - message.metadata.vetkey_epoch, - ) - .is_ok() - }) - .take(limit.unwrap_or(u32::MAX) as usize) - .collect() - }), - } -} - -fn ensure_latest_and_correct_vetkey_and_symmetric_key_epoch( - chat_id: ChatId, - vetkey_epoch_id: VetKeyEpochId, - symmetric_key_epoch_id: SymmetricKeyEpochId, -) -> Result<(), String> { - let latest_vetkey_epoch_metadata = latest_vetkey_epoch_metadata(chat_id) - .ok_or(format!("No vetkey epoch found for chat {chat_id:?}"))?; - - if vetkey_epoch_id != latest_vetkey_epoch_metadata.epoch_id { - return Err(format!( - "Wrong vetKey epoch: expected {:?} but got {:?}", - latest_vetkey_epoch_metadata.epoch_id, vetkey_epoch_id - )); - } - - let now = ic_cdk::api::time(); - let creation = latest_vetkey_epoch_metadata.creation_timestamp.0; - let rotation: u64 = latest_vetkey_epoch_metadata - .symmetric_key_rotation_duration - .0; - let epoch_offset = rotation - .checked_mul(symmetric_key_epoch_id.0) - .ok_or(format!( - "Overflow: too large epoch id ({}) or rotation duration ({rotation})", - symmetric_key_epoch_id.0 - ))?; - let epoch_start = creation.checked_add(epoch_offset).ok_or(format!( - "Overflow: too large creation date ({creation}) or epoch offset ({epoch_offset})" - ))?; - let epoch_end = epoch_start.checked_add(rotation).ok_or(format!( - "Overflow: too large epoch start ({epoch_start}) or rotation duration ({rotation})" - ))?; - - if now < epoch_start { - return Err(format!( - "Wrong symmetric key epoch {:?} is not yet active, current time is {now} and epoch start is {epoch_start}", - symmetric_key_epoch_id.0 - )); - } - - if epoch_end <= now { - return Err(format!( - "Wrong symmetric key epoch: epoch {:?} is expired, current time is {now} and epoch end is {epoch_end}", - symmetric_key_epoch_id.0 - )); - } - Ok(()) -} - -#[ic_cdk::update] -fn update_my_symmetric_key_cache( - chat_id: ChatId, - vetkey_epoch_id: VetKeyEpochId, - user_cache: EncryptedSymmetricKeyEpochCache, -) -> Result<(), String> { - let caller = ic_cdk::api::msg_caller(); - ensure_chat_and_vetkey_epoch_exist(chat_id, vetkey_epoch_id)?; - ensure_user_has_access_to_chat_at_epoch(caller, chat_id, vetkey_epoch_id)?; - ensure_payload_has_reasonable_size_for_key(&user_cache.0)?; - - ENCRYPTED_MAPS.with_borrow_mut(|opt_maps| { - let maps = opt_maps - .as_mut() - .expect("bug: encrypted maps should be initialized after canister initialization"); - let _ = maps - .insert_encrypted_value( - caller, - map_id(caller), - map_key_id(chat_id, vetkey_epoch_id), - ic_vetkeys::types::ByteBuf::from(user_cache.0), - ) - .expect("bug: failed to insert encrypted value"); - }); - - let now = Time(ic_cdk::api::time()); - EXPIRING_VETKEY_EPOCHS_CACHES.with_borrow_mut(|caches| { - caches.insert((now, chat_id, caller), vetkey_epoch_id); - }); - - RESHARED_VETKEYS.with_borrow_mut(|reshared_vetkeys| { - let _ = reshared_vetkeys.remove(&(chat_id, vetkey_epoch_id, caller)); - }); - - Ok(()) -} - -#[ic_cdk::update] -fn get_my_symmetric_key_cache( - chat_id: ChatId, - vetkey_epoch_id: VetKeyEpochId, -) -> Result, String> { - let caller = ic_cdk::api::msg_caller(); - ensure_chat_and_vetkey_epoch_exist(chat_id, vetkey_epoch_id)?; - ensure_user_has_access_to_chat_at_epoch(caller, chat_id, vetkey_epoch_id)?; - - ENCRYPTED_MAPS.with_borrow(|opt_maps| { - let maps = opt_maps - .as_ref() - .expect("bug: encrypted maps should be initialized after canister initialization"); - - maps.get_encrypted_value(caller, map_id(caller), map_key_id(chat_id, vetkey_epoch_id)) - .map(|opt_cache| opt_cache.map(|cache| EncryptedSymmetricKeyEpochCache(cache.into()))) - }) -} - -#[ic_cdk::update] -async fn get_encrypted_vetkey_for_my_cache_storage( - transport_key: serde_bytes::ByteBuf, -) -> serde_bytes::ByteBuf { - let caller: Principal = ic_cdk::api::msg_caller(); - let transport_key = ic_vetkeys::types::ByteBuf::from(transport_key.into_vec()); - - let encrypted_vetkey = ENCRYPTED_MAPS - .with_borrow(|opt_maps| { - opt_maps - .as_ref() - .expect("bug: encrypted maps should be initialized after canister initialization") - .get_encrypted_vetkey(caller, map_id(caller), transport_key) - .expect("bug: failed to get user's vetkey") - }) - .await; - - serde_bytes::ByteBuf::from(encrypted_vetkey.as_ref().to_vec()) -} - -#[ic_cdk::update] -async fn get_vetkey_verification_key_for_my_cache_storage() -> serde_bytes::ByteBuf { - let verification_key = ENCRYPTED_MAPS - .with_borrow(|opt_maps| { - opt_maps - .as_ref() - .expect("bug: encrypted maps should be initialized after canister initialization") - .get_vetkey_verification_key() - }) - .await; - - serde_bytes::ByteBuf::from(verification_key.as_ref().to_vec()) -} - -#[ic_cdk::update] -fn reshare_ibe_encrypted_vetkeys( - chat_id: ChatId, - vetkey_epoch_id: VetKeyEpochId, - users_and_encrypted_vetkeys: Vec<(Principal, IbeEncryptedVetKey)>, -) -> Result<(), String> { - let caller = ic_cdk::api::msg_caller(); - ensure_chat_and_vetkey_epoch_exist(chat_id, vetkey_epoch_id)?; - ensure_user_has_access_to_chat_at_epoch(caller, chat_id, vetkey_epoch_id)?; - - users_and_encrypted_vetkeys.iter().map(|(user, _encrypted_vetkey)| { - ensure_user_has_access_to_chat_at_epoch(*user, chat_id, vetkey_epoch_id)?; - ensure_user_has_no_cached_key_for_chat_and_vetkey_epoch(*user, chat_id, vetkey_epoch_id)?; - - if *user == caller { - return Err(format!("User {user} cannot reshare a vetkey with themselves")); - } - - RESHARED_VETKEYS.with_borrow_mut(|reshared_vetkeys| { - let resharing_exists = reshared_vetkeys.get(&(chat_id, vetkey_epoch_id, *user)).is_some(); - if resharing_exists { - Err(format!("User {user} already has a reshared key for chat {chat_id:?} at vetkey epoch {vetkey_epoch_id:?}")) - } - else { - Ok(()) - } - }) - }).collect::, String>>()?; - - for (user, encrypted_vetkey) in users_and_encrypted_vetkeys.into_iter() { - RESHARED_VETKEYS.with_borrow_mut(|reshared_vetkeys| { - let todo_remove_ = - reshared_vetkeys.insert((chat_id, vetkey_epoch_id, user), encrypted_vetkey); - assert!(todo_remove_.is_none()); - }); - } - Ok(()) -} - -#[ic_cdk::update] -fn get_my_reshared_ibe_encrypted_vetkey( - chat_id: ChatId, - vetkey_epoch_id: VetKeyEpochId, -) -> Result, String> { - let caller = ic_cdk::api::msg_caller(); - - ensure_chat_and_vetkey_epoch_exist(chat_id, vetkey_epoch_id)?; - - Ok(RESHARED_VETKEYS - .with_borrow(|reshared_vetkeys| reshared_vetkeys.get(&(chat_id, vetkey_epoch_id, caller)))) -} - -#[ic_cdk::update] -async fn get_vetkey_resharing_ibe_decryption_key( - transport_key: serde_bytes::ByteBuf, -) -> serde_bytes::ByteBuf { - let caller = ic_cdk::api::msg_caller(); - let args = ic_cdk::management_canister::VetKDDeriveKeyArgs { - input: vec![], - context: resharing_context(caller), - transport_public_key: transport_key.into_vec(), - key_id: key_id(), - }; - let result = ic_cdk::management_canister::vetkd_derive_key(&args) - .await - .unwrap(); - serde_bytes::ByteBuf::from(result.encrypted_key) -} - -#[ic_cdk::update] -async fn get_vetkey_resharing_ibe_encryption_key(user: Principal) -> serde_bytes::ByteBuf { - let args = ic_cdk::management_canister::VetKDPublicKeyArgs { - canister_id: None, - context: resharing_context(user), - key_id: key_id(), - }; - let result = ic_cdk::management_canister::vetkd_public_key(&args) - .await - .unwrap(); - - serde_bytes::ByteBuf::from(result.public_key) -} - -#[ic_cdk::update] -fn modify_group_chat_participants( - group_chat_id: GroupChatId, - group_modification: GroupModification, -) -> Result { - let caller = ic_cdk::api::msg_caller(); - let now = Time(ic_cdk::api::time()); - let chat_id = ChatId::Group(group_chat_id); - - if group_modification.add_participants.is_empty() - && group_modification.remove_participants.is_empty() - { - return Err("No modifications provided".to_string()); - } - - let latest_epoch_metadata = - latest_vetkey_epoch_metadata(chat_id).ok_or(format!("No chat {chat_id:?} found"))?; - ensure_user_has_access_to_chat_at_epoch(caller, chat_id, latest_epoch_metadata.epoch_id)?; - - for participant in group_modification.add_participants.iter() { - if latest_epoch_metadata.participants.contains(participant) { - return Err(format!( - "Participant {participant} is already a member of the group chat and cannot be added" - )); - } - } - - for participant in group_modification.remove_participants.iter() { - if !latest_epoch_metadata.participants.contains(participant) { - return Err(format!( - "Participant {participant} is not a member of the group chat and cannot be removed" - )); - } - } - - let mut new_participants = latest_epoch_metadata.participants.clone(); - new_participants.extend(group_modification.add_participants); - new_participants - .retain(|participant| !group_modification.remove_participants.contains(participant)); - new_participants.sort(); - - let messages_start_with_id = CHAT_TO_MESSAGE_COUNTERS.with_borrow(|counters| { - counters - .get(&chat_id) - .expect("bug: uninitialized chat message counter") - }); - - let new_vetkey_epoch_id = CHAT_TO_VETKEYS_METADATA.with_borrow_mut(|metadata| { - let new_vetkey_epoch_id = VetKeyEpochId(latest_epoch_metadata.epoch_id.0 + 1); - let new_vetkey_epoch_metadata = VetKeyEpochMetadata { - epoch_id: new_vetkey_epoch_id, - creation_timestamp: now, - participants: new_participants, - symmetric_key_rotation_duration: latest_epoch_metadata.symmetric_key_rotation_duration, - messages_start_with_id, - }; - - for participant in new_vetkey_epoch_metadata.participants.iter().copied() { - USER_TO_CHAT_MAP.with_borrow_mut(|map| { - let todo_remove = map.insert((participant, chat_id, new_vetkey_epoch_id), ()); - assert!(todo_remove.is_none()); - }); - } - - for participant in group_modification.remove_participants.iter() { - USER_TO_CHAT_MAP.with_borrow_mut(|map| { - let keys_to_remove = map - .keys_range((caller, chat_id, VetKeyEpochId(0))..) - .take_while(|(user, this_chat_id, _)| { - user == participant && *this_chat_id == chat_id - }) - .collect::>(); - for key_to_remove in keys_to_remove { - let todo_remove = map.remove(&key_to_remove); - assert!(todo_remove.is_some()); - } - }); - } - - let todo_remove = metadata.insert((chat_id, now), new_vetkey_epoch_metadata); - assert!(todo_remove.is_none()); - - clean_up_expired_vetkey_epochs(metadata, chat_id); - - new_vetkey_epoch_id - }); - - Ok(new_vetkey_epoch_id) -} - -fn start_expired_cleanup_timer_job_with_interval(secs: u64) { - let secs = std::time::Duration::from_secs(secs); - let _timer_id = ic_cdk_timers::set_timer_interval(secs, periodic_cleanup_of_expired_items); -} - -fn periodic_cleanup_of_expired_items() { - let now = Time(ic_cdk::api::time()); - - let mut num_expired_direct_messages: usize = 0; - let mut num_expired_group_messages: usize = 0; - let mut num_expired_vetkey_epochs_caches: usize = 0; - let mut num_expired_reshared_vetkeys: usize = 0; - - EXPIRING_MESSAGES.with_borrow_mut(|expiring_messages| { - let now = Time(ic_cdk::api::time()); - let expired_messages: Vec<_> = expiring_messages - .iter() - .filter(|entry| entry.key().0 <= now) - .map(|entry| *entry.key()) - .collect(); - for key in expired_messages { - let todo_remove = expiring_messages.remove(&key); - assert!(todo_remove.is_some()); - - match key.1 { - ChatId::Direct(chat_id) => { - num_expired_direct_messages += 1; - DIRECT_CHAT_MESSAGES.with_borrow_mut(|messages| { - let todo_remove = messages.remove(&(chat_id, key.2)); - assert!(todo_remove.is_some()); - }); - } - ChatId::Group(group_chat_id) => { - num_expired_group_messages += 1; - GROUP_CHAT_MESSAGES.with_borrow_mut(|messages| { - let todo_remove = messages.remove(&(group_chat_id, key.2)); - assert!(todo_remove.is_some()); - }); - } - } - } - }); - - EXPIRING_VETKEY_EPOCHS_CACHES.with_borrow_mut(|expiring_vetkey_epochs_caches| { - let mut expired_vetkey_epochs = std::collections::BTreeSet::new(); - let expired_vetkey_epochs_caches: Vec<_> = expiring_vetkey_epochs_caches - .iter() - .filter(|entry| entry.key().0 <= now) - .map(|entry| (*entry.key(), entry.value())) - .collect(); - for ((time, chat_id, principal), vetkey_epoch_id) in expired_vetkey_epochs_caches { - expired_vetkey_epochs.insert((chat_id, vetkey_epoch_id)); - let todo_remove_1 = expiring_vetkey_epochs_caches.remove(&(time, chat_id, principal)); - assert!(todo_remove_1.is_some()); - - ENCRYPTED_MAPS.with_borrow_mut(|opt_maps| { - let maps = opt_maps.as_mut().expect( - "bug: encrypted maps should be initialized after canister initialization", - ); - if maps - .remove_encrypted_value( - principal, - map_id(principal), - map_key_id(chat_id, vetkey_epoch_id), - ) - .unwrap() - .is_some() - { - num_expired_vetkey_epochs_caches += 1 - } - }); - } - - for (chat_id, vetkey_epoch_id) in expired_vetkey_epochs { - RESHARED_VETKEYS.with_borrow_mut(|reshared_vetkeys| { - let reshared_vetkeys_to_remove: Vec<_> = reshared_vetkeys - .range(&(chat_id, vetkey_epoch_id, Principal::management_canister())..) - .filter(|entry| entry.key().0 == chat_id && entry.key().1 == vetkey_epoch_id) - .map(|entry| *entry.key()) - .collect(); - for key in reshared_vetkeys_to_remove { - let todo_remove = reshared_vetkeys.remove(&key); - assert!(todo_remove.is_some()); - num_expired_reshared_vetkeys += 1; - } - }); - } - }); - - ic_cdk::println!( - "Timer job: cleaned up {} expired direct messages, {} expired group messages, {} expired vetkey epochs caches, {} expired reshared vetkeys", - num_expired_direct_messages, - num_expired_group_messages, - num_expired_vetkey_epochs_caches, - num_expired_reshared_vetkeys - ); -} - -fn clean_up_expired_vetkey_epochs( - metadata: &mut StableBTreeMap<(ChatId, Time), VetKeyEpochMetadata, Memory>, - chat_id: ChatId, -) { - let now = Time(ic_cdk::api::time()); - let message_expiry_setting = CHAT_TO_MESSAGE_EXPIRY_SETTING - .with_borrow(|expiry_settings| expiry_settings.get(&chat_id)) - .expect("bug: expiry should always exist for existing chats"); - - let expired_epochs: Vec<_> = metadata - .range((chat_id, Time(0))..) - .take_while(|metadata| { - (metadata.key().1 .0 + message_expiry_setting.0) < now.0 && metadata.key().0 == chat_id - }) - .map(|metadata| metadata.value()) - .collect(); - - for epoch in expired_epochs { - let todo_remove = metadata.remove(&(chat_id, epoch.creation_timestamp)); - assert!(todo_remove.is_some()); - } -} - -fn ensure_user_has_access_to_chat_at_epoch( - user: Principal, - chat_id: ChatId, - vetkey_epoch_id: VetKeyEpochId, -) -> Result<(), String> { - let result = USER_TO_CHAT_MAP.with_borrow(|chats| chats.get(&(user, chat_id, vetkey_epoch_id))); - - result.ok_or(format!( - "User {} does not have access to chat {:?} at epoch {:?}", - user, chat_id, vetkey_epoch_id - )) -} - -fn ensure_user_has_no_cached_key_for_chat_and_vetkey_epoch( - user: Principal, - chat_id: ChatId, - vetkey_epoch_id: VetKeyEpochId, -) -> Result<(), String> { - let cache_exists = ENCRYPTED_MAPS.with_borrow(|opt_maps| { - let maps = opt_maps - .as_ref() - .expect("bug: encrypted maps should be initialized after canister initialization"); - let map_id = ( - user, - ic_stable_structures::storable::Blob::<32>::from_bytes(Cow::Borrowed( - b"encrypted_chat_cache", - )), - ); - let map_key_id = map_key_id(chat_id, vetkey_epoch_id); - maps.get_encrypted_value(user, map_id, map_key_id) - .expect("bug: failed to get encrypted value") - .is_some() - }); - if cache_exists { - Err(format!( - "User {} already has a cached key for chat {:?} at vetkey epoch {:?}", - user, chat_id, vetkey_epoch_id - )) - } else { - Ok(()) - } -} - -fn ensure_chat_and_vetkey_epoch_exist( - chat_id: ChatId, - vetkey_epoch_id: VetKeyEpochId, -) -> Result<(), String> { - let _ = latest_vetkey_epoch_id(chat_id).ok_or(format!("No chat {chat_id:?} found"))?; - - CHAT_TO_VETKEYS_METADATA.with_borrow(|metadata| { - metadata - .range(&(chat_id, Time(0))..) - .take_while(|metadata| metadata.key().0 == chat_id) - .find(|metadata| metadata.value().epoch_id == vetkey_epoch_id) - .map(|_| ()) - .ok_or(format!( - "vetKey epoch {vetkey_epoch_id:?} not found for chat {chat_id:?}" - )) - }) -} - -fn ensure_nonce_is_unique(chat_id: ChatId, nonce: Nonce) -> Result<(), String> { - let caller = ic_cdk::api::msg_caller(); - let maybe_existing_id = SET_CHAT_AND_SENDER_AND_USER_MESSAGE_ID - .with_borrow(|message_ids| message_ids.get(&(chat_id, Sender(caller), nonce))); - - match maybe_existing_id { - Some(_) => Err(format!( - "Message {nonce:?} already exists for sender {caller} chat {chat_id:?}" - )), - None => Ok(()), - } -} - -fn ensure_payload_has_reasonable_size_for_key(payload: &[u8]) -> Result<(), String> { - if payload.len() > 200 { - Err(format!( - "Payload is way too large: expected <= 200 B, got {} B", - payload.len() - )) - } else { - Ok(()) - } -} - -fn map_id(caller: Principal) -> (Principal, ic_stable_structures::storable::Blob<32>) { - ( - caller, - ic_stable_structures::storable::Blob::<32>::from_bytes(Cow::Borrowed( - b"encrypted_chat_cache", - )), - ) -} - -fn map_key_id( - chat_id: ChatId, - vetkey_epoch_id: VetKeyEpochId, -) -> ic_stable_structures::storable::Blob<32> { - ic_stable_structures::storable::Blob::<32>::from_bytes(Cow::Owned( - sha2::Sha256::digest( - chat_id - .to_bytes() - .iter() - .cloned() - .chain(vetkey_epoch_id.to_bytes().iter().cloned()) - .collect::>(), - ) - .to_vec(), - )) -} - -fn latest_vetkey_epoch_id(chat_id: ChatId) -> Option { - CHAT_TO_VETKEYS_METADATA.with_borrow(|metadata| { - metadata - .range(&(chat_id, Time(0))..) - .take_while(|metadata| metadata.key().0 == chat_id) - .last() - .map(|metadata| metadata.value().epoch_id) - }) -} - -fn latest_vetkey_epoch_metadata(chat_id: ChatId) -> Option { - CHAT_TO_VETKEYS_METADATA.with_borrow(|metadata| { - metadata - .range(&(chat_id, Time(0))..) - .take_while(|metadata| metadata.key().0 == chat_id) - .last() - .map(|metadata| metadata.value()) - }) -} - -fn key_id() -> VetKDKeyId { - let name = VETKD_KEY_NAME.with(|name| name.borrow().get().clone()); - VetKDKeyId { - curve: VetKDCurve::Bls12_381_G2, - name, - } -} - -pub fn ratchet_context(chat_id: ChatId, vetkey_epoch_id: VetKeyEpochId) -> Vec { - let chat_id_bytes = chat_id.to_bytes(); - let mut context = vec![]; - - context.extend_from_slice(&[DOMAIN_SEPARATOR_VETKEY_ROTATION.len() as u8]); - context.extend_from_slice(DOMAIN_SEPARATOR_VETKEY_ROTATION.as_bytes()); - - context.extend_from_slice(&[chat_id_bytes.len() as u8]); - context.extend_from_slice(&chat_id_bytes); - - context.extend_from_slice(&vetkey_epoch_id.0.to_le_bytes()); - - context -} - -pub fn resharing_context(caller: Principal) -> Vec { - let mut context = vec![]; - - context.extend_from_slice(&[DOMAIN_SEPARATOR_VETKEY_RESHARING.len() as u8]); - context.extend_from_slice(DOMAIN_SEPARATOR_VETKEY_ROTATION.as_bytes()); - - context.extend_from_slice(caller.as_slice()); - - context -} - -ic_cdk::export_candid!(); diff --git a/rust/vetkeys/encrypted_chat/rust/backend/src/types.rs b/rust/vetkeys/encrypted_chat/rust/backend/src/types.rs deleted file mode 100644 index fe5dd998f..000000000 --- a/rust/vetkeys/encrypted_chat/rust/backend/src/types.rs +++ /dev/null @@ -1,321 +0,0 @@ -use candid::{CandidType, Principal}; -use ic_stable_structures::storable::{Bound, Storable}; -use serde::{Deserialize, Serialize}; -use std::borrow::Cow; - -macro_rules! storable_unbounded { - ($name:ident) => { - impl Storable for $name { - fn to_bytes(&self) -> Cow<[u8]> { - Cow::Owned(serde_cbor::to_vec(self).expect("failed to serialize")) - } - - fn into_bytes(self) -> Vec { - self.to_bytes().into_owned() - } - - fn from_bytes(bytes: Cow<[u8]>) -> Self { - serde_cbor::from_slice(&bytes).expect("failed to deserialize") - } - - const BOUND: Bound = Bound::Unbounded; - } - }; -} - -macro_rules! storable_delegate { - ($name:ident, $t:ident) => { - impl Storable for $name { - fn to_bytes(&self) -> Cow<'_, [u8]> { - self.0.to_bytes() - } - - fn into_bytes(self) -> Vec { - self.0.into_bytes() - } - - fn from_bytes(bytes: Cow<[u8]>) -> Self { - $name($t::from_bytes(bytes)) - } - - const BOUND: Bound = $t::BOUND; - } - }; -} - -#[derive(CandidType, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord, Clone, Debug)] -pub struct EncryptedMessage { - pub content: Vec, - pub metadata: EncryptedMessageMetadata, -} - -storable_unbounded!(EncryptedMessage); - -#[derive(CandidType, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord, Clone, Debug)] -pub struct EncryptedMessageMetadata { - pub sender: Principal, - /// timestamp when the message was received by the canister - determines the symmetric key epoch - pub timestamp: Time, - pub vetkey_epoch: VetKeyEpochId, - pub symmetric_key_epoch: SymmetricKeyEpochId, - pub chat_message_id: ChatMessageId, - pub nonce: Nonce, -} - -impl Storable for EncryptedMessageMetadata { - fn to_bytes(&self) -> Cow<'_, [u8]> { - let mut bytes = Vec::new(); - bytes.extend_from_slice(self.sender.as_slice()); - bytes.extend_from_slice(&self.timestamp.0.to_le_bytes()); - bytes.extend_from_slice(&self.vetkey_epoch.0.to_le_bytes()); - bytes.extend_from_slice(&self.symmetric_key_epoch.0.to_le_bytes()); - bytes.extend_from_slice(&self.chat_message_id.0.to_le_bytes()); - bytes.extend_from_slice(&self.nonce.0.to_le_bytes()); - Cow::Owned(bytes) - } - - fn into_bytes(self) -> Vec { - self.to_bytes().into_owned() - } - - #[inline] - fn from_bytes(bytes: Cow<[u8]>) -> Self { - let (sender_bytes, rest) = bytes.as_ref().split_at(Principal::MAX_LENGTH_IN_BYTES); - let sender = Principal::from_slice(sender_bytes); - - let (timestamp_bytes, rest) = rest.split_at(8); - let timestamp = Time(u64::from_le_bytes(timestamp_bytes.try_into().unwrap())); - - let (vetkey_epoch_bytes, rest) = rest.split_at(8); - let vetkey_epoch = - VetKeyEpochId(u64::from_le_bytes(vetkey_epoch_bytes.try_into().unwrap())); - - let (symmetric_key_epoch_bytes, rest) = rest.split_at(8); - let symmetric_key_epoch = SymmetricKeyEpochId(u64::from_le_bytes( - symmetric_key_epoch_bytes.try_into().unwrap(), - )); - - let (chat_message_id_bytes, nonce_bytes) = rest.split_at(8); - let chat_message_id = ChatMessageId(u64::from_le_bytes( - chat_message_id_bytes.try_into().unwrap(), - )); - - let nonce = Nonce(u64::from_le_bytes(nonce_bytes.try_into().unwrap())); - - Self { - sender, - timestamp, - vetkey_epoch, - symmetric_key_epoch, - chat_message_id, - nonce, - } - } - - const BOUND: Bound = Bound::Bounded { - max_size: Principal::MAX_LENGTH_IN_BYTES as u32 + 4 * 8, - is_fixed_size: false, - }; -} - -#[derive(CandidType, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord, Clone, Debug)] -pub struct UserMessage { - pub content: Vec, - pub vetkey_epoch: VetKeyEpochId, - pub symmetric_key_epoch: SymmetricKeyEpochId, - pub nonce: Nonce, -} - -storable_unbounded!(UserMessage); - -#[derive( - CandidType, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord, Clone, Debug, Copy, -)] -pub struct SymmetricKeyEpochId(pub u64); - -storable_delegate!(SymmetricKeyEpochId, u64); - -#[derive( - CandidType, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord, Clone, Debug, Copy, -)] -pub struct DirectChatId(pub(crate) Principal, pub(crate) Principal); - -impl Storable for DirectChatId { - fn to_bytes(&self) -> Cow<'_, [u8]> { - Cow::Owned( - self.0 - .as_slice() - .iter() - .chain(self.1.as_slice().iter()) - .cloned() - .collect(), - ) - } - - fn into_bytes(self) -> Vec { - self.to_bytes().into_owned() - } - - fn from_bytes(bytes: Cow<[u8]>) -> Self { - let (a_bytes, b_bytes) = bytes.as_ref().split_at(Principal::MAX_LENGTH_IN_BYTES); - let a = Principal::from_slice(a_bytes); - let b = Principal::from_slice(b_bytes); - Self(a, b) - } - - const BOUND: Bound = Bound::Bounded { - max_size: 2 * Principal::MAX_LENGTH_IN_BYTES as u32, - is_fixed_size: false, - }; -} - -impl DirectChatId { - pub fn new(participants: (Principal, Principal)) -> Self { - let (a, b) = participants; - let (a, b) = if a < b { (a, b) } else { (b, a) }; - Self(a, b) - } -} - -#[derive( - CandidType, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord, Clone, Debug, Copy, -)] -pub struct GroupChatMetadata { - pub chat_id: GroupChatId, - pub creation_timestamp: Time, -} - -storable_unbounded!(GroupChatMetadata); - -#[derive( - CandidType, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord, Clone, Debug, Copy, -)] -pub struct GroupChatId(pub u64); - -storable_delegate!(GroupChatId, u64); - -#[derive(CandidType, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord, Clone, Debug)] -pub struct EncryptedSymmetricKeyEpochCache(#[serde(with = "serde_bytes")] pub Vec); - -storable_unbounded!(EncryptedSymmetricKeyEpochCache); - -#[derive( - CandidType, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord, Clone, Debug, Copy, -)] -pub struct Time(pub u64); - -storable_delegate!(Time, u64); - -#[derive(CandidType, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord, Clone, Debug)] -pub struct VetKeyEpochMetadata { - pub epoch_id: VetKeyEpochId, - pub participants: Vec, - pub creation_timestamp: Time, - pub symmetric_key_rotation_duration: Time, - pub messages_start_with_id: ChatMessageId, -} - -storable_unbounded!(VetKeyEpochMetadata); - -#[derive( - CandidType, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord, Clone, Debug, Copy, -)] -pub enum ChatId { - Direct(DirectChatId), - Group(GroupChatId), -} - -impl ChatId { - pub const MIN_VALUE: Self = Self::Direct(DirectChatId( - Principal::management_canister(), - Principal::management_canister(), - )); -} - -impl Storable for ChatId { - fn to_bytes(&self) -> Cow<'_, [u8]> { - let result = match self { - ChatId::Direct(id) => [0].iter().chain(id.to_bytes().iter()).cloned().collect(), - ChatId::Group(id) => [1].iter().chain(id.to_bytes().iter()).cloned().collect(), - }; - - Cow::Owned(result) - } - - fn into_bytes(self) -> Vec { - self.to_bytes().into_owned() - } - - fn from_bytes(bytes: Cow<[u8]>) -> Self { - match bytes.as_ref() { - [0, ..] => ChatId::Direct(DirectChatId::from_bytes(Cow::Borrowed( - &bytes.as_ref()[1..], - ))), - [1, ..] => ChatId::Group(GroupChatId::from_bytes(Cow::Borrowed(&bytes.as_ref()[1..]))), - _ => panic!("invalid chat id"), - } - } - - const BOUND: Bound = Bound::Bounded { - max_size: 1 + 2 * Principal::MAX_LENGTH_IN_BYTES as u32, - is_fixed_size: false, - }; -} - -/// User-assigned nonce used for message encryption. -#[derive( - CandidType, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord, Clone, Debug, Copy, -)] -pub struct Nonce(pub u64); - -storable_delegate!(Nonce, u64); - -/// Chat message id is assigned to each message in the chat sequentially. -/// The IDs are assigned from an incrementing counter for a chat for all users. -/// This is useful because user's messages can arrive out of order (which makes user's IDs unreliable) or arrive many at the same consensus time. -/// Therefore, it's important to be able to provide convenient pagination of chat messages, s.t. a user can retrieve a longer chat history iteratively. -#[derive( - CandidType, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord, Clone, Debug, Copy, -)] -pub struct ChatMessageId(pub u64); - -storable_delegate!(ChatMessageId, u64); - -#[derive( - CandidType, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord, Clone, Debug, Copy, -)] -pub struct Sender(pub Principal); - -storable_delegate!(Sender, Principal); - -#[derive( - CandidType, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord, Clone, Debug, Copy, -)] -pub struct VetKeyEpochId(pub u64); - -storable_delegate!(VetKeyEpochId, u64); - -#[derive(CandidType, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord, Clone, Debug)] -pub struct IbeEncryptedVetKey(#[serde(with = "serde_bytes")] pub Vec); - -impl Storable for IbeEncryptedVetKey { - fn to_bytes(&self) -> Cow<'_, [u8]> { - Cow::Borrowed(self.0.as_slice()) - } - - fn into_bytes(self) -> Vec { - self.0 - } - - fn from_bytes(bytes: Cow<[u8]>) -> Self { - Self(bytes.into_owned()) - } - - const BOUND: Bound = Bound::Unbounded; -} - -#[derive(CandidType, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord, Clone, Debug)] -pub struct GroupModification { - pub add_participants: Vec, - pub remove_participants: Vec, -} diff --git a/rust/vetkeys/encrypted_chat/rust/backend/tests/common/mod.rs b/rust/vetkeys/encrypted_chat/rust/backend/tests/common/mod.rs deleted file mode 100644 index 3ae1e89fa..000000000 --- a/rust/vetkeys/encrypted_chat/rust/backend/tests/common/mod.rs +++ /dev/null @@ -1,131 +0,0 @@ -use candid::{decode_one, encode_one, CandidType, Principal}; -use pocket_ic::{PocketIc, PocketIcBuilder}; -use rand::{CryptoRng, Rng, SeedableRng}; -use rand_chacha::ChaCha20Rng; -use std::path::Path; - -#[allow(dead_code)] -pub const NANOSECONDS_IN_MINUTE: u64 = 60_000_000_000; - -pub fn reproducible_rng() -> ChaCha20Rng { - let mut seed = [0u8; 32]; - rand::rng().fill(&mut seed); - let rng = ChaCha20Rng::from_seed(seed); - println!("{seed:?}"); - rng -} - -pub fn random_self_authenticating_principal(rng: &mut R) -> Principal { - let fake_pk = random_bytes(32, rng); - Principal::self_authenticating(&fake_pk) -} - -pub fn random_bytes(size: usize, rng: &mut R) -> Vec { - let mut buf = vec![0; size]; - rng.fill_bytes(&mut buf); - buf -} - -pub struct TestEnvironment { - pub pic: PocketIc, - pub canister_id: Principal, - #[allow(dead_code)] - pub principal_0: Principal, - #[allow(dead_code)] - pub principal_1: Principal, - #[allow(dead_code)] - pub principal_2: Principal, -} - -impl TestEnvironment { - pub fn new(rng: &mut R) -> Self { - let pic = PocketIcBuilder::new() - .with_application_subnet() - .with_ii_subnet() - .with_fiduciary_subnet() - .with_nonmainnet_features(true) - .build(); - - let canister_id = pic.create_canister(); - pic.add_cycles(canister_id, 2_000_000_000_000); - - let wasm_bytes = load_canister_wasm(); - pic.install_canister( - canister_id, - wasm_bytes, - encode_one("dfx_test_key").unwrap(), - None, - ); - - // Make sure the canister is properly initialized - fast_forward(&pic, 5); - - Self { - pic, - canister_id, - principal_0: random_self_authenticating_principal(rng), - principal_1: random_self_authenticating_principal(rng), - principal_2: random_self_authenticating_principal(rng), - } - } - - pub fn update candid::Deserialize<'de>>( - &self, - caller: Principal, - method_name: &str, - args: Vec, - ) -> T { - let reply = self - .pic - .update_call(self.canister_id, caller, method_name, args); - match reply { - Ok(data) => decode_one(&data).expect("failed to decode reply"), - Err(user_error) => panic!("canister returned a user error: {user_error}"), - } - } - - pub fn query candid::Deserialize<'de>>( - &self, - caller: Principal, - method_name: &str, - args: Vec, - ) -> T { - let reply = self - .pic - .query_call(self.canister_id, caller, method_name, args); - match reply { - Ok(data) => decode_one(&data).expect("failed to decode reply"), - Err(user_error) => panic!("canister returned a user error: {user_error}"), - } - } -} - -fn fast_forward(ic: &PocketIc, ticks: u64) { - for _ in 0..ticks - 1 { - ic.tick(); - } -} - -fn load_canister_wasm() -> Vec { - let wasm_path_string = match std::env::var("CUSTOM_WASM_PATH") { - Ok(path) if !path.is_empty() => path, - _ => format!( - "{}/examples/encrypted_chat/rust/target/wasm32-unknown-unknown/release/ic_vetkeys_example_encrypted_chat_backend.wasm", - git_root_dir() - ), - }; - let wasm_path = Path::new(&wasm_path_string); - std::fs::read(wasm_path) - .expect("wasm does not exist - run `cargo build --release --target wasm32-unknown-unknown`") -} - -pub fn git_root_dir() -> String { - let output = std::process::Command::new("git") - .args(["rev-parse", "--show-toplevel"]) - .output() - .expect("Failed to execute git command"); - assert!(output.status.success()); - let root_dir_with_newline = - String::from_utf8(output.stdout).expect("Failed to convert stdout to string"); - root_dir_with_newline.trim_end_matches('\n').to_string() -} diff --git a/rust/vetkeys/encrypted_chat/rust/backend/tests/direct_chat.rs b/rust/vetkeys/encrypted_chat/rust/backend/tests/direct_chat.rs deleted file mode 100644 index cea335cf6..000000000 --- a/rust/vetkeys/encrypted_chat/rust/backend/tests/direct_chat.rs +++ /dev/null @@ -1,1544 +0,0 @@ -use candid::{encode_args, Principal}; -use ic_vetkeys_example_encrypted_chat_backend::types::{ - ChatId, ChatMessageId, DirectChatId, EncryptedMessage, EncryptedMessageMetadata, - EncryptedSymmetricKeyEpochCache, IbeEncryptedVetKey, Nonce, SymmetricKeyEpochId, Time, - UserMessage, VetKeyEpochId, -}; -use serde_bytes::ByteBuf; - -mod common; -use common::*; - -#[test] -fn can_create_chat() { - let rng = &mut reproducible_rng(); - let env = TestEnvironment::new(rng); - - let p0_self_chat_id = ChatId::Direct(DirectChatId::new((env.principal_0, env.principal_0))); - let p0_p1_chat_id = ChatId::Direct(DirectChatId::new((env.principal_0, env.principal_1))); - - for p in [env.principal_0, env.principal_1] { - assert_eq!( - env.query::>( - p, - "get_my_chat_ids", - encode_args(()).unwrap() - ), - vec![] - ); - } - - env.update::>( - env.principal_0, - "create_direct_chat", - encode_args((env.principal_0, Time(1_000), Time(10_000))).unwrap(), - ) - .unwrap(); - - let chat_ids: Vec<(ChatId, ChatMessageId)> = - env.query(env.principal_0, "get_my_chat_ids", encode_args(()).unwrap()); - assert_eq!(chat_ids, vec![(p0_self_chat_id, ChatMessageId(0))]); - - assert_eq!( - env.query::>( - env.principal_1, - "get_my_chat_ids", - encode_args(()).unwrap() - ), - vec![] - ); - - env.update::>( - env.principal_0, - "create_direct_chat", - encode_args((env.principal_1, Time(1_000), Time(10_000))).unwrap(), - ) - .unwrap(); - - assert_eq!( - env.query::>( - env.principal_1, - "get_my_chat_ids", - encode_args(()).unwrap() - ), - vec![(p0_p1_chat_id, ChatMessageId(0))] - ); - - let chat_ids: Vec<(ChatId, ChatMessageId)> = - env.query(env.principal_0, "get_my_chat_ids", encode_args(()).unwrap()); - assert!(chat_ids.contains(&(p0_self_chat_id, ChatMessageId(0)))); - assert!(chat_ids.contains(&(p0_p1_chat_id, ChatMessageId(0)))); - assert_eq!(chat_ids.len(), 2); -} - -#[test] -fn fails_to_create_chat_with_same_participants_more_than_once() { - let rng = &mut reproducible_rng(); - let env = TestEnvironment::new(rng); - - env.update::>( - env.principal_0, - "create_direct_chat", - encode_args((env.principal_0, Time(1_000), Time(10_000))).unwrap(), - ) - .unwrap(); - - for _ in 0..3 { - assert_eq!( - env.update::>( - env.principal_0, - "create_direct_chat", - encode_args((env.principal_0, Time(1_000), Time(10_000))).unwrap(), - ), - Err(format!( - "Chat {:?} already exists", - ChatId::Direct(DirectChatId::new((env.principal_0, env.principal_0))) - )) - ); - } -} - -#[test] -fn can_send_and_get_messages() { - let rng = &mut reproducible_rng(); - let env = TestEnvironment::new(rng); - - env.update::>( - env.principal_0, - "create_direct_chat", - encode_args((env.principal_1, Time(1_000), Time(10_000))).unwrap(), - ) - .unwrap(); - - let message_content = b"dummy encrypted message".to_vec(); - - let mut message_id_counters = - std::collections::BTreeMap::from([(env.principal_0, 0), (env.principal_1, 0)]); - - let mut expected_chat_history = vec![]; - - let chat_id = ChatId::Direct(DirectChatId::new((env.principal_0, env.principal_1))); - - for caller in [env.principal_0, env.principal_1].iter().copied() { - assert_eq!( - env.update::>( - caller, - "get_messages", - encode_args((chat_id, ChatMessageId(0), Option::::None)).unwrap(), - ), - vec![] - ); - } - - for _ in 0..10 { - for sender in [env.principal_0, env.principal_1].iter().copied() { - let nonce_raw = *message_id_counters.get(&sender).unwrap(); - message_id_counters.insert(sender, nonce_raw + 1); - - let user_message = UserMessage { - content: message_content.clone(), - vetkey_epoch: VetKeyEpochId(0), - symmetric_key_epoch: SymmetricKeyEpochId(0), - nonce: Nonce(nonce_raw), - }; - - // + 1 is because the update call calls `tick` internally - let expected_message_time = env.pic.get_time().as_nanos_since_unix_epoch() + 1; - - let message_time = env - .update::>( - sender, - "send_direct_message", - encode_args(( - user_message, - if sender == env.principal_0 { - env.principal_1 - } else { - env.principal_0 - }, - )) - .unwrap(), - ) - .unwrap(); - - let expected_added_chat_message = EncryptedMessage { - content: message_content.clone(), - metadata: EncryptedMessageMetadata { - sender, - timestamp: message_time, - vetkey_epoch: VetKeyEpochId(0), - symmetric_key_epoch: SymmetricKeyEpochId(0), - chat_message_id: ChatMessageId(expected_chat_history.len() as u64), - nonce: Nonce(nonce_raw), - }, - }; - - expected_chat_history.push(expected_added_chat_message); - - for caller in [env.principal_0, env.principal_1].iter().copied() { - assert_eq!( - env.update::>( - caller, - "get_messages", - encode_args((chat_id, ChatMessageId(0), Option::::None)).unwrap(), - ), - expected_chat_history - ); - } - - assert_eq!(message_time.0, expected_message_time); - } - } -} - -#[test] -fn fails_to_send_messages_with_wrong_symmetric_key_epoch() { - let rng = &mut reproducible_rng(); - let env = TestEnvironment::new(rng); - - let symmetric_key_rotation_minutes = Time(1_000); - let chat_message_expiration_minutes = Time(10_000); - - let chat_creation_time = env - .update::>( - env.principal_0, - "create_direct_chat", - encode_args(( - env.principal_1, - symmetric_key_rotation_minutes, - chat_message_expiration_minutes, - )) - .unwrap(), - ) - .unwrap(); - - let message_content = b"dummy encrypted message".to_vec(); - let chat_id = ChatId::Direct(DirectChatId::new((env.principal_0, env.principal_1))); - - // check that epoch 1 fails while we have epoch 0 - for i in 0..2 { - for sender in [env.principal_0, env.principal_1].iter().copied() { - let symmetric_key_epoch = SymmetricKeyEpochId(1); - let user_message = UserMessage { - content: message_content.clone(), - vetkey_epoch: VetKeyEpochId(0), - symmetric_key_epoch, - nonce: Nonce(0), - }; - - let result = env.update::>( - sender, - "send_direct_message", - encode_args(( - user_message, - if sender == env.principal_0 { - env.principal_1 - } else { - env.principal_0 - }, - )) - .unwrap(), - ); - - assert_eq!( - result, - Err( - format!( - "Wrong symmetric key epoch {} is not yet active, current time is {} and epoch start is {}", - symmetric_key_epoch.0, - env.pic.get_time().as_nanos_since_unix_epoch(), - chat_creation_time.0 + symmetric_key_epoch.0 * symmetric_key_rotation_minutes.0 * NANOSECONDS_IN_MINUTE - ) - ) - ); - } - - // set time to 2 ns before the change to epoch 1 - if i == 0 { - env.pic - .set_time(pocket_ic::Time::from_nanos_since_unix_epoch( - chat_creation_time.0 + symmetric_key_rotation_minutes.0 * NANOSECONDS_IN_MINUTE - - 2, - )); - } - } - - // check that epoch 0 and 2 fails while we have epoch 1 - for i in 0..2 { - for sender in [env.principal_0, env.principal_1].iter().copied() { - // use epoch 0 - { - let symmetric_key_epoch = SymmetricKeyEpochId(0); - let user_message = UserMessage { - content: message_content.clone(), - vetkey_epoch: VetKeyEpochId(0), - symmetric_key_epoch, - nonce: Nonce(0), - }; - - let result = env.update::>( - sender, - "send_direct_message", - encode_args(( - user_message, - if sender == env.principal_0 { - env.principal_1 - } else { - env.principal_0 - }, - )) - .unwrap(), - ); - - assert_eq!( - result, - Err( - format!( - "Wrong symmetric key epoch: epoch {} is expired, current time is {} and epoch end is {}", - symmetric_key_epoch.0, - env.pic.get_time().as_nanos_since_unix_epoch(), - chat_creation_time.0 + symmetric_key_rotation_minutes.0 * NANOSECONDS_IN_MINUTE - ) - ) - ); - } - - // use epoch 2 - { - let symmetric_key_epoch = SymmetricKeyEpochId(2); - let user_message = UserMessage { - content: message_content.clone(), - vetkey_epoch: VetKeyEpochId(0), - symmetric_key_epoch, - nonce: Nonce(0), - }; - - let result = env.update::>( - sender, - "send_direct_message", - encode_args(( - user_message, - if sender == env.principal_0 { - env.principal_1 - } else { - env.principal_0 - }, - )) - .unwrap(), - ); - - assert_eq!( - result, - Err( - format!( - "Wrong symmetric key epoch {} is not yet active, current time is {} and epoch start is {}", - symmetric_key_epoch.0, - env.pic.get_time().as_nanos_since_unix_epoch(), - chat_creation_time.0 + symmetric_key_epoch.0 * symmetric_key_rotation_minutes.0 * NANOSECONDS_IN_MINUTE - ) - ) - ); - } - } - - // set time to 4 ns before the change to epoch 2 - if i == 0 { - env.pic - .set_time(pocket_ic::Time::from_nanos_since_unix_epoch( - chat_creation_time.0 - + 2 * symmetric_key_rotation_minutes.0 * NANOSECONDS_IN_MINUTE - - 4, - )); - } - } - - // sanity check that no messages were added - for caller in [env.principal_0, env.principal_1].iter().copied() { - assert_eq!( - env.update::>( - caller, - "get_messages", - encode_args((chat_id, ChatMessageId(0), Option::::None)).unwrap(), - ), - vec![] - ); - } -} - -#[test] -fn can_get_vetkey_for_chat() { - let rng = &mut reproducible_rng(); - let env = TestEnvironment::new(rng); - - env.update::>( - env.principal_0, - "create_direct_chat", - encode_args((env.principal_1, Time(1_000), Time(10_000))).unwrap(), - ) - .unwrap(); - let chat_id = ChatId::Direct(DirectChatId::new((env.principal_0, env.principal_1))); - - // DON'T REUSE THE SAME TRANSPORT KEYS IN PRODUCTION - let transport_key = ic_vetkeys::TransportSecretKey::from_seed(random_bytes(32, rng)).unwrap(); - - let mut raw_encrypted_vetkeys = std::collections::BTreeMap::new(); - - for latest_epoch in 0..3 { - for caller in [env.principal_0, env.principal_1] { - for epoch in 0..=latest_epoch { - for vetkey_epoch_id in [Option::::None, Some(VetKeyEpochId(epoch))] { - if vetkey_epoch_id.is_none() && epoch != latest_epoch { - continue; - } - - let raw_encrypted_vetkey = env - .update::>( - caller, - "derive_chat_vetkey", - encode_args(( - chat_id, - vetkey_epoch_id, - ByteBuf::from(transport_key.public_key()), - )) - .unwrap(), - ) - .unwrap() - .into_vec(); - let opt_evicted = - raw_encrypted_vetkeys.insert(epoch, raw_encrypted_vetkey.clone()); - if let Some(evicted) = opt_evicted { - assert_eq!(evicted, raw_encrypted_vetkey, "epoch: {epoch}, latest_epoch: {latest_epoch}, vetkey_epoch_id: {vetkey_epoch_id:?}"); - } - } - } - } - - let new_epoch = env - .update::>( - env.principal_0, - "rotate_chat_vetkey", - encode_args((chat_id,)).unwrap(), - ) - .unwrap(); - assert_eq!(new_epoch, VetKeyEpochId(latest_epoch + 1)); - } - - for (epoch, raw_encrypted_vetkey) in raw_encrypted_vetkeys.into_iter() { - let raw_public_key = env - .update::( - env.principal_0, - "chat_public_key", - encode_args((chat_id, VetKeyEpochId(epoch))).unwrap(), - ) - .into_vec(); - - let public_key = - ic_vetkeys::DerivedPublicKey::deserialize(raw_public_key.as_slice()).unwrap(); - - let _vetkey = ic_vetkeys::EncryptedVetKey::deserialize(&raw_encrypted_vetkey) - .unwrap() - .decrypt_and_verify(&transport_key, &public_key, &[]) - .unwrap(); - } -} - -#[test] -fn public_keys_for_different_chats_and_epochs_are_different() { - let rng = &mut reproducible_rng(); - let env = TestEnvironment::new(rng); - - let chat_id_0 = ChatId::Direct(DirectChatId::new((env.principal_0, env.principal_1))); - let chat_id_1 = ChatId::Direct(DirectChatId::new((env.principal_1, env.principal_2))); - - // we can get public key for any chats, also non-existing ones - let raw_public_key_00 = env - .update::( - env.principal_0, - "chat_public_key", - encode_args((chat_id_0, VetKeyEpochId(0))).unwrap(), - ) - .into_vec(); - - let raw_public_key_01 = env - .update::( - env.principal_0, - "chat_public_key", - encode_args((chat_id_0, VetKeyEpochId(1))).unwrap(), - ) - .into_vec(); - - let raw_public_key_10 = env - .update::( - env.principal_0, - "chat_public_key", - encode_args((chat_id_1, VetKeyEpochId(0))).unwrap(), - ) - .into_vec(); - - assert_ne!(raw_public_key_00, raw_public_key_01); - assert_ne!(raw_public_key_00, raw_public_key_10); -} - -#[test] -fn fails_to_get_vetkey_for_chat_if_unauthorized() { - let rng = &mut reproducible_rng(); - let env = TestEnvironment::new(rng); - - let transport_key = ic_vetkeys::TransportSecretKey::from_seed(random_bytes(32, rng)).unwrap(); - - env.update::>( - env.principal_0, - "create_direct_chat", - encode_args((env.principal_1, Time(1_000), Time(10_000))).unwrap(), - ) - .unwrap(); - - let chat_id = ChatId::Direct(DirectChatId::new((env.principal_0, env.principal_1))); - - let result = env.update::>( - env.principal_2, - "derive_chat_vetkey", - encode_args(( - chat_id, - Option::::None, - ByteBuf::from(transport_key.public_key()), - )) - .unwrap(), - ); - - assert_eq!( - result, - Err(format!( - "User {} does not have access to chat {chat_id:?} at epoch {:?}", - env.principal_2, - VetKeyEpochId(0) - )) - ); -} - -#[test] -fn fails_to_send_messages_with_wrong_vetkey_epoch() { - let rng = &mut reproducible_rng(); - let env = TestEnvironment::new(rng); - - let symmetric_key_rotation_minutes = Time(1_000); - let chat_message_expiration_minutes = Time(10_000); - - env.update::>( - env.principal_0, - "create_direct_chat", - encode_args(( - env.principal_1, - symmetric_key_rotation_minutes, - chat_message_expiration_minutes, - )) - .unwrap(), - ) - .unwrap(); - - let message_content = b"dummy encrypted message".to_vec(); - let chat_id = ChatId::Direct(DirectChatId::new((env.principal_0, env.principal_1))); - - // Start with using epoch 1 before it's been rotated to (should fail) - for latest_epoch in 0..3 { - for sender in [env.principal_0, env.principal_1].iter().copied() { - let user_message = UserMessage { - content: message_content.clone(), - vetkey_epoch: VetKeyEpochId(latest_epoch + 1), - symmetric_key_epoch: SymmetricKeyEpochId(0), - nonce: Nonce(0), - }; - - let result = env.update::>( - sender, - "send_direct_message", - encode_args(( - user_message, - if sender == env.principal_0 { - env.principal_1 - } else { - env.principal_0 - }, - )) - .unwrap(), - ); - - assert_eq!( - result, - Err(format!( - "vetKey epoch {:?} not found for chat {chat_id:?}", - VetKeyEpochId(latest_epoch + 1) - )) - ); - } - - // Rotate to next epoch - let new_epoch = env - .update::>( - env.principal_0, - "rotate_chat_vetkey", - encode_args((chat_id,)).unwrap(), - ) - .unwrap(); - assert_eq!(new_epoch, VetKeyEpochId(latest_epoch + 1)); - } - - // sanity check that no messages were added - for caller in [env.principal_0, env.principal_1].iter().copied() { - assert_eq!( - env.update::>( - caller, - "get_messages", - encode_args((chat_id, ChatMessageId(0), Option::::None)).unwrap(), - ), - vec![] - ); - } -} - -#[test] -fn fails_to_derive_vetkey_with_wrong_vetkey_epoch() { - let rng = &mut reproducible_rng(); - let env = TestEnvironment::new(rng); - - let transport_key = ic_vetkeys::TransportSecretKey::from_seed(random_bytes(32, rng)).unwrap(); - - env.update::>( - env.principal_0, - "create_direct_chat", - encode_args((env.principal_1, Time(1_000), Time(10_000))).unwrap(), - ) - .unwrap(); - - let chat_id = ChatId::Direct(DirectChatId::new((env.principal_0, env.principal_1))); - - // Use epoch 1 before it's been rotated to - for latest_epoch in 0..3 { - for caller in [env.principal_0, env.principal_1] { - let result = env.update::>( - caller, - "derive_chat_vetkey", - encode_args(( - chat_id, - Some(VetKeyEpochId(latest_epoch + 1)), - ByteBuf::from(transport_key.public_key()), - )) - .unwrap(), - ); - - assert_eq!( - result, - Err(format!( - "vetKey epoch {:?} not found for chat {chat_id:?}", - VetKeyEpochId(latest_epoch + 1) - )) - ); - } - - // Rotate to next epoch - let new_epoch = env - .update::>( - env.principal_0, - "rotate_chat_vetkey", - encode_args((chat_id,)).unwrap(), - ) - .unwrap(); - assert_eq!(new_epoch, VetKeyEpochId(latest_epoch + 1)); - } -} - -#[test] -fn can_rotate_chat_vetkey() { - let rng = &mut reproducible_rng(); - let env = TestEnvironment::new(rng); - - env.update::>( - env.principal_0, - "create_direct_chat", - encode_args((env.principal_1, Time(1_000), Time(10_000))).unwrap(), - ) - .unwrap(); - - let chat_id = ChatId::Direct(DirectChatId::new((env.principal_0, env.principal_1))); - - // Initially, epoch 0 should be the latest (we can verify this by trying to use epoch 1) - let result = env.update::>( - env.principal_0, - "derive_chat_vetkey", - encode_args(( - chat_id, - Some(VetKeyEpochId(1)), - ByteBuf::from(vec![0u8; 32]), // dummy transport key - )) - .unwrap(), - ); - assert!(result.is_err()); // Should fail because epoch 1 doesn't exist yet - - // Rotate to epoch 1 - let new_epoch = env - .update::>( - env.principal_0, - "rotate_chat_vetkey", - encode_args((chat_id,)).unwrap(), - ) - .unwrap(); - assert_eq!(new_epoch, VetKeyEpochId(1)); - - // Rotate to epoch 2 - let new_epoch = env - .update::>( - env.principal_0, - "rotate_chat_vetkey", - encode_args((chat_id,)).unwrap(), - ) - .unwrap(); - assert_eq!(new_epoch, VetKeyEpochId(2)); - - // Both participants should be able to rotate - let new_epoch = env - .update::>( - env.principal_1, - "rotate_chat_vetkey", - encode_args((chat_id,)).unwrap(), - ) - .unwrap(); - assert_eq!(new_epoch, VetKeyEpochId(3)); -} - -#[test] -fn unauthorized_user_cannot_rotate_chat_vetkey() { - let rng = &mut reproducible_rng(); - let env = TestEnvironment::new(rng); - - env.update::>( - env.principal_0, - "create_direct_chat", - encode_args((env.principal_1, Time(1_000), Time(10_000))).unwrap(), - ) - .unwrap(); - - let chat_id = ChatId::Direct(DirectChatId::new((env.principal_0, env.principal_1))); - - let result = env.update::>( - env.principal_2, - "rotate_chat_vetkey", - encode_args((chat_id,)).unwrap(), - ); - - assert_eq!( - result, - Err(format!( - "User {} does not have access to chat {chat_id:?} at epoch {:?}", - env.principal_2, - VetKeyEpochId(0) - )) - ); -} - -#[test] -fn can_update_and_get_symmetric_key_cache() { - let rng = &mut reproducible_rng(); - let env = TestEnvironment::new(rng); - - env.update::>( - env.principal_0, - "create_direct_chat", - encode_args((env.principal_1, Time(1_000), Time(10_000))).unwrap(), - ) - .unwrap(); - - let chat_id = ChatId::Direct(DirectChatId::new((env.principal_0, env.principal_1))); - let cache_data = b"dummy symmetric key cache".to_vec(); - let user_cache = EncryptedSymmetricKeyEpochCache(cache_data.clone()); - - // Initially, cache should be empty for both participants - for caller in [env.principal_0, env.principal_1] { - assert_eq!( - env.update::, String>>( - caller, - "get_my_symmetric_key_cache", - encode_args((chat_id, VetKeyEpochId(0))).unwrap(), - ), - Ok(None) - ); - } - - // Authorized user can create cache - for caller in [env.principal_0, env.principal_1] { - let result = env.update::>( - caller, - "update_my_symmetric_key_cache", - encode_args((chat_id, VetKeyEpochId(0), user_cache.clone())).unwrap(), - ); - assert_eq!(result, Ok(())); - } - - // Authorized user can retrieve their cache - for caller in [env.principal_0, env.principal_1] { - let result = env.update::, String>>( - caller, - "get_my_symmetric_key_cache", - encode_args((chat_id, VetKeyEpochId(0))).unwrap(), - ); - assert_eq!(result, Ok(Some(user_cache.clone()))); - } - - // Authorized user can update their cache - let updated_cache_data = b"updated symmetric key cache".to_vec(); - let updated_user_cache = EncryptedSymmetricKeyEpochCache(updated_cache_data.clone()); - - for caller in [env.principal_0, env.principal_1] { - let result = env.update::>( - caller, - "update_my_symmetric_key_cache", - encode_args((chat_id, VetKeyEpochId(0), updated_user_cache.clone())).unwrap(), - ); - assert_eq!(result, Ok(())); - } - - // Verify the cache was updated - for caller in [env.principal_0, env.principal_1] { - let result = env.update::, String>>( - caller, - "get_my_symmetric_key_cache", - encode_args((chat_id, VetKeyEpochId(0))).unwrap(), - ); - assert_eq!(result, Ok(Some(updated_user_cache.clone()))); - } -} - -#[test] -fn unauthorized_user_cannot_access_symmetric_key_cache() { - let rng = &mut reproducible_rng(); - let env = TestEnvironment::new(rng); - - env.update::>( - env.principal_0, - "create_direct_chat", - encode_args((env.principal_1, Time(1_000), Time(10_000))).unwrap(), - ) - .unwrap(); - - let chat_id = ChatId::Direct(DirectChatId::new((env.principal_0, env.principal_1))); - let cache_data = b"dummy symmetric key cache".to_vec(); - let user_cache = EncryptedSymmetricKeyEpochCache(cache_data); - - // Unauthorized user cannot update cache - let result = env.update::>( - env.principal_2, - "update_my_symmetric_key_cache", - encode_args((chat_id, VetKeyEpochId(0), user_cache.clone())).unwrap(), - ); - assert_eq!( - result, - Err(format!( - "User {} does not have access to chat {chat_id:?} at epoch {:?}", - env.principal_2, - VetKeyEpochId(0) - )) - ); - - // Unauthorized user cannot get cache - let result = env.update::, String>>( - env.principal_2, - "get_my_symmetric_key_cache", - encode_args((chat_id, VetKeyEpochId(0))).unwrap(), - ); - assert_eq!( - result, - Err(format!( - "User {} does not have access to chat {chat_id:?} at epoch {:?}", - env.principal_2, - VetKeyEpochId(0) - )) - ); -} - -#[test] -#[ignore = "vetkey epoch expiry not fully implemented yet"] -fn cannot_access_cache_after_vetkey_epoch_expires() { - let rng = &mut reproducible_rng(); - let env = TestEnvironment::new(rng); - - let expiry_setting_minutes = 10_000; - - let chat_creation_time = env - .update::>( - env.principal_0, - "create_direct_chat", - encode_args((env.principal_1, Time(1_000), Time(expiry_setting_minutes))).unwrap(), - ) - .unwrap(); - - let chat_id = ChatId::Direct(DirectChatId::new((env.principal_0, env.principal_1))); - let cache_data = b"dummy symmetric key cache".to_vec(); - let user_cache = EncryptedSymmetricKeyEpochCache(cache_data.clone()); - - // Create cache for epoch 0 - for caller in [env.principal_0, env.principal_1] { - env.update::>( - caller, - "update_my_symmetric_key_cache", - encode_args((chat_id, VetKeyEpochId(0), user_cache.clone())).unwrap(), - ) - .unwrap(); - } - - let expiry_time = chat_creation_time.0 + expiry_setting_minutes * NANOSECONDS_IN_MINUTE; - // Fast forward time to expire epoch 0 - env.pic - .set_time(pocket_ic::Time::from_nanos_since_unix_epoch(expiry_time)); - - // Neither authorized nor unauthorized users can access expired epoch cache - for caller in [env.principal_0, env.principal_1] { - // Cannot update cache for expired epoch - let result = env.update::>( - caller, - "update_my_symmetric_key_cache", - encode_args((chat_id, VetKeyEpochId(0), user_cache.clone())).unwrap(), - ); - assert_eq!( - result, - Err(format!("vetKey epoch {:?} expired", VetKeyEpochId(0),)) - ); - - // Cannot get cache for expired epoch - let result = env.update::, String>>( - caller, - "get_my_symmetric_key_cache", - encode_args((chat_id, VetKeyEpochId(0))).unwrap(), - ); - assert_eq!( - result, - Err(format!("vetKey epoch {:?} expired", VetKeyEpochId(0),)) - ); - } -} - -#[test] -fn cannot_derive_vetkey_after_cache_exists() { - let rng = &mut reproducible_rng(); - let env = TestEnvironment::new(rng); - - env.update::>( - env.principal_0, - "create_direct_chat", - encode_args((env.principal_1, Time(1_000), Time(10_000))).unwrap(), - ) - .unwrap(); - - let chat_id = ChatId::Direct(DirectChatId::new((env.principal_0, env.principal_1))); - let cache_data = b"dummy symmetric key cache".to_vec(); - let user_cache = EncryptedSymmetricKeyEpochCache(cache_data.clone()); - - // DON'T REUSE THE SAME TRANSPORT KEYS IN PRODUCTION - let transport_key = ic_vetkeys::TransportSecretKey::from_seed(random_bytes(32, rng)).unwrap(); - - for caller in [env.principal_0, env.principal_1] { - // Create cache - env.update::>( - caller, - "update_my_symmetric_key_cache", - encode_args((chat_id, VetKeyEpochId(0), user_cache.clone())).unwrap(), - ) - .unwrap(); - - // Now derive_vetkey should fail - let result = env.update::>( - caller, - "derive_chat_vetkey", - encode_args(( - chat_id, - Option::::None, - ByteBuf::from(transport_key.public_key()), - )) - .unwrap(), - ); - assert_eq!( - result, - Err(format!( - "User {} already has a cached key for chat {chat_id:?} at vetkey epoch {:?}", - caller, - VetKeyEpochId(0) - )) - ); - } -} - -#[test] -fn cache_is_separate_for_different_epochs() { - let rng = &mut reproducible_rng(); - let env = TestEnvironment::new(rng); - - env.update::>( - env.principal_0, - "create_direct_chat", - encode_args((env.principal_1, Time(1_000), Time(10_000))).unwrap(), - ) - .unwrap(); - - let chat_id = ChatId::Direct(DirectChatId::new((env.principal_0, env.principal_1))); - let user_cache_0 = EncryptedSymmetricKeyEpochCache(b"cache for epoch 0".to_vec()); - let user_cache_1 = EncryptedSymmetricKeyEpochCache(b"cache for epoch 1".to_vec()); - - for caller in [env.principal_0, env.principal_1] { - // Create cache for epoch 0 - env.update::>( - caller, - "update_my_symmetric_key_cache", - encode_args((chat_id, VetKeyEpochId(0), user_cache_0.clone())).unwrap(), - ) - .unwrap(); - - // Verify cache exists for epoch 0 - let result = env.update::, String>>( - caller, - "get_my_symmetric_key_cache", - encode_args((chat_id, VetKeyEpochId(0))).unwrap(), - ); - assert_eq!(result, Ok(Some(user_cache_0.clone()))); - - // Verify no cache exists for epoch 1 - let result = env.update::, String>>( - caller, - "get_my_symmetric_key_cache", - encode_args((chat_id, VetKeyEpochId(1))).unwrap(), - ); - assert_eq!( - result, - Err(format!( - "vetKey epoch {:?} not found for chat {chat_id:?}", - VetKeyEpochId(1) - )) - ); - } - - // Rotate to epoch 1 - let new_epoch = env - .update::>( - env.principal_0, - "rotate_chat_vetkey", - encode_args((chat_id,)).unwrap(), - ) - .unwrap(); - assert_eq!(new_epoch, VetKeyEpochId(1)); - - for caller in [env.principal_0, env.principal_1] { - // Create cache for epoch 1 - env.update::>( - caller, - "update_my_symmetric_key_cache", - encode_args((chat_id, VetKeyEpochId(1), user_cache_1.clone())).unwrap(), - ) - .unwrap(); - - // Verify cache still exists for epoch 0 - let result = env.update::, String>>( - caller, - "get_my_symmetric_key_cache", - encode_args((chat_id, VetKeyEpochId(0))).unwrap(), - ); - assert_eq!(result, Ok(Some(user_cache_0.clone()))); - - // Verify cache exists for epoch 1 - let result = env.update::, String>>( - caller, - "get_my_symmetric_key_cache", - encode_args((chat_id, VetKeyEpochId(1))).unwrap(), - ); - assert_eq!(result, Ok(Some(user_cache_1.clone()))); - } -} - -#[test] -fn can_reshare_vetkey() { - let rng = &mut reproducible_rng(); - let env = TestEnvironment::new(rng); - - env.update::>( - env.principal_0, - "create_direct_chat", - encode_args((env.principal_1, Time(1_000), Time(10_000))).unwrap(), - ) - .unwrap(); - - let chat_id = ChatId::Direct(DirectChatId::new((env.principal_0, env.principal_1))); - - let reshared_vetkey = b"dummy_encrypted_vetkey".to_vec(); - - env.update::>( - env.principal_0, - "reshare_ibe_encrypted_vetkeys", - encode_args(( - chat_id, - VetKeyEpochId(0), - vec![(env.principal_1, IbeEncryptedVetKey(reshared_vetkey.clone()))], - )) - .unwrap(), - ) - .unwrap(); - - let result = env.update::, String>>( - env.principal_1, - "get_my_reshared_ibe_encrypted_vetkey", - encode_args((chat_id, VetKeyEpochId(0))).unwrap(), - ); - - assert_eq!(result, Ok(Some(IbeEncryptedVetKey(reshared_vetkey)))); -} - -#[test] -fn reshared_vetkey_is_deleted_and_rejected_after_user_uploads_cache() { - let rng = &mut reproducible_rng(); - let env = TestEnvironment::new(rng); - - env.update::>( - env.principal_0, - "create_direct_chat", - encode_args((env.principal_1, Time(1_000), Time(10_000))).unwrap(), - ) - .unwrap(); - let chat_id = ChatId::Direct(DirectChatId::new((env.principal_0, env.principal_1))); - - env.update::>( - env.principal_0, - "reshare_ibe_encrypted_vetkeys", - encode_args(( - chat_id, - VetKeyEpochId(0), - vec![( - env.principal_1, - IbeEncryptedVetKey(b"dummy_encrypted_vetkey".to_vec()), - )], - )) - .unwrap(), - ) - .unwrap(); - - let user_cache = EncryptedSymmetricKeyEpochCache(b"dummy symmetric key cache".to_vec()); - let result = env.update::>( - env.principal_1, - "update_my_symmetric_key_cache", - encode_args((chat_id, VetKeyEpochId(0), user_cache.clone())).unwrap(), - ); - assert_eq!(result, Ok(())); - - let result = env.update::, String>>( - env.principal_1, - "get_my_reshared_ibe_encrypted_vetkey", - encode_args((chat_id, VetKeyEpochId(0))).unwrap(), - ); - - assert_eq!(result, Ok(None)); - - let result = env.update::>( - env.principal_1, - "reshare_ibe_encrypted_vetkeys", - encode_args(( - chat_id, - VetKeyEpochId(0), - vec![( - env.principal_1, - IbeEncryptedVetKey(b"dummy_encrypted_vetkey".to_vec()), - )], - )) - .unwrap(), - ); - assert_eq!( - result, - Err(format!( - "User {} already has a cached key for chat {chat_id:?} at vetkey epoch {:?}", - env.principal_1, - VetKeyEpochId(0) - )) - ); -} - -#[test] -fn cannot_reshare_vetkey_twice() { - let rng = &mut reproducible_rng(); - let env = TestEnvironment::new(rng); - - env.update::>( - env.principal_0, - "create_direct_chat", - encode_args((env.principal_1, Time(1_000), Time(10_000))).unwrap(), - ) - .unwrap(); - - let chat_id = ChatId::Direct(DirectChatId::new((env.principal_0, env.principal_1))); - - let reshared_vetkey = b"dummy_encrypted_vetkey".to_vec(); - - env.update::>( - env.principal_0, - "reshare_ibe_encrypted_vetkeys", - encode_args(( - chat_id, - VetKeyEpochId(0), - vec![(env.principal_1, IbeEncryptedVetKey(reshared_vetkey.clone()))], - )) - .unwrap(), - ) - .unwrap(); - - assert_eq!( - env.update::>( - env.principal_0, - "reshare_ibe_encrypted_vetkeys", - encode_args(( - chat_id, - VetKeyEpochId(0), - vec![( - env.principal_1, - IbeEncryptedVetKey(b"dummy_encrypted_vetkey_2".to_vec()) - )], - )) - .unwrap(), - ), - Err(format!( - "User {} already has a reshared key for chat {chat_id:?} at vetkey epoch {:?}", - env.principal_1, - VetKeyEpochId(0) - )) - ); - - let result = env.update::, String>>( - env.principal_1, - "get_my_reshared_ibe_encrypted_vetkey", - encode_args((chat_id, VetKeyEpochId(0))).unwrap(), - ); - - assert_eq!(result, Ok(Some(IbeEncryptedVetKey(reshared_vetkey)))); -} - -#[test] -fn fails_to_reshare_vetkey_if_unauthorized() { - let rng = &mut reproducible_rng(); - let env = TestEnvironment::new(rng); - - env.update::>( - env.principal_0, - "create_direct_chat", - encode_args((env.principal_1, Time(1_000), Time(10_000))).unwrap(), - ) - .unwrap(); - - let chat_id = ChatId::Direct(DirectChatId::new((env.principal_0, env.principal_1))); - - let reshared_vetkey = b"dummy_encrypted_vetkey".to_vec(); - - assert_eq!( - env.update::>( - env.principal_2, - "reshare_ibe_encrypted_vetkeys", - encode_args(( - chat_id, - VetKeyEpochId(0), - vec![(env.principal_1, IbeEncryptedVetKey(reshared_vetkey.clone()))], - )) - .unwrap(), - ), - Err(format!( - "User {} does not have access to chat {chat_id:?} at epoch {:?}", - env.principal_2, - VetKeyEpochId(0) - )) - ); - - let result = env.update::, String>>( - env.principal_0, - "get_my_reshared_ibe_encrypted_vetkey", - encode_args((chat_id, VetKeyEpochId(0))).unwrap(), - ); - - assert_eq!(result, Ok(None)); -} - -#[test] -fn fails_to_reshare_vetkey_with_oneself() { - let rng = &mut reproducible_rng(); - let env = TestEnvironment::new(rng); - - env.update::>( - env.principal_0, - "create_direct_chat", - encode_args((env.principal_1, Time(1_000), Time(10_000))).unwrap(), - ) - .unwrap(); - - let chat_id = ChatId::Direct(DirectChatId::new((env.principal_0, env.principal_1))); - - let reshared_vetkey = b"dummy_encrypted_vetkey".to_vec(); - - assert_eq!( - env.update::>( - env.principal_0, - "reshare_ibe_encrypted_vetkeys", - encode_args(( - chat_id, - VetKeyEpochId(0), - vec![(env.principal_0, IbeEncryptedVetKey(reshared_vetkey.clone()))], - )) - .unwrap(), - ), - Err(format!( - "User {} cannot reshare a vetkey with themselves", - env.principal_0 - )) - ); - - let result = env.update::, String>>( - env.principal_0, - "get_my_reshared_ibe_encrypted_vetkey", - encode_args((chat_id, VetKeyEpochId(0))).unwrap(), - ); - - assert_eq!(result, Ok(None)); -} - -#[test] -#[ignore = "vetkey epoch expiry not fully implemented yet"] -fn fails_to_reshare_or_get_reshared_vetkeys_for_invalid_vetkey_epochs() { - let rng = &mut reproducible_rng(); - let env = TestEnvironment::new(rng); - - let message_expiry_time_minutes = Time(10_000); - - let chat_creation_time = env - .update::>( - env.principal_0, - "create_direct_chat", - encode_args((env.principal_1, Time(1_000), message_expiry_time_minutes)).unwrap(), - ) - .unwrap(); - - let chat_id = ChatId::Direct(DirectChatId::new((env.principal_0, env.principal_1))); - - let reshared_vetkey = b"dummy_encrypted_vetkey".to_vec(); - - assert_eq!( - env.update::>( - env.principal_0, - "reshare_ibe_encrypted_vetkeys", - encode_args(( - chat_id, - VetKeyEpochId(1), - vec![(env.principal_1, IbeEncryptedVetKey(reshared_vetkey.clone()))], - )) - .unwrap(), - ), - Err(format!( - "vetKey epoch {:?} not found for chat {chat_id:?}", - VetKeyEpochId(1) - )) - ); - - env.update::>( - env.principal_0, - "reshare_ibe_encrypted_vetkeys", - encode_args(( - chat_id, - VetKeyEpochId(0), - vec![(env.principal_1, IbeEncryptedVetKey(reshared_vetkey.clone()))], - )) - .unwrap(), - ) - .unwrap(); - - let result = env.update::, String>>( - env.principal_1, - "get_my_reshared_ibe_encrypted_vetkey", - encode_args((chat_id, VetKeyEpochId(1))).unwrap(), - ); - - assert_eq!( - result, - Err(format!( - "vetKey epoch {:?} not found for chat {chat_id:?}", - VetKeyEpochId(1) - )) - ); - - let new_epoch = env - .update::>( - env.principal_0, - "rotate_chat_vetkey", - encode_args((chat_id,)).unwrap(), - ) - .unwrap(); - assert_eq!(new_epoch, VetKeyEpochId(1)); - - let result = env.update::, String>>( - env.principal_1, - "get_my_reshared_ibe_encrypted_vetkey", - encode_args((chat_id, VetKeyEpochId(0))).unwrap(), - ); - - assert_eq!( - result, - Ok(Some(IbeEncryptedVetKey(reshared_vetkey.clone()))) - ); - - let result = env.update::, String>>( - env.principal_1, - "get_my_reshared_ibe_encrypted_vetkey", - encode_args((chat_id, VetKeyEpochId(2))).unwrap(), - ); - - assert_eq!( - result, - Err(format!( - "vetKey epoch {:?} not found for chat {chat_id:?}", - VetKeyEpochId(2) - )) - ); - - env.pic - .set_time(pocket_ic::Time::from_nanos_since_unix_epoch( - chat_creation_time.0 + message_expiry_time_minutes.0 * NANOSECONDS_IN_MINUTE, - )); - - let result = env.update::, String>>( - env.principal_1, - "get_my_reshared_ibe_encrypted_vetkey", - encode_args((chat_id, VetKeyEpochId(0))).unwrap(), - ); - - assert_eq!( - result, - Err(format!("vetKey epoch {:?} expired", VetKeyEpochId(0))) - ); - - env.update::, String>>( - env.principal_1, - "get_my_reshared_ibe_encrypted_vetkey", - encode_args((chat_id, VetKeyEpochId(1))).unwrap(), - ) - .unwrap(); - - env.pic.advance_time(std::time::Duration::from_nanos(10)); - - let result = env.update::, String>>( - env.principal_1, - "get_my_reshared_ibe_encrypted_vetkey", - encode_args((chat_id, VetKeyEpochId(1))).unwrap(), - ); - - assert_eq!( - result, - Err(format!("vetKey epoch {:?} expired", VetKeyEpochId(1))) - ); - - for i in 0..2 { - let result = env.update::>( - env.principal_0, - "reshare_ibe_encrypted_vetkeys", - encode_args(( - chat_id, - VetKeyEpochId(i), - vec![(env.principal_1, IbeEncryptedVetKey(reshared_vetkey.clone()))], - )) - .unwrap(), - ); - - assert_eq!( - result, - Err(format!("vetKey epoch {:?} expired", VetKeyEpochId(i))) - ); - } -} - -#[test] -fn time_job_reports_cleaned_up_expired_items() { - let rng = &mut reproducible_rng(); - let env = TestEnvironment::new(rng); - - let chat_id_01 = ChatId::Direct(DirectChatId::new((env.principal_0, env.principal_1))); - let chat_id_02 = ChatId::Direct(DirectChatId::new((env.principal_0, env.principal_2))); - let user_cache = EncryptedSymmetricKeyEpochCache(b"dummy_symmetric_key".to_vec()); - let dummy_encrypted_vetkey = IbeEncryptedVetKey(b"dummy_encrypted_vetkey".to_vec()); - - env.update::>( - env.principal_0, - "create_direct_chat", - encode_args((env.principal_1, Time(30), Time(60))).unwrap(), - ) - .unwrap(); - - env.update::>( - env.principal_0, - "create_direct_chat", - encode_args((env.principal_2, Time(30), Time(60))).unwrap(), - ) - .unwrap(); - - for i in 0..2 { - for j in 0..2 { - let user_message = UserMessage { - content: b"hello".to_vec(), - vetkey_epoch: VetKeyEpochId(i), - symmetric_key_epoch: SymmetricKeyEpochId(0), - nonce: Nonce(i + 2 * j), - }; - env.update::>( - env.principal_0, - "send_direct_message", - encode_args((user_message.clone(), env.principal_1)).unwrap(), - ) - .unwrap(); - env.update::>( - env.principal_0, - "send_direct_message", - encode_args((user_message.clone(), env.principal_2)).unwrap(), - ) - .unwrap(); - } - - for (chat_id, receiver) in [(chat_id_01, env.principal_1), (chat_id_02, env.principal_2)] { - env.update::>( - env.principal_0, - "update_my_symmetric_key_cache", - encode_args((chat_id, VetKeyEpochId(i), user_cache.clone())).unwrap(), - ) - .unwrap(); - - env.update::>( - env.principal_0, - "reshare_ibe_encrypted_vetkeys", - encode_args(( - chat_id, - VetKeyEpochId(i), - vec![(receiver, dummy_encrypted_vetkey.clone())], - )) - .unwrap(), - ) - .unwrap(); - - if i == 0 { - let new_epoch = env - .update::>( - env.principal_0, - "rotate_chat_vetkey", - encode_args((chat_id,)).unwrap(), - ) - .unwrap(); - - assert_eq!(new_epoch, VetKeyEpochId(1)); - } - } - } - - env.pic - .advance_time(std::time::Duration::from_secs(24 * 3600)); - env.pic.tick(); - - let logs = env - .pic - .fetch_canister_logs(env.canister_id, Principal::anonymous()) - .unwrap(); - let log_string = logs.iter().fold(String::new(), |acc, log| { - format!("{acc}{}", String::from_utf8(log.content.clone()).unwrap()) - }); - assert_eq!(log_string, "Timer job: cleaned up 8 expired direct messages, 0 expired group messages, 4 expired vetkey epochs caches, 4 expired reshared vetkeys"); -} diff --git a/rust/vetkeys/encrypted_chat/rust/backend/tests/group_chat.rs b/rust/vetkeys/encrypted_chat/rust/backend/tests/group_chat.rs deleted file mode 100644 index d727fa63d..000000000 --- a/rust/vetkeys/encrypted_chat/rust/backend/tests/group_chat.rs +++ /dev/null @@ -1,1939 +0,0 @@ -use candid::{encode_args, Principal}; -use ic_vetkeys_example_encrypted_chat_backend::types::{ - ChatId, ChatMessageId, EncryptedMessage, EncryptedMessageMetadata, - EncryptedSymmetricKeyEpochCache, GroupChatId, GroupChatMetadata, GroupModification, - IbeEncryptedVetKey, Nonce, SymmetricKeyEpochId, Time, UserMessage, VetKeyEpochId, - VetKeyEpochMetadata, -}; -use serde_bytes::ByteBuf; - -mod common; -use common::*; - -#[test] -fn can_create_chat() { - let rng = &mut reproducible_rng(); - let env = TestEnvironment::new(rng); - - let mut expected_chat_id = 0; - - for other_participants in [ - vec![], - vec![env.principal_1], - vec![env.principal_1, env.principal_2], - ] { - let result = env.update::>( - env.principal_0, - "create_group_chat", - encode_args((other_participants, Time(1_000), Time(10_000))).unwrap(), - ); - - assert_eq!( - result, - Ok(GroupChatMetadata { - chat_id: GroupChatId(expected_chat_id), - creation_timestamp: Time(env.pic.get_time().as_nanos_since_unix_epoch()), - }) - ); - - expected_chat_id += 1; - } -} - -#[test] -fn can_send_and_get_messages() { - let rng = &mut reproducible_rng(); - let env = TestEnvironment::new(rng); - - for other_participants in [ - vec![], - vec![env.principal_1], - vec![env.principal_1, env.principal_2], - ] { - let all_participants: Vec<_> = [env.principal_0] - .into_iter() - .chain(other_participants.iter().copied()) - .collect(); - - let group_chat_metadata = env - .update::>( - env.principal_0, - "create_group_chat", - encode_args((other_participants, Time(1_000), Time(10_000))).unwrap(), - ) - .unwrap(); - - let message_content = b"dummy encrypted message".to_vec(); - - let mut message_id_counters = std::collections::BTreeMap::from([ - (env.principal_0, 0), - (env.principal_1, 0), - (env.principal_2, 0), - ]); - - let mut expected_chat_history = vec![]; - - let chat_id = ChatId::Group(group_chat_metadata.chat_id); - - for caller in all_participants.iter().copied() { - assert_eq!( - env.update::>( - caller, - "get_messages", - encode_args((chat_id, ChatMessageId(0), Option::::None)).unwrap(), - ), - vec![] - ); - } - - for _ in 0..10 { - for sender in all_participants.iter().copied() { - let message_id_raw = *message_id_counters.get(&sender).unwrap(); - message_id_counters.insert(sender, message_id_raw + 1); - - let user_message = UserMessage { - content: message_content.clone(), - vetkey_epoch: VetKeyEpochId(0), - symmetric_key_epoch: SymmetricKeyEpochId(0), - nonce: Nonce(message_id_raw), - }; - - // + 1 is because the update call calls `tick` internally - let expected_message_time = env.pic.get_time().as_nanos_since_unix_epoch() + 1; - - let message_time = env - .update::>( - sender, - "send_group_message", - encode_args((user_message, group_chat_metadata.chat_id)).unwrap(), - ) - .unwrap(); - - let expected_added_chat_message = EncryptedMessage { - content: message_content.clone(), - metadata: EncryptedMessageMetadata { - sender, - timestamp: message_time, - vetkey_epoch: VetKeyEpochId(0), - symmetric_key_epoch: SymmetricKeyEpochId(0), - chat_message_id: ChatMessageId(expected_chat_history.len() as u64), - nonce: Nonce(message_id_raw), - }, - }; - - expected_chat_history.push(expected_added_chat_message); - - for caller in all_participants.iter().copied() { - assert_eq!( - env.update::>( - caller, - "get_messages", - encode_args((chat_id, ChatMessageId(0), Option::::None)).unwrap(), - ), - expected_chat_history - ); - } - - assert_eq!(message_time.0, expected_message_time); - } - } - } -} - -#[test] -fn fails_to_send_messages_with_wrong_symmetric_key_epoch() { - let rng = &mut reproducible_rng(); - let env = TestEnvironment::new(rng); - - let symmetric_key_rotation_minutes = Time(1_000); - let chat_message_expiration_minutes = Time(10_000); - - for other_participants in [ - vec![], - vec![env.principal_1], - vec![env.principal_1, env.principal_2], - ] { - let all_participants: Vec<_> = [env.principal_0] - .into_iter() - .chain(other_participants.iter().copied()) - .collect(); - - let group_chat_metadata = env - .update::>( - env.principal_0, - "create_group_chat", - encode_args(( - other_participants, - symmetric_key_rotation_minutes, - chat_message_expiration_minutes, - )) - .unwrap(), - ) - .unwrap(); - - let message_content = b"dummy encrypted message".to_vec(); - let chat_id = ChatId::Group(group_chat_metadata.chat_id); - - // check that epoch 1 fails while we have epoch 0 - for i in 0..2 { - for sender in all_participants.iter().copied() { - let symmetric_key_epoch = SymmetricKeyEpochId(1); - let user_message = UserMessage { - content: message_content.clone(), - vetkey_epoch: VetKeyEpochId(0), - symmetric_key_epoch, - nonce: Nonce(0), - }; - - let result = env.update::>( - sender, - "send_group_message", - encode_args((user_message, group_chat_metadata.chat_id)).unwrap(), - ); - - assert_eq!( - result, - Err( - format!( - "Wrong symmetric key epoch {} is not yet active, current time is {} and epoch start is {}", - symmetric_key_epoch.0, - env.pic.get_time().as_nanos_since_unix_epoch(), - group_chat_metadata.creation_timestamp.0 + symmetric_key_epoch.0 * symmetric_key_rotation_minutes.0 * NANOSECONDS_IN_MINUTE - ) - ) - ); - } - - // set time to `all_participants.len()` ns before the change to epoch 1 - if i == 0 { - env.pic - .set_time(pocket_ic::Time::from_nanos_since_unix_epoch( - group_chat_metadata.creation_timestamp.0 - + symmetric_key_rotation_minutes.0 * NANOSECONDS_IN_MINUTE - - all_participants.len() as u64, - )); - } - } - - // check that epoch 0 and 2 fails while we have epoch 1 - for i in 0..2 { - for sender in all_participants.iter().copied() { - // use epoch 0 - { - let symmetric_key_epoch = SymmetricKeyEpochId(0); - let user_message = UserMessage { - content: message_content.clone(), - vetkey_epoch: VetKeyEpochId(0), - symmetric_key_epoch, - nonce: Nonce(0), - }; - - let result = env.update::>( - sender, - "send_group_message", - encode_args((user_message, group_chat_metadata.chat_id)).unwrap(), - ); - - assert_eq!( - result, - Err( - format!( - "Wrong symmetric key epoch: epoch {} is expired, current time is {} and epoch end is {}", - symmetric_key_epoch.0, - env.pic.get_time().as_nanos_since_unix_epoch(), - group_chat_metadata.creation_timestamp.0 + symmetric_key_rotation_minutes.0 * NANOSECONDS_IN_MINUTE - ) - ) - ); - } - - // use epoch 2 - { - let symmetric_key_epoch = SymmetricKeyEpochId(2); - let user_message = UserMessage { - content: message_content.clone(), - vetkey_epoch: VetKeyEpochId(0), - symmetric_key_epoch, - nonce: Nonce(0), - }; - - let result = env.update::>( - sender, - "send_group_message", - encode_args((user_message, group_chat_metadata.chat_id)).unwrap(), - ); - - assert_eq!( - result, - Err( - format!( - "Wrong symmetric key epoch {} is not yet active, current time is {} and epoch start is {}", - symmetric_key_epoch.0, - env.pic.get_time().as_nanos_since_unix_epoch(), - group_chat_metadata.creation_timestamp.0 + symmetric_key_epoch.0 * symmetric_key_rotation_minutes.0 * NANOSECONDS_IN_MINUTE - ) - ) - ); - } - } - - // set time to `2 * all_participants.len()` ns before the change to epoch 2 - if i == 0 { - env.pic - .set_time(pocket_ic::Time::from_nanos_since_unix_epoch( - group_chat_metadata.creation_timestamp.0 - + 2 * symmetric_key_rotation_minutes.0 * NANOSECONDS_IN_MINUTE - - 2 * all_participants.len() as u64, - )); - } - } - - // sanity check that no messages were added - for caller in all_participants.iter().copied() { - assert_eq!( - env.update::>( - caller, - "get_messages", - encode_args((chat_id, ChatMessageId(0), Option::::None)).unwrap(), - ), - vec![] - ); - } - } -} - -#[test] -fn can_get_vetkey_for_chat() { - let rng = &mut reproducible_rng(); - let env = TestEnvironment::new(rng); - - for other_participants in [ - vec![], - vec![env.principal_1], - vec![env.principal_1, env.principal_2], - ] { - let participants: Vec<_> = [env.principal_0] - .into_iter() - .chain(other_participants.iter().copied()) - .collect(); - let group_chat_metadata = env - .update::>( - env.principal_0, - "create_group_chat", - encode_args((other_participants, Time(1_000), Time(10_000))).unwrap(), - ) - .unwrap(); - - let chat_id = ChatId::Group(group_chat_metadata.chat_id); - // DON'T REUSE THE SAME TRANSPORT KEYS IN PRODUCTION - let transport_key = - ic_vetkeys::TransportSecretKey::from_seed(random_bytes(32, rng)).unwrap(); - - let mut raw_encrypted_vetkeys = std::collections::BTreeMap::new(); - - for latest_epoch in 0..3 { - for caller in participants.iter().copied() { - for epoch in 0..=latest_epoch { - for vetkey_epoch_id in - [Option::::None, Some(VetKeyEpochId(epoch))] - { - if vetkey_epoch_id.is_none() && epoch != latest_epoch { - continue; - } - - let raw_encrypted_vetkey = env - .update::>( - caller, - "derive_chat_vetkey", - encode_args(( - chat_id, - vetkey_epoch_id, - ByteBuf::from(transport_key.public_key()), - )) - .unwrap(), - ) - .unwrap() - .into_vec(); - let opt_evicted = - raw_encrypted_vetkeys.insert(epoch, raw_encrypted_vetkey.clone()); - if let Some(evicted) = opt_evicted { - assert_eq!(evicted, raw_encrypted_vetkey, "epoch: {epoch}, latest_epoch: {latest_epoch}, vetkey_epoch_id: {vetkey_epoch_id:?}"); - } - } - } - } - - let new_epoch = env - .update::>( - env.principal_0, - "rotate_chat_vetkey", - encode_args((chat_id,)).unwrap(), - ) - .unwrap(); - assert_eq!(new_epoch, VetKeyEpochId(latest_epoch + 1)); - } - - for (epoch, raw_encrypted_vetkey) in raw_encrypted_vetkeys.into_iter() { - let raw_public_key = env - .update::( - env.principal_0, - "chat_public_key", - encode_args((chat_id, VetKeyEpochId(epoch))).unwrap(), - ) - .into_vec(); - - let public_key = - ic_vetkeys::DerivedPublicKey::deserialize(raw_public_key.as_slice()).unwrap(); - - let _vetkey = ic_vetkeys::EncryptedVetKey::deserialize(&raw_encrypted_vetkey) - .unwrap() - .decrypt_and_verify(&transport_key, &public_key, &[]) - .unwrap(); - } - } -} - -#[test] -fn public_keys_for_different_chats_and_epochs_are_different() { - let rng = &mut reproducible_rng(); - let env = TestEnvironment::new(rng); - - let chat_id_0 = ChatId::Group(GroupChatId(0)); - let chat_id_1 = ChatId::Group(GroupChatId(1)); - - // we can get public key for any chats, also non-existing ones - let raw_public_key_00 = env - .update::( - env.principal_0, - "chat_public_key", - encode_args((chat_id_0, VetKeyEpochId(0))).unwrap(), - ) - .into_vec(); - - let raw_public_key_01 = env - .update::( - env.principal_0, - "chat_public_key", - encode_args((chat_id_0, VetKeyEpochId(1))).unwrap(), - ) - .into_vec(); - - let raw_public_key_10 = env - .update::( - env.principal_0, - "chat_public_key", - encode_args((chat_id_1, VetKeyEpochId(0))).unwrap(), - ) - .into_vec(); - - assert_ne!(raw_public_key_00, raw_public_key_01); - assert_ne!(raw_public_key_00, raw_public_key_10); -} - -#[test] -fn fails_to_get_vetkey_for_chat_if_unauthorized() { - let rng = &mut reproducible_rng(); - let env = TestEnvironment::new(rng); - - let transport_key = ic_vetkeys::TransportSecretKey::from_seed(random_bytes(32, rng)).unwrap(); - - for (other_participants, unauthorized_participants) in [ - (vec![], vec![env.principal_1, env.principal_2]), - (vec![env.principal_1], vec![env.principal_2]), - ] { - env.update::>( - env.principal_0, - "create_group_chat", - encode_args((other_participants, Time(1_000), Time(10_000))).unwrap(), - ) - .unwrap(); - - let chat_id = ChatId::Group(GroupChatId(0)); - - for unauthorized_participant in unauthorized_participants { - let result = env.update::>( - unauthorized_participant, - "derive_chat_vetkey", - encode_args(( - chat_id, - Option::::None, - ByteBuf::from(transport_key.public_key()), - )) - .unwrap(), - ); - - assert_eq!( - result, - Err(format!( - "User {} does not have access to chat {chat_id:?} at epoch {:?}", - unauthorized_participant, - VetKeyEpochId(0) - )) - ); - } - } -} - -#[test] -fn fails_to_send_group_chat_message_with_wrong_vetkey_epoch() { - let rng = &mut reproducible_rng(); - let env = TestEnvironment::new(rng); - - for other_participants in [ - vec![], - vec![env.principal_1], - vec![env.principal_1, env.principal_2], - ] { - let participants: Vec<_> = [env.principal_0] - .into_iter() - .chain(other_participants.iter().copied()) - .collect(); - let group_chat_metadata = env - .update::>( - env.principal_0, - "create_group_chat", - encode_args((other_participants, Time(1_000), Time(10_000))).unwrap(), - ) - .unwrap(); - - let chat_id = ChatId::Group(group_chat_metadata.chat_id); - - let message_content = b"dummy encrypted message".to_vec(); - - // Start with using epoch 1 before it's been rotated to (should fail) - for latest_epoch in 0..3 { - for sender in participants.iter().copied() { - let user_message = UserMessage { - content: message_content.clone(), - vetkey_epoch: VetKeyEpochId(latest_epoch + 1), - symmetric_key_epoch: SymmetricKeyEpochId(0), - nonce: Nonce(0), - }; - - let result = env.update::>( - sender, - "send_group_message", - encode_args((user_message, group_chat_metadata.chat_id)).unwrap(), - ); - - assert_eq!( - result, - Err(format!( - "vetKey epoch {:?} not found for chat {chat_id:?}", - VetKeyEpochId(latest_epoch + 1) - )) - ); - } - - // Rotate to next epoch - let new_epoch = env - .update::>( - env.principal_0, - "rotate_chat_vetkey", - encode_args((chat_id,)).unwrap(), - ) - .unwrap(); - assert_eq!(new_epoch, VetKeyEpochId(latest_epoch + 1)); - } - - // sanity check that no messages were added - for caller in participants.iter().copied() { - assert_eq!( - env.update::>( - caller, - "get_messages", - encode_args((chat_id, ChatMessageId(0), Option::::None)).unwrap(), - ), - vec![] - ); - } - } -} - -#[test] -fn fails_to_derive_vetkey_with_wrong_vetkey_epoch() { - let rng = &mut reproducible_rng(); - let env = TestEnvironment::new(rng); - - let transport_key = ic_vetkeys::TransportSecretKey::from_seed(random_bytes(32, rng)).unwrap(); - - for other_participants in [ - vec![], - vec![env.principal_1], - vec![env.principal_1, env.principal_2], - ] { - let participants: Vec<_> = [env.principal_0] - .into_iter() - .chain(other_participants.iter().copied()) - .collect(); - - let group_chat_metadata = env - .update::>( - env.principal_0, - "create_group_chat", - encode_args((other_participants, Time(1_000), Time(10_000))).unwrap(), - ) - .unwrap(); - - let chat_id = ChatId::Group(group_chat_metadata.chat_id); - - // Use epoch 1 before it's been rotated to - for latest_epoch in 0..3 { - for caller in participants.iter().copied() { - let result = env.update::>( - caller, - "derive_chat_vetkey", - encode_args(( - chat_id, - Some(VetKeyEpochId(latest_epoch + 1)), - ByteBuf::from(transport_key.public_key()), - )) - .unwrap(), - ); - - assert_eq!( - result, - Err(format!( - "vetKey epoch {:?} not found for chat {chat_id:?}", - VetKeyEpochId(latest_epoch + 1) - )) - ); - } - - // Rotate to next epoch - let new_epoch = env - .update::>( - env.principal_0, - "rotate_chat_vetkey", - encode_args((chat_id,)).unwrap(), - ) - .unwrap(); - assert_eq!(new_epoch, VetKeyEpochId(latest_epoch + 1)); - } - } -} - -#[test] -fn can_rotate_chat_vetkey() { - let rng = &mut reproducible_rng(); - let env = TestEnvironment::new(rng); - - for other_participants in [ - vec![], - vec![env.principal_1], - vec![env.principal_1, env.principal_2], - ] { - let participants: Vec<_> = [env.principal_0] - .into_iter() - .chain(other_participants.iter().copied()) - .collect(); - - let group_chat_metadata = env - .update::>( - env.principal_0, - "create_group_chat", - encode_args((other_participants, Time(1_000), Time(10_000))).unwrap(), - ) - .unwrap(); - - let chat_id = ChatId::Group(group_chat_metadata.chat_id); - - // Initially, epoch 0 should be the latest (we can verify this by trying to use epoch 1) - let result = env.update::>( - env.principal_0, - "derive_chat_vetkey", - encode_args(( - chat_id, - Some(VetKeyEpochId(1)), - ByteBuf::from(vec![0u8; 32]), // dummy transport key - )) - .unwrap(), - ); - assert!(result.is_err()); // Should fail because epoch 1 doesn't exist yet - - // Rotate to epoch 1 - let new_epoch = env - .update::>( - env.principal_0, - "rotate_chat_vetkey", - encode_args((chat_id,)).unwrap(), - ) - .unwrap(); - assert_eq!(new_epoch, VetKeyEpochId(1)); - - // All participants should be able to rotate - for (i, participant) in participants.iter().copied().enumerate() { - let new_epoch = env - .update::>( - participant, - "rotate_chat_vetkey", - encode_args((chat_id,)).unwrap(), - ) - .unwrap(); - // The epoch should increment for each rotation - assert_eq!(new_epoch, VetKeyEpochId(i as u64 + 2)); - } - } -} - -#[test] -fn unauthorized_user_cannot_rotate_chat_vetkey() { - let rng = &mut reproducible_rng(); - let env = TestEnvironment::new(rng); - - for (other_participants, unauthorized_participants) in [ - (vec![], vec![env.principal_1, env.principal_2]), - (vec![env.principal_1], vec![env.principal_2]), - ] { - env.update::>( - env.principal_0, - "create_group_chat", - encode_args((other_participants, Time(1_000), Time(10_000))).unwrap(), - ) - .unwrap(); - - let chat_id = ChatId::Group(GroupChatId(0)); - - for unauthorized_participant in unauthorized_participants { - let result = env.update::>( - unauthorized_participant, - "rotate_chat_vetkey", - encode_args((chat_id,)).unwrap(), - ); - - assert_eq!( - result, - Err(format!( - "User {} does not have access to chat {chat_id:?} at epoch {:?}", - unauthorized_participant, - VetKeyEpochId(0) - )) - ); - } - } -} - -#[test] -fn can_update_and_get_symmetric_key_cache() { - let rng = &mut reproducible_rng(); - let env = TestEnvironment::new(rng); - - for other_participants in [ - vec![], - vec![env.principal_1], - vec![env.principal_1, env.principal_2], - ] { - let participants: Vec<_> = [env.principal_0] - .into_iter() - .chain(other_participants.iter().copied()) - .collect(); - - let group_chat_metadata = env - .update::>( - env.principal_0, - "create_group_chat", - encode_args((other_participants, Time(1_000), Time(10_000))).unwrap(), - ) - .unwrap(); - - let chat_id = ChatId::Group(group_chat_metadata.chat_id); - let cache_data = b"dummy symmetric key cache".to_vec(); - let user_cache = EncryptedSymmetricKeyEpochCache(cache_data.clone()); - - // Initially, cache should be empty for all participants - for caller in participants.iter().copied() { - assert_eq!( - env.update::, String>>( - caller, - "get_my_symmetric_key_cache", - encode_args((chat_id, VetKeyEpochId(0))).unwrap(), - ), - Ok(None) - ); - } - - // Authorized user can create cache - for caller in participants.iter().copied() { - let result = env.update::>( - caller, - "update_my_symmetric_key_cache", - encode_args((chat_id, VetKeyEpochId(0), user_cache.clone())).unwrap(), - ); - assert_eq!(result, Ok(())); - } - - // Authorized user can retrieve their cache - for caller in participants.iter().copied() { - let result = env.update::, String>>( - caller, - "get_my_symmetric_key_cache", - encode_args((chat_id, VetKeyEpochId(0))).unwrap(), - ); - assert_eq!(result, Ok(Some(user_cache.clone()))); - } - - // Authorized user can update their cache - let updated_cache_data = b"updated symmetric key cache".to_vec(); - let updated_user_cache = EncryptedSymmetricKeyEpochCache(updated_cache_data.clone()); - - for caller in participants.iter().copied() { - let result = env.update::>( - caller, - "update_my_symmetric_key_cache", - encode_args((chat_id, VetKeyEpochId(0), updated_user_cache.clone())).unwrap(), - ); - assert_eq!(result, Ok(())); - } - - // Verify the cache was updated - for caller in participants.iter().copied() { - let result = env.update::, String>>( - caller, - "get_my_symmetric_key_cache", - encode_args((chat_id, VetKeyEpochId(0))).unwrap(), - ); - assert_eq!(result, Ok(Some(updated_user_cache.clone()))); - } - } -} - -#[test] -fn unauthorized_user_cannot_access_symmetric_key_cache() { - let rng = &mut reproducible_rng(); - let env = TestEnvironment::new(rng); - - for (other_participants, unauthorized_participants) in [ - (vec![], vec![env.principal_1, env.principal_2]), - (vec![env.principal_1], vec![env.principal_2]), - ] { - env.update::>( - env.principal_0, - "create_group_chat", - encode_args((other_participants, Time(1_000), Time(10_000))).unwrap(), - ) - .unwrap(); - - let chat_id = ChatId::Group(GroupChatId(0)); - let cache_data = b"dummy symmetric key cache".to_vec(); - let user_cache = EncryptedSymmetricKeyEpochCache(cache_data); - - for unauthorized_participant in unauthorized_participants { - // Unauthorized user cannot update cache - let result = env.update::>( - unauthorized_participant, - "update_my_symmetric_key_cache", - encode_args((chat_id, VetKeyEpochId(0), user_cache.clone())).unwrap(), - ); - assert_eq!( - result, - Err(format!( - "User {} does not have access to chat {chat_id:?} at epoch {:?}", - unauthorized_participant, - VetKeyEpochId(0) - )) - ); - - // Unauthorized user cannot get cache - let result = env.update::, String>>( - unauthorized_participant, - "get_my_symmetric_key_cache", - encode_args((chat_id, VetKeyEpochId(0))).unwrap(), - ); - assert_eq!( - result, - Err(format!( - "User {} does not have access to chat {chat_id:?} at epoch {:?}", - unauthorized_participant, - VetKeyEpochId(0) - )) - ); - } - } -} - -#[test] -#[ignore = "vetkey epoch expiry not fully implemented yet"] -fn cannot_access_cache_after_vetkey_epoch_expires() { - let rng = &mut reproducible_rng(); - let env = TestEnvironment::new(rng); - - for other_participants in [ - vec![], - vec![env.principal_1], - vec![env.principal_1, env.principal_2], - ] { - let participants: Vec<_> = [env.principal_0] - .into_iter() - .chain(other_participants.iter().copied()) - .collect(); - - let group_chat_metadata = env - .update::>( - env.principal_0, - "create_group_chat", - encode_args((other_participants.clone(), Time(1_000), Time(10_000))).unwrap(), - ) - .unwrap(); - - let chat_id = ChatId::Group(group_chat_metadata.chat_id); - let cache_data = b"dummy symmetric key cache".to_vec(); - let user_cache = EncryptedSymmetricKeyEpochCache(cache_data.clone()); - - // Create cache for epoch 0 - for caller in participants.iter().copied() { - env.update::>( - caller, - "update_my_symmetric_key_cache", - encode_args((chat_id, VetKeyEpochId(0), user_cache.clone())).unwrap(), - ) - .unwrap(); - } - - // Rotate to epoch 1 - let new_epoch = env - .update::>( - env.principal_0, - "rotate_chat_vetkey", - encode_args((chat_id,)).unwrap(), - ) - .unwrap(); - assert_eq!(new_epoch, VetKeyEpochId(1)); - - let expiry_setting_minutes = 10_000; - let expiry_time = group_chat_metadata.creation_timestamp.0 - + expiry_setting_minutes * NANOSECONDS_IN_MINUTE; - // Fast forward time to expire epoch 0 - env.pic - .set_time(pocket_ic::Time::from_nanos_since_unix_epoch(expiry_time)); - - // Neither authorized nor unauthorized users can access expired epoch cache - for caller in [env.principal_0, env.principal_1, env.principal_2] - .into_iter() - .filter(|p| *p == env.principal_0 || other_participants.contains(&p)) - { - // Cannot update cache for expired epoch - let result = env.update::>( - caller, - "update_my_symmetric_key_cache", - encode_args((chat_id, VetKeyEpochId(0), user_cache.clone())).unwrap(), - ); - assert_eq!( - result, - Err(format!("vetKey epoch {:?} expired", VetKeyEpochId(0),)) - ); - - // Cannot get cache for expired epoch - let result = env.update::, String>>( - caller, - "get_my_symmetric_key_cache", - encode_args((chat_id, VetKeyEpochId(0))).unwrap(), - ); - assert_eq!( - result, - Err(format!("vetKey epoch {:?} expired", VetKeyEpochId(0),)) - ); - } - } -} - -#[test] -fn cannot_derive_vetkey_after_cache_exists() { - let rng = &mut reproducible_rng(); - let env = TestEnvironment::new(rng); - - for other_participants in [ - vec![], - vec![env.principal_1], - vec![env.principal_1, env.principal_2], - ] { - let participants: Vec<_> = [env.principal_0] - .into_iter() - .chain(other_participants.iter().copied()) - .collect(); - - let group_chat_metadata = env - .update::>( - env.principal_0, - "create_group_chat", - encode_args((other_participants, Time(1_000), Time(10_000))).unwrap(), - ) - .unwrap(); - - let chat_id = ChatId::Group(group_chat_metadata.chat_id); - let cache_data = b"dummy symmetric key cache".to_vec(); - let user_cache = EncryptedSymmetricKeyEpochCache(cache_data.clone()); - - // DON'T REUSE THE SAME TRANSPORT KEYS IN PRODUCTION - let transport_key = - ic_vetkeys::TransportSecretKey::from_seed(random_bytes(32, rng)).unwrap(); - - for caller in participants.iter().copied() { - // Create cache - env.update::>( - caller, - "update_my_symmetric_key_cache", - encode_args((chat_id, VetKeyEpochId(0), user_cache.clone())).unwrap(), - ) - .unwrap(); - - // Now derive_vetkey should fail - let result = env.update::>( - caller, - "derive_chat_vetkey", - encode_args(( - chat_id, - Option::::None, - ByteBuf::from(transport_key.public_key()), - )) - .unwrap(), - ); - assert_eq!( - result, - Err(format!( - "User {} already has a cached key for chat {chat_id:?} at vetkey epoch {:?}", - caller, - VetKeyEpochId(0) - )) - ); - } - } -} - -#[test] -fn cache_is_separate_for_different_epochs() { - let rng = &mut reproducible_rng(); - let env = TestEnvironment::new(rng); - - for other_participants in [ - vec![], - vec![env.principal_1], - vec![env.principal_1, env.principal_2], - ] { - let participants: Vec<_> = [env.principal_0] - .into_iter() - .chain(other_participants.iter().copied()) - .collect(); - - let group_chat_metadata = env - .update::>( - env.principal_0, - "create_group_chat", - encode_args((other_participants, Time(1_000), Time(10_000))).unwrap(), - ) - .unwrap(); - - let chat_id = ChatId::Group(group_chat_metadata.chat_id); - let user_cache_0 = EncryptedSymmetricKeyEpochCache(b"cache for epoch 0".to_vec()); - let user_cache_1 = EncryptedSymmetricKeyEpochCache(b"cache for epoch 1".to_vec()); - - for caller in participants.iter().copied() { - // Create cache for epoch 0 - env.update::>( - caller, - "update_my_symmetric_key_cache", - encode_args((chat_id, VetKeyEpochId(0), user_cache_0.clone())).unwrap(), - ) - .unwrap(); - - // Verify cache exists for epoch 0 - let result = env.update::, String>>( - caller, - "get_my_symmetric_key_cache", - encode_args((chat_id, VetKeyEpochId(0))).unwrap(), - ); - assert_eq!(result, Ok(Some(user_cache_0.clone()))); - - // Verify no cache exists for epoch 1 - let result = env.update::, String>>( - caller, - "get_my_symmetric_key_cache", - encode_args((chat_id, VetKeyEpochId(1))).unwrap(), - ); - assert_eq!( - result, - Err(format!( - "vetKey epoch {:?} not found for chat {chat_id:?}", - VetKeyEpochId(1), - )) - ); - } - - // Rotate to epoch 1 - let new_epoch = env - .update::>( - env.principal_0, - "rotate_chat_vetkey", - encode_args((chat_id,)).unwrap(), - ) - .unwrap(); - assert_eq!(new_epoch, VetKeyEpochId(1)); - - for caller in participants.iter().copied() { - // Create cache for epoch 1 - env.update::>( - caller, - "update_my_symmetric_key_cache", - encode_args((chat_id, VetKeyEpochId(1), user_cache_1.clone())).unwrap(), - ) - .unwrap(); - - // Verify cache still exists for epoch 0 - let result = env.update::, String>>( - caller, - "get_my_symmetric_key_cache", - encode_args((chat_id, VetKeyEpochId(0))).unwrap(), - ); - assert_eq!(result, Ok(Some(user_cache_0.clone()))); - - // Verify cache exists for epoch 1 - let result = env.update::, String>>( - caller, - "get_my_symmetric_key_cache", - encode_args((chat_id, VetKeyEpochId(1))).unwrap(), - ); - assert_eq!(result, Ok(Some(user_cache_1.clone()))); - } - } -} - -#[test] -fn modify_chat_participants() { - let sorted_principals = |mut principals: Vec| { - principals.sort(); - principals - }; - - let mut rng = reproducible_rng(); - - let env = TestEnvironment::new(&mut rng); - let principal_0 = env.principal_0; - let principal_1 = env.principal_1; - let principal_2 = env.principal_2; - let principal_3 = random_self_authenticating_principal(&mut rng); - let principal_4 = random_self_authenticating_principal(&mut rng); - - let symmetric_key_rotation_duration_minutes = Time(1_000); - let symmetric_key_rotation_duration = - Time(NANOSECONDS_IN_MINUTE * symmetric_key_rotation_duration_minutes.0); - - let group_metadata = env - .update::>( - principal_0, - "create_group_chat", - encode_args(( - vec![principal_1], - symmetric_key_rotation_duration_minutes, - Time(10_000), - )) - .unwrap(), - ) - .unwrap(); - - let chat_id = ChatId::Group(group_metadata.chat_id); - - { - let group_metadata = env - .query::>( - principal_0, - "get_latest_chat_vetkey_epoch_metadata", - encode_args((chat_id,)).unwrap(), - ) - .unwrap(); - assert_eq!( - group_metadata, - VetKeyEpochMetadata { - epoch_id: VetKeyEpochId(0), - participants: sorted_principals(vec![principal_0, principal_1]), - creation_timestamp: Time(env.pic.get_time().as_nanos_since_unix_epoch()), - symmetric_key_rotation_duration, - messages_start_with_id: ChatMessageId(0), - } - ); - } - - let add_participants = vec![principal_2, principal_3]; - let remove_participants: Vec = vec![]; - let result = env.update::>( - principal_0, - "modify_group_chat_participants", - encode_args(( - group_metadata.chat_id, - GroupModification { - add_participants: add_participants.clone(), - remove_participants: remove_participants.clone(), - }, - )) - .unwrap(), - ); - assert_eq!(result, Ok(VetKeyEpochId(1))); - { - let group_metadata = env - .query::>( - principal_0, - "get_latest_chat_vetkey_epoch_metadata", - encode_args((chat_id,)).unwrap(), - ) - .unwrap(); - assert_eq!( - group_metadata, - VetKeyEpochMetadata { - epoch_id: VetKeyEpochId(1), - participants: sorted_principals(vec![ - principal_0, - principal_1, - principal_2, - principal_3 - ]), - creation_timestamp: Time(env.pic.get_time().as_nanos_since_unix_epoch()), - symmetric_key_rotation_duration, - messages_start_with_id: ChatMessageId(0), - } - ); - } - - let add_participants: Vec = vec![]; - let remove_participants = vec![principal_1, principal_2]; - let result = env.update::>( - principal_0, - "modify_group_chat_participants", - encode_args(( - group_metadata.chat_id, - GroupModification { - add_participants: add_participants.clone(), - remove_participants: remove_participants.clone(), - }, - )) - .unwrap(), - ); - assert_eq!(result, Ok(VetKeyEpochId(2))); - { - let group_metadata = env - .query::>( - principal_0, - "get_latest_chat_vetkey_epoch_metadata", - encode_args((chat_id,)).unwrap(), - ) - .unwrap(); - assert_eq!( - group_metadata, - VetKeyEpochMetadata { - epoch_id: VetKeyEpochId(2), - participants: sorted_principals(vec![principal_0, principal_3]), - creation_timestamp: Time(env.pic.get_time().as_nanos_since_unix_epoch()), - symmetric_key_rotation_duration, - messages_start_with_id: ChatMessageId(0), - } - ); - } - - let add_participants: Vec = vec![principal_1, principal_2]; - let remove_participants = vec![principal_3]; - let result = env.update::>( - principal_0, - "modify_group_chat_participants", - encode_args(( - group_metadata.chat_id, - GroupModification { - add_participants: add_participants.clone(), - remove_participants: remove_participants.clone(), - }, - )) - .unwrap(), - ); - assert_eq!(result, Ok(VetKeyEpochId(3))); - { - let group_metadata = env - .query::>( - principal_0, - "get_latest_chat_vetkey_epoch_metadata", - encode_args((chat_id,)).unwrap(), - ) - .unwrap(); - assert_eq!( - group_metadata, - VetKeyEpochMetadata { - epoch_id: VetKeyEpochId(3), - participants: sorted_principals(vec![principal_0, principal_1, principal_2]), - creation_timestamp: Time(env.pic.get_time().as_nanos_since_unix_epoch()), - symmetric_key_rotation_duration, - messages_start_with_id: ChatMessageId(0), - } - ); - } - - // 2. Unauthorized user (principal_4, not a member) tries to add principal_4 - let add_participants = vec![principal_4]; - let remove_participants: Vec = vec![]; - let result = env.update::>( - principal_4, - "modify_group_chat_participants", - encode_args(( - group_metadata.chat_id, - GroupModification { - add_participants: add_participants.clone(), - remove_participants: remove_participants.clone(), - }, - )) - .unwrap(), - ); - assert_eq!( - result, - Err(format!( - "User {principal_4} does not have access to chat {:?} at epoch {:?}", - chat_id, - VetKeyEpochId(3) - )) - ); - - // 2b. Unauthorized user (principal_4) tries to remove principal_0 - let add_participants: Vec = vec![]; - let remove_participants = vec![principal_0]; - let result = env.update::>( - principal_4, - "modify_group_chat_participants", - encode_args(( - group_metadata.chat_id, - GroupModification { - add_participants, - remove_participants, - }, - )) - .unwrap(), - ); - assert_eq!( - result, - Err(format!( - "User {principal_4} does not have access to chat {:?} at epoch {:?}", - chat_id, - VetKeyEpochId(3) - )) - ); -} - -#[test] -fn can_reshare_vetkey() { - let rng = &mut reproducible_rng(); - let env = TestEnvironment::new(rng); - - for other_participants in [ - vec![env.principal_1], - vec![env.principal_1, env.principal_2], - ] { - let participants: Vec<_> = [env.principal_0] - .into_iter() - .chain(other_participants.iter().copied()) - .collect(); - - let group_chat_metadata = env - .update::>( - participants[0], - "create_group_chat", - encode_args((other_participants, Time(1_000), Time(10_000))).unwrap(), - ) - .unwrap(); - - let chat_id = ChatId::Group(group_chat_metadata.chat_id); - let reshared_vetkey = b"dummy_encrypted_vetkey".to_vec(); - - // Reshare vetkey to all participants except the resharing user - let resharing_user = participants[0]; - let target_users: Vec<_> = participants - .iter() - .copied() - .filter(|&p| p != resharing_user) - .collect(); - - env.update::>( - resharing_user, - "reshare_ibe_encrypted_vetkeys", - encode_args(( - chat_id, - VetKeyEpochId(0), - target_users - .iter() - .map(|&user| (user, IbeEncryptedVetKey(reshared_vetkey.clone()))) - .collect::>(), - )) - .unwrap(), - ) - .unwrap(); - - // Verify all target users can retrieve their reshared vetkey - for target_user in target_users.iter().copied() { - let result = env.update::, String>>( - target_user, - "get_my_reshared_ibe_encrypted_vetkey", - encode_args((chat_id, VetKeyEpochId(0))).unwrap(), - ); - - assert_eq!( - result, - Ok(Some(IbeEncryptedVetKey(reshared_vetkey.clone()))) - ); - } - } -} - -#[test] -fn reshared_vetkey_is_deleted_and_rejected_after_user_uploads_cache() { - let rng = &mut reproducible_rng(); - let env = TestEnvironment::new(rng); - - for other_participants in [ - vec![env.principal_1], - vec![env.principal_1, env.principal_2], - ] { - let participants: Vec<_> = [env.principal_0] - .into_iter() - .chain(other_participants.iter().copied()) - .collect(); - - let group_chat_metadata = env - .update::>( - env.principal_0, - "create_group_chat", - encode_args((other_participants, Time(1_000), Time(10_000))).unwrap(), - ) - .unwrap(); - - let chat_id = ChatId::Group(group_chat_metadata.chat_id); - - // Reshare vetkey to all participants except the resharing user - let resharing_user = env.principal_0; - let target_users: Vec<_> = participants - .iter() - .copied() - .filter(|&p| p != resharing_user) - .collect(); - - env.update::>( - resharing_user, - "reshare_ibe_encrypted_vetkeys", - encode_args(( - chat_id, - VetKeyEpochId(0), - target_users - .iter() - .map(|&user| (user, IbeEncryptedVetKey(b"dummy_encrypted_vetkey".to_vec()))) - .collect::>(), - )) - .unwrap(), - ) - .unwrap(); - - // One of the target users uploads their cache - let target_user = target_users[0]; - let user_cache = EncryptedSymmetricKeyEpochCache(b"dummy symmetric key cache".to_vec()); - let result = env.update::>( - target_user, - "update_my_symmetric_key_cache", - encode_args((chat_id, VetKeyEpochId(0), user_cache.clone())).unwrap(), - ); - assert_eq!(result, Ok(())); - - // Verify the reshared vetkey is deleted for that user - let result = env.update::, String>>( - target_user, - "get_my_reshared_ibe_encrypted_vetkey", - encode_args((chat_id, VetKeyEpochId(0))).unwrap(), - ); - - assert_eq!(result, Ok(None)); - - // Verify that user cannot receive reshared vetkey anymore - let result = env.update::>( - resharing_user, - "reshare_ibe_encrypted_vetkeys", - encode_args(( - chat_id, - VetKeyEpochId(0), - vec![( - target_user, - IbeEncryptedVetKey(b"dummy_encrypted_vetkey".to_vec()), - )], - )) - .unwrap(), - ); - assert_eq!( - result, - Err(format!( - "User {} already has a cached key for chat {chat_id:?} at vetkey epoch {:?}", - target_user, - VetKeyEpochId(0) - )) - ); - } -} - -#[test] -fn cannot_reshare_vetkey_twice() { - let rng = &mut reproducible_rng(); - let env = TestEnvironment::new(rng); - - for other_participants in [ - vec![env.principal_1], - vec![env.principal_1, env.principal_2], - ] { - let participants: Vec<_> = [env.principal_0] - .into_iter() - .chain(other_participants.iter().copied()) - .collect(); - - let group_chat_metadata = env - .update::>( - env.principal_0, - "create_group_chat", - encode_args((other_participants, Time(1_000), Time(10_000))).unwrap(), - ) - .unwrap(); - - let chat_id = ChatId::Group(group_chat_metadata.chat_id); - let reshared_vetkey = b"dummy_encrypted_vetkey".to_vec(); - - // Reshare vetkey to all participants except the resharing user - let resharing_user = env.principal_0; - let target_users: Vec<_> = participants - .iter() - .copied() - .filter(|&p| p != resharing_user) - .collect(); - - env.update::>( - resharing_user, - "reshare_ibe_encrypted_vetkeys", - encode_args(( - chat_id, - VetKeyEpochId(0), - target_users - .iter() - .map(|&user| (user, IbeEncryptedVetKey(reshared_vetkey.clone()))) - .collect::>(), - )) - .unwrap(), - ) - .unwrap(); - - // Try to reshare again to the same users - assert_eq!( - env.update::>( - resharing_user, - "reshare_ibe_encrypted_vetkeys", - encode_args(( - chat_id, - VetKeyEpochId(0), - target_users - .iter() - .map(|&user| ( - user, - IbeEncryptedVetKey(b"dummy_encrypted_vetkey_2".to_vec()) - )) - .collect::>(), - )) - .unwrap(), - ), - Err(format!( - "User {} already has a reshared key for chat {chat_id:?} at vetkey epoch {:?}", - target_users[0], - VetKeyEpochId(0) - )) - ); - - // Verify the original reshared vetkey is still available - for target_user in target_users.iter().copied() { - let result = env.update::, String>>( - target_user, - "get_my_reshared_ibe_encrypted_vetkey", - encode_args((chat_id, VetKeyEpochId(0))).unwrap(), - ); - - assert_eq!( - result, - Ok(Some(IbeEncryptedVetKey(reshared_vetkey.clone()))) - ); - } - } -} - -#[test] -fn fails_to_reshare_vetkey_if_unauthorized() { - let mut rng = reproducible_rng(); - let env = TestEnvironment::new(&mut rng); - - for other_participants in [ - vec![env.principal_1], - vec![env.principal_1, env.principal_2], - ] { - let participants: Vec<_> = [env.principal_0] - .into_iter() - .chain(other_participants.iter().copied()) - .collect(); - - let group_chat_metadata = env - .update::>( - env.principal_0, - "create_group_chat", - encode_args((other_participants, Time(1_000), Time(10_000))).unwrap(), - ) - .unwrap(); - - let chat_id = ChatId::Group(group_chat_metadata.chat_id); - let reshared_vetkey = b"dummy_encrypted_vetkey".to_vec(); - - // Create an unauthorized user - let mut unauthorized_user = random_self_authenticating_principal(&mut rng); - while participants.contains(&unauthorized_user) { - unauthorized_user = random_self_authenticating_principal(&mut rng); - } - - assert_eq!( - env.update::>( - unauthorized_user, - "reshare_ibe_encrypted_vetkeys", - encode_args(( - chat_id, - VetKeyEpochId(0), - vec![(participants[0], IbeEncryptedVetKey(reshared_vetkey.clone()))], - )) - .unwrap(), - ), - Err(format!( - "User {} does not have access to chat {chat_id:?} at epoch {:?}", - unauthorized_user, - VetKeyEpochId(0) - )) - ); - - // Verify no reshared vetkey was created - let result = env.update::, String>>( - participants[0], - "get_my_reshared_ibe_encrypted_vetkey", - encode_args((chat_id, VetKeyEpochId(0))).unwrap(), - ); - - assert_eq!(result, Ok(None)); - } -} - -#[test] -fn fails_to_reshare_vetkey_with_oneself() { - let rng = &mut reproducible_rng(); - let env = TestEnvironment::new(rng); - - for other_participants in [ - vec![env.principal_1], - vec![env.principal_1, env.principal_2], - ] { - let participants: Vec<_> = [env.principal_0] - .into_iter() - .chain(other_participants.iter().copied()) - .collect(); - - let group_chat_metadata = env - .update::>( - env.principal_0, - "create_group_chat", - encode_args((other_participants, Time(1_000), Time(10_000))).unwrap(), - ) - .unwrap(); - - let chat_id = ChatId::Group(group_chat_metadata.chat_id); - let reshared_vetkey = b"dummy_encrypted_vetkey".to_vec(); - - for resharing_user in participants.iter().copied() { - assert_eq!( - env.update::>( - resharing_user, - "reshare_ibe_encrypted_vetkeys", - encode_args(( - chat_id, - VetKeyEpochId(0), - vec![(resharing_user, IbeEncryptedVetKey(reshared_vetkey.clone()))], - )) - .unwrap(), - ), - Err(format!( - "User {} cannot reshare a vetkey with themselves", - resharing_user - )) - ); - - // Verify no reshared vetkey was created - let result = env.update::, String>>( - resharing_user, - "get_my_reshared_ibe_encrypted_vetkey", - encode_args((chat_id, VetKeyEpochId(0))).unwrap(), - ); - - assert_eq!(result, Ok(None)); - } - } -} - -#[test] -#[ignore = "vetkey epoch expiry not fully implemented yet"] -fn fails_to_reshare_or_get_reshared_vetkeys_for_invalid_vetkey_epochs() { - let rng = &mut reproducible_rng(); - let env = TestEnvironment::new(rng); - let expiry_setting_minutes = Time(10_000); - - for other_participants in [ - vec![env.principal_1], - vec![env.principal_1, env.principal_2], - ] { - let participants: Vec<_> = [env.principal_0] - .into_iter() - .chain(other_participants.iter().copied()) - .collect(); - - let group_chat_metadata = env - .update::>( - env.principal_0, - "create_group_chat", - encode_args((other_participants, Time(1_000), expiry_setting_minutes)).unwrap(), - ) - .unwrap(); - - let chat_id = ChatId::Group(group_chat_metadata.chat_id); - let reshared_vetkey = b"dummy_encrypted_vetkey".to_vec(); - - // Try to reshare to epoch 1 before it exists - assert_eq!( - env.update::>( - participants[0], - "reshare_ibe_encrypted_vetkeys", - encode_args(( - chat_id, - VetKeyEpochId(1), - vec![(participants[1], IbeEncryptedVetKey(reshared_vetkey.clone()))], - )) - .unwrap(), - ), - Err(format!( - "vetKey epoch {:?} not found for chat {chat_id:?}", - VetKeyEpochId(1) - )) - ); - - // Reshare to epoch 0 - env.update::>( - participants[0], - "reshare_ibe_encrypted_vetkeys", - encode_args(( - chat_id, - VetKeyEpochId(0), - vec![(participants[1], IbeEncryptedVetKey(reshared_vetkey.clone()))], - )) - .unwrap(), - ) - .unwrap(); - - // Try to get reshared vetkey for epoch 1 before it exists - let result = env.update::, String>>( - participants[1], - "get_my_reshared_ibe_encrypted_vetkey", - encode_args((chat_id, VetKeyEpochId(1))).unwrap(), - ); - - assert_eq!( - result, - Err(format!( - "vetKey epoch {:?} not found for chat {chat_id:?}", - VetKeyEpochId(1) - )) - ); - - // Rotate to epoch 1 - let new_epoch = env - .update::>( - participants[0], - "rotate_chat_vetkey", - encode_args((chat_id,)).unwrap(), - ) - .unwrap(); - assert_eq!(new_epoch, VetKeyEpochId(1)); - - // Verify epoch 0 reshared vetkey is still available - let result = env.update::, String>>( - participants[1], - "get_my_reshared_ibe_encrypted_vetkey", - encode_args((chat_id, VetKeyEpochId(0))).unwrap(), - ); - - assert_eq!( - result, - Ok(Some(IbeEncryptedVetKey(reshared_vetkey.clone()))) - ); - - // Try to get reshared vetkey for epoch 2 before it exists - let result = env.update::, String>>( - participants[1], - "get_my_reshared_ibe_encrypted_vetkey", - encode_args((chat_id, VetKeyEpochId(2))).unwrap(), - ); - - assert_eq!( - result, - Err(format!( - "vetKey epoch {:?} not found for chat {chat_id:?}", - VetKeyEpochId(2) - )) - ); - - // Fast forward time to expire epoch 0 - let expiry_time = group_chat_metadata.creation_timestamp.0 - + expiry_setting_minutes.0 * NANOSECONDS_IN_MINUTE; - env.pic - .set_time(pocket_ic::Time::from_nanos_since_unix_epoch(expiry_time)); - - // Verify epoch 0 reshared vetkey is expired - let result = env.update::, String>>( - participants[1], - "get_my_reshared_ibe_encrypted_vetkey", - encode_args((chat_id, VetKeyEpochId(0))).unwrap(), - ); - - assert_eq!( - result, - Err(format!("vetKey epoch {:?} expired", VetKeyEpochId(0))) - ); - - // Verify epoch 1 reshared vetkey is still available - env.update::, String>>( - participants[0], - "get_my_reshared_ibe_encrypted_vetkey", - encode_args((chat_id, VetKeyEpochId(1))).unwrap(), - ) - .unwrap(); - - // Fast forward time to expire epoch 1 - env.pic.advance_time(std::time::Duration::from_nanos(10)); - - let result = env.update::, String>>( - participants[1], - "get_my_reshared_ibe_encrypted_vetkey", - encode_args((chat_id, VetKeyEpochId(1))).unwrap(), - ); - - assert_eq!( - result, - Err(format!("vetKey epoch {:?} expired", VetKeyEpochId(1))) - ); - - // Try to reshare to expired epochs - for i in 0..2 { - let result = env.update::>( - participants[0], - "reshare_ibe_encrypted_vetkeys", - encode_args(( - chat_id, - VetKeyEpochId(i), - vec![(participants[1], IbeEncryptedVetKey(reshared_vetkey.clone()))], - )) - .unwrap(), - ); - - assert_eq!( - result, - Err(format!("vetKey epoch {:?} expired", VetKeyEpochId(i))) - ); - } - } -} - -#[test] -fn time_job_reports_cleaned_up_expired_items() { - let rng = &mut reproducible_rng(); - let env = TestEnvironment::new(rng); - - let user_cache = EncryptedSymmetricKeyEpochCache(b"dummy_symmetric_key".to_vec()); - let dummy_encrypted_vetkey = IbeEncryptedVetKey(b"dummy_encrypted_vetkey".to_vec()); - - // Create two group chats - let group_chat_metadata_1 = env - .update::>( - env.principal_0, - "create_group_chat", - encode_args((vec![env.principal_1], Time(30), Time(60))).unwrap(), - ) - .unwrap(); - - let group_chat_metadata_2 = env - .update::>( - env.principal_0, - "create_group_chat", - encode_args((vec![env.principal_2], Time(30), Time(60))).unwrap(), - ) - .unwrap(); - - let chat_id_1 = ChatId::Group(group_chat_metadata_1.chat_id); - let chat_id_2 = ChatId::Group(group_chat_metadata_2.chat_id); - - for i in 0..2 { - for j in 0..2 { - let user_message = UserMessage { - content: b"hello".to_vec(), - vetkey_epoch: VetKeyEpochId(i), - symmetric_key_epoch: SymmetricKeyEpochId(0), - nonce: Nonce(i + 2 * j), - }; - env.update::>( - env.principal_0, - "send_group_message", - encode_args((user_message.clone(), group_chat_metadata_1.chat_id)).unwrap(), - ) - .unwrap(); - env.update::>( - env.principal_0, - "send_group_message", - encode_args((user_message.clone(), group_chat_metadata_2.chat_id)).unwrap(), - ) - .unwrap(); - } - - for (chat_id, receiver) in [(chat_id_1, env.principal_1), (chat_id_2, env.principal_2)] { - env.update::>( - env.principal_0, - "update_my_symmetric_key_cache", - encode_args((chat_id, VetKeyEpochId(i), user_cache.clone())).unwrap(), - ) - .unwrap(); - - env.update::>( - env.principal_0, - "reshare_ibe_encrypted_vetkeys", - encode_args(( - chat_id, - VetKeyEpochId(i), - vec![(receiver, dummy_encrypted_vetkey.clone())], - )) - .unwrap(), - ) - .unwrap(); - - if i == 0 { - let new_epoch = env - .update::>( - env.principal_0, - "rotate_chat_vetkey", - encode_args((chat_id,)).unwrap(), - ) - .unwrap(); - - assert_eq!(new_epoch, VetKeyEpochId(1)); - } - } - } - - env.pic - .advance_time(std::time::Duration::from_secs(24 * 3600)); - env.pic.tick(); - - let logs = env - .pic - .fetch_canister_logs(env.canister_id, Principal::anonymous()) - .unwrap(); - let log_string = logs.iter().fold(String::new(), |acc, log| { - format!("{acc}{}", String::from_utf8(log.content.clone()).unwrap()) - }); - assert_eq!(log_string, "Timer job: cleaned up 0 expired direct messages, 8 expired group messages, 4 expired vetkey epochs caches, 4 expired reshared vetkeys"); -} diff --git a/rust/vetkeys/encrypted_chat/rust/backend/tests/misc.rs b/rust/vetkeys/encrypted_chat/rust/backend/tests/misc.rs deleted file mode 100644 index de339a71f..000000000 --- a/rust/vetkeys/encrypted_chat/rust/backend/tests/misc.rs +++ /dev/null @@ -1,121 +0,0 @@ -use candid::encode_args; -use candid::Principal; -use ic_vetkeys_example_encrypted_chat_backend::types::*; -use rand::seq::SliceRandom; -use rand::Rng; - -mod common; -use common::*; - -/// This test ensures that the minimum value of the ChatId enum is the direct chat with the management canister as both participants. -#[test] -fn test_chat_id_min_value() { - assert_eq!( - ChatId::MIN_VALUE, - ChatId::Direct(DirectChatId::new(( - Principal::management_canister(), - Principal::management_canister(), - ))) - ); - - assert!(ChatId::MIN_VALUE < ChatId::Group(GroupChatId(0))); - - // modify this test if more enum variants are added - match ChatId::MIN_VALUE { - ChatId::Direct(_) => {} - ChatId::Group(_) => {} - }; -} - -#[test] -fn can_create_many_chats() { - let rng = &mut reproducible_rng(); - let env = TestEnvironment::new(rng); - - let mut num_group_chats = 0; - let mut num_direct_chats = 0; - - let participants = (0..10) - .map(|_| random_self_authenticating_principal(rng)) - .collect::>(); - - let mut expected_chat_ids = std::collections::BTreeMap::new(); - - while num_group_chats < 10 || num_direct_chats < 10 { - let direct = rng.random_bool(0.5); - - if direct { - let p_0 = participants[0]; - let p_1 = participants[rng.random_range(0..participants.len())]; - - let chat_id = ChatId::Direct(DirectChatId::new((p_0, p_1))); - if expected_chat_ids.insert((p_0, chat_id), ()).is_some() { - continue; - } - expected_chat_ids.insert((p_1, chat_id), ()); - - env.update::>( - p_0, - "create_direct_chat", - encode_args((p_1, Time(1_000), Time(10_000))).unwrap(), - ) - .unwrap(); - - num_direct_chats += 1; - } else { - let number_of_participants = rng.random_range(1..5); - let mut invited_participants = participants.clone(); - invited_participants.shuffle(rng); - invited_participants.truncate(number_of_participants); - let group_creator = invited_participants.split_off(0); - - if group_creator[0] != participants[0] - && !invited_participants.contains(&participants[0]) - { - invited_participants.push(participants[0].clone()); - } - - let chat_id = ChatId::Group(GroupChatId(num_group_chats)); - - assert!(expected_chat_ids - .insert((group_creator[0], chat_id), ()) - .is_none()); - for p in invited_participants.clone() { - assert!(expected_chat_ids.insert((p, chat_id), ()).is_none()); - } - - env.update::>( - group_creator[0], - "create_group_chat", - encode_args((invited_participants, Time(1_000), Time(10_000))).unwrap(), - ) - .unwrap(); - - num_group_chats += 1; - } - - for p in participants.clone() { - let my_chat_ids = env.query::>( - p, - "get_my_chat_ids", - encode_args(()).unwrap(), - ); - - let expected_chat_ids = expected_chat_ids - .range((p, ChatId::MIN_VALUE)..) - .take_while(|(key, _)| key.0 == p) - .map(|(key, _value)| (key.1, ChatMessageId(0))) - .collect::>(); - - assert_eq!(my_chat_ids, expected_chat_ids); - } - - let p0_chat_ids = env.query::>( - participants[0], - "get_my_chat_ids", - encode_args(()).unwrap(), - ); - - assert_eq!(p0_chat_ids.len() as u64, num_group_chats + num_direct_chats); - } -} diff --git a/rust/vetkeys/encrypted_chat/rust/dfx.json b/rust/vetkeys/encrypted_chat/rust/dfx.json deleted file mode 100644 index c9525e274..000000000 --- a/rust/vetkeys/encrypted_chat/rust/dfx.json +++ /dev/null @@ -1,50 +0,0 @@ -{ - "canisters": { - "encrypted_chat": { - "candid": "backend/backend.did", - "type": "custom", - "init_arg": "(\"test_key_1\")", - "gzip": true, - "wasm": "target/wasm32-unknown-unknown/release/ic_vetkeys_example_encrypted_chat_backend.wasm", - "build": [ - "cd backend && cargo build --release --target wasm32-unknown-unknown && candid-extractor ../target/wasm32-unknown-unknown/release/ic_vetkeys_example_encrypted_chat_backend.wasm > backend.did" - ], - "metadata": [ - { - "name": "candid:service", - "visibility": "public" - } - ] - }, - "internet-identity": { - "candid": "https://github.com/dfinity/internet-identity/releases/download/release-2026-03-16/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/download/release-2026-03-16/internet_identity_dev.wasm.gz" - }, - "www": { - "dependencies": ["encrypted_chat", "internet-identity"], - "build": [ - "cp .env frontend/.env && cd frontend && npm i --include=dev && npm run build && cd - && rm -r dist > /dev/null 2>&1; mv frontend/dist ./" - ], - "frontend": { - "entrypoint": "dist/index.html" - }, - "gzip": true, - "source": ["dist/"], - "type": "assets" - } - }, - "output_env_file": ".env", - "networks": { - "local": { - "bind": "127.0.0.1:4943", - "type": "ephemeral" - } - } -} diff --git a/rust/vetkeys/encrypted_chat/rust/frontend b/rust/vetkeys/encrypted_chat/rust/frontend deleted file mode 120000 index af288785f..000000000 --- a/rust/vetkeys/encrypted_chat/rust/frontend +++ /dev/null @@ -1 +0,0 @@ -../frontend \ No newline at end of file diff --git a/rust/vetkeys/encrypted_chat/rust/rust-toolchain.toml b/rust/vetkeys/encrypted_chat/rust/rust-toolchain.toml deleted file mode 100644 index 2a2058b04..000000000 --- a/rust/vetkeys/encrypted_chat/rust/rust-toolchain.toml +++ /dev/null @@ -1,4 +0,0 @@ -[toolchain] -channel = "1.88.0" -targets = ["wasm32-unknown-unknown"] -profile = "default" \ No newline at end of file