- 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/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 (
-
- );
-}
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/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 (
-
- );
-};
-
-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/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 @@
-
\ 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 @@
-
-
-
-
- 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)}
-