diff --git a/.github/actions/setup/action.yml b/.github/actions/setup/action.yml index 346407f0..8778251d 100644 --- a/.github/actions/setup/action.yml +++ b/.github/actions/setup/action.yml @@ -15,8 +15,24 @@ runs: with: fetch-depth: 1 + - name: Set Compact environment variables + shell: bash + run: | + COMPACT_HOME="$HOME/compactc" + echo "COMPACT_HOME=$COMPACT_HOME" >> "$GITHUB_ENV" + echo "$COMPACT_HOME" >> "$GITHUB_PATH" + + - name: Cache Compact compiler + id: cache-compact + uses: actions/cache@v4 + with: + path: ${{ env.COMPACT_HOME }} + key: compact-compiler-0.23.0 + restore-keys: | + compact-compiler- + - name: Install Compact compiler - id: setup + if: steps.cache-compact.outputs.cache-hit != 'true' shell: bash env: COMPILER_VERSION: "0.23.0" @@ -24,7 +40,6 @@ runs: run: | set -euo pipefail # Create directory for compiler - COMPACT_HOME="$HOME/compactc" mkdir -p "$COMPACT_HOME" # Create URL @@ -38,10 +53,9 @@ runs: unzip -qo "$COMPACT_HOME/compactc.zip" -d "$COMPACT_HOME" chmod +x "$COMPACT_HOME"/{compactc,compactc.bin,zkir} - echo "📁 Setting environment variables..." - echo "COMPACT_HOME=$COMPACT_HOME" >> "$GITHUB_ENV" - echo "$COMPACT_HOME" >> "$GITHUB_PATH" - + - name: Verify Compact compiler installation + shell: bash + run: | echo "✅ Verifying installation..." if [ ! -f "$COMPACT_HOME/compactc" ]; then echo "::error::❌ compactc not found in $COMPACT_HOME" @@ -51,14 +65,6 @@ runs: echo "🤖 Testing installation..." "$COMPACT_HOME/compactc" --version - - name: Cache Compact compiler - uses: actions/cache@v4 - with: - path: ${{ env.COMPACT_HOME }} - key: compact-compiler-0.23.0 - restore-keys: | - compact-compiler- - - name: Install pnpm uses: pnpm/action-setup@v2 with: @@ -95,3 +101,27 @@ runs: - name: Install dependencies shell: bash run: pnpm install --frozen-lockfile --prefer-offline + + - name: Build contract packages (with retry on hash mismatch) + if: inputs.skip-compile != 'true' + shell: bash + run: | + set -euo pipefail + + build_contracts() { + echo "⚙️ Building contract packages..." + if ! pnpm build:contracts; then + echo "❌ Build failed." + if [ -d "$HOME/.cache/midnight/zk-params" ]; then + echo "⚠️ zk-params exists. Removing cache and retrying..." + rm -rf "$HOME/.cache/midnight/zk-params" + echo "::notice::♻️ Retrying build after clearing zk-params..." + pnpm build:contracts || { echo "::error::❌ Retry also failed."; exit 1; } + else + echo "🚫 Build failed and zk-params missing. No retry." + exit 1 + fi + fi + } + + build_contracts diff --git a/.github/workflows/apps.yml b/.github/workflows/apps.yml index dbce804e..2f2246e0 100644 --- a/.github/workflows/apps.yml +++ b/.github/workflows/apps.yml @@ -46,6 +46,9 @@ jobs: run: pnpm test:apps timeout-minutes: 10 + - name: Check types for apps and packages + run: pnpm types:apps && pnpm types:packages + - name: Upload build artifacts if: steps.build.outcome == 'success' uses: actions/upload-artifact@v4 diff --git a/.github/workflows/checks.yml b/.github/workflows/checks.yml index 9a5d0ab9..8a1d42d9 100644 --- a/.github/workflows/checks.yml +++ b/.github/workflows/checks.yml @@ -18,22 +18,22 @@ jobs: - name: Checkout repository uses: actions/checkout@v5 with: - fetch-depth: 1 + fetch-depth: 0 - name: Setup Environment uses: ./.github/actions/setup - - name: Lint - run: pnpm lint + - name: Lint and Format + run: pnpm lint && pnpm fmt timeout-minutes: 10 - - name: Format - run: pnpm fmt - timeout-minutes: 10 + - name: Commitlint + run: pnpm commitlint --from=${{ github.event.pull_request.base.sha }} --to=${{ github.event.pull_request.head.sha }} - - name: Type check - run: pnpm types - timeout-minutes: 10 + - name: Spell Check with Typos + uses: crate-ci/typos@v1.36.2 + with: + config: .github/workflows/config/typos.toml - name: Cleanup on failure if: failure() diff --git a/.github/workflows/commitlint.yml b/.github/workflows/commitlint.yml deleted file mode 100644 index 96756c4a..00000000 --- a/.github/workflows/commitlint.yml +++ /dev/null @@ -1,33 +0,0 @@ -name: Commit - -on: - push: - branches: [main] - pull_request: - branches: [main] - -concurrency: - group: commitlint-${{ github.run_id }}-${{ github.ref }} - cancel-in-progress: true - -jobs: - lint: - runs-on: ubuntu-22.04 - - steps: - - name: Checkout repository - uses: actions/checkout@v5 - with: - fetch-depth: 0 - - - name: Setup Environment - uses: ./.github/actions/setup - - - name: Check commit messages - run: pnpm commitlint --from=${{ github.event.pull_request.base.sha }} --to=${{ github.event.pull_request.head.sha }} - - - name: Cleanup on failure - if: failure() - run: | - echo "Linting failed. Cleaning up..." - rm -rf contracts/*/src/artifacts/ diff --git a/.github/workflows/contracts.yml b/.github/workflows/contracts.yml index 15a8411d..352b43b2 100644 --- a/.github/workflows/contracts.yml +++ b/.github/workflows/contracts.yml @@ -46,28 +46,8 @@ jobs: echo "✅ Language version matches: $COMPUTED_LANGUAGE_VERSION" - - name: Compile contracts (with retry on hash mismatch) - shell: bash - run: | - set -euo pipefail - - compile() { - echo "⚙️ Running Compact compilation..." - if ! pnpm compact; then - echo "❌ Compilation failed." - if [ -d "$HOME/.cache/midnight/zk-params" ]; then - echo "⚠️ zk-params exists. Removing cache and retrying..." - rm -rf "$HOME/.cache/midnight/zk-params" - echo "::notice::♻️ Retrying compilation after clearing zk-params..." - pnpm compact || { echo "::error::❌ Retry also failed."; exit 1; } - else - echo "🚫 Compilation failed and zk-params missing. No retry." - exit 1 - fi - fi - } - - compile - - name: Run tests run: pnpm test:contracts + + - name: Check types + run: pnpm types:contracts diff --git a/.github/workflows/typos.yml b/.github/workflows/typos.yml deleted file mode 100644 index a8599f4c..00000000 --- a/.github/workflows/typos.yml +++ /dev/null @@ -1,22 +0,0 @@ -name: Typo Check - -on: - push: - branches: [main] - pull_request: - branches: [main] - -concurrency: - group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }} - cancel-in-progress: true - -jobs: - typos: - name: Spell Check with Typos - runs-on: ubuntu-22.04 - steps: - - uses: actions/checkout@v5 - - name: Use typos with config file - uses: crate-ci/typos@v1.38.1 - with: - config: .github/workflows/config/typos.toml diff --git a/README.md b/README.md index 74ccccf2..b07128f3 100644 --- a/README.md +++ b/README.md @@ -21,19 +21,19 @@ nvm use 22.14.0 1. **Clone the Repository**: ```bash git clone - cd midnight-dapps + cd midnight-apps ``` 2. **Install Dependencies**: ```bash pnpm install ``` - - This installs all workspace dependencies and runs the `prepare` script, which sets up Husky and builds `@midnight-dapps/compact`. + - This installs all workspace dependencies and runs the `prepare` script, which sets up Husky and builds `@openzeppelin-midnight-apps/compact`. 3. **Workspace Structure**: - - `contracts/*`: Compact Smart contract projects (e.g., `@midnight-dapps/access-contract`). - - `packages/*`: Utility packages (e.g., `@midnight-dapps/compact`). - - `apps/*`: Frontend applications (e.g., `@midnight-dapps/lunarswap-ui`). + - `contracts/*`: Compact Smart contract projects (e.g., `@openzeppelin-midnight-apps/access-contract`). + - `packages/*`: Utility packages (e.g., `@openzeppelin-midnight-apps/compact`). + - `apps/*`: Frontend applications (e.g., `@openzeppelin-midnight-apps/lunarswap-ui`). See `pnpm-workspace.yaml` for the full list. @@ -86,7 +86,7 @@ Commits are linted with `commitlint` and staged files are processed with `lint-s 2. **Pre-Commit Hook**: - Runs `turbo run precommit` via `.husky/pre-commit`. - - For `@midnight-dapps/access-contract`: + - For `@openzeppelin-midnight-apps/access-contract`: - `lint-staged`: Formats and lints staged files (see `.lintstagedrc.json`). - `pnpm run types`: Checks TypeScript types. @@ -104,7 +104,7 @@ Output: husky - pre-commit hook > turbo run precommit • Running precommit in X packages -• @midnight-dapps/access-contract:precommit +• @openzeppelin-midnight-apps/access-contract:precommit Tasks: 1 successful, 1 total Time: 500ms diff --git a/apps/lunarswap-ui/biome.json b/apps/lunarswap-ui/biome.json index 98ebf7d2..d4a1d599 100644 --- a/apps/lunarswap-ui/biome.json +++ b/apps/lunarswap-ui/biome.json @@ -1,3 +1,3 @@ { - "extends": ["@midnight-dapps/biome-config"] + "extends": ["@openzeppelin-midnight-apps/biome-config"] } diff --git a/apps/lunarswap-ui/package.json b/apps/lunarswap-ui/package.json index 0c60a710..98404ca6 100644 --- a/apps/lunarswap-ui/package.json +++ b/apps/lunarswap-ui/package.json @@ -1,5 +1,5 @@ { - "name": "@midnight-dapps/lunarswap-ui", + "name": "@openzeppelin-midnight-apps/lunarswap-ui", "version": "1.0.0-alpha.1", "private": true, "scripts": { @@ -69,7 +69,7 @@ "zod": "^3.25.67" }, "devDependencies": { - "@midnight-dapps/biome-config": "workspace:^", + "@openzeppelin-midnight-apps/biome-config": "workspace:^", "@types/node": "^22", "@types/react": "^19", "@types/react-dom": "^19", diff --git a/contracts/access/biome.json b/contracts/access/biome.json index 98ebf7d2..d4a1d599 100644 --- a/contracts/access/biome.json +++ b/contracts/access/biome.json @@ -1,3 +1,3 @@ { - "extends": ["@midnight-dapps/biome-config"] + "extends": ["@openzeppelin-midnight-apps/biome-config"] } diff --git a/contracts/access/package.json b/contracts/access/package.json index 411e7087..426907a6 100644 --- a/contracts/access/package.json +++ b/contracts/access/package.json @@ -1,5 +1,5 @@ { - "name": "@midnight-dapps/access-contract", + "name": "@openzeppelin-midnight-apps/access-control", "version": "1.0.0-alpha.1", "private": true, "description": "Compacts for implementing LunarSwap DEX", @@ -26,9 +26,9 @@ "vitest": "^3.1.4" }, "dependencies": { - "@midnight-dapps/biome-config": "workspace:^", - "@midnight-dapps/compact": "workspace:^", - "@midnight-dapps/compact-std": "workspace:^", + "@openzeppelin-midnight-apps/biome-config": "workspace:^", + "@openzeppelin-midnight-apps/compact": "workspace:^", + "@openzeppelin-midnight-apps/compact-std": "workspace:^", "@midnight-ntwrk/compact-runtime": "^0.8.1", "@midnight-ntwrk/midnight-js-network-id": "^2.0.1", "@midnight-ntwrk/zswap": "^4.0.0" diff --git a/contracts/access/src/index.ts b/contracts/access/src/index.ts index 37b5c67c..0589633c 100644 --- a/contracts/access/src/index.ts +++ b/contracts/access/src/index.ts @@ -1,5 +1,5 @@ /** - * @module @midnight-dapps/access-contract + * @module @openzeppelin-midnight-apps/access-contract * @description Main entry point for the AccessControl contract package, exporting private state utilities, witness implementations, and type definitions. * @remarks This module serves as the primary export point for TypeScript consumers of the AccessControl contract. */ diff --git a/contracts/access/src/test/AccessControlContractSimulator.ts b/contracts/access/src/test/AccessControlContractSimulator.ts index 1d6d77c2..8f213a63 100644 --- a/contracts/access/src/test/AccessControlContractSimulator.ts +++ b/contracts/access/src/test/AccessControlContractSimulator.ts @@ -1,4 +1,3 @@ -import type { ZswapCoinPublicKey } from '@midnight-dapps/compact-std'; import { type CircuitContext, type CoinPublicKey, @@ -9,6 +8,7 @@ import { encodeCoinPublicKey, } from '@midnight-ntwrk/compact-runtime'; import { sampleContractAddress } from '@midnight-ntwrk/zswap'; +import type { ZswapCoinPublicKey } from '@openzeppelin-midnight-apps/compact-std'; import type { AccessControl_Role } from '../artifacts/Index/contract/index.cjs'; import { type Ledger, diff --git a/contracts/access/src/types/ledger.ts b/contracts/access/src/types/ledger.ts index 2a982cac..9a7c5d9f 100644 --- a/contracts/access/src/types/ledger.ts +++ b/contracts/access/src/types/ledger.ts @@ -1,5 +1,5 @@ /** - * @module @midnight-dapps/access-contract/types/ledger + * @module @openzeppelin-midnight-apps/access-contract/types/ledger * @description Re-exports ledger-related types from the AccessControl contract’s Index artifact. * @remarks Renames AccessControl_Role to AccessControlRole to follow TypeScript naming conventions (camelCase over snake_case). */ diff --git a/contracts/access/src/types/role.ts b/contracts/access/src/types/role.ts index ae065acc..ed54bfc0 100644 --- a/contracts/access/src/types/role.ts +++ b/contracts/access/src/types/role.ts @@ -1,4 +1,4 @@ -import type { MerkleTreePath } from '@midnight-dapps/compact-std'; +import type { MerkleTreePath } from '@openzeppelin-midnight-apps/compact-std'; import type { AccessControlRole } from './ledger'; /** diff --git a/contracts/access/src/utils/compactHelper.ts b/contracts/access/src/utils/compactHelper.ts index c22b19dc..1df8a2cb 100644 --- a/contracts/access/src/utils/compactHelper.ts +++ b/contracts/access/src/utils/compactHelper.ts @@ -1,4 +1,4 @@ -import type { Maybe } from '@midnight-dapps/compact-std'; +import type { Maybe } from '@openzeppelin-midnight-apps/compact-std'; /** * @description Converts a nullable value into a Maybe type, providing a default empty value if null or undefined. diff --git a/contracts/access/src/witnesses/AccessControlWitnesses.ts b/contracts/access/src/witnesses/AccessControlWitnesses.ts index 5ec4644e..a46f5f0b 100644 --- a/contracts/access/src/witnesses/AccessControlWitnesses.ts +++ b/contracts/access/src/witnesses/AccessControlWitnesses.ts @@ -1,10 +1,10 @@ import { getRandomValues } from 'node:crypto'; +import type { WitnessContext } from '@midnight-ntwrk/compact-runtime'; import type { Maybe, MerkleTreePath, ZswapCoinPublicKey, -} from '@midnight-dapps/compact-std'; -import type { WitnessContext } from '@midnight-ntwrk/compact-runtime'; +} from '@openzeppelin-midnight-apps/compact-std'; import { AccessControl_Role, type Ledger, diff --git a/contracts/access/src/witnesses/interface.ts b/contracts/access/src/witnesses/interface.ts index 4c59f67d..f6fdd072 100644 --- a/contracts/access/src/witnesses/interface.ts +++ b/contracts/access/src/witnesses/interface.ts @@ -1,5 +1,8 @@ -import type { Maybe, MerkleTreePath } from '@midnight-dapps/compact-std'; import type { WitnessContext } from '@midnight-ntwrk/compact-runtime'; +import type { + Maybe, + MerkleTreePath, +} from '@openzeppelin-midnight-apps/compact-std'; import type { AccessControl_Role, Ledger, diff --git a/contracts/math/README.md b/contracts/math/README.md index 3832de21..f11a5716 100644 --- a/contracts/math/README.md +++ b/contracts/math/README.md @@ -1,4 +1,4 @@ -# `@midnight-dapps/math-contracts` +# `@openzeppelin-midnight-apps/math-contracts` A comprehensive mathematical operations library for Midnight Network smart contracts, providing efficient and secure arithmetic operations for various integer types. @@ -125,7 +125,7 @@ The following table shows the constraint counts and circuit sizes for each mathe ## Installation ```bash -pnpm add @midnight-dapps/math-contracts +pnpm add @openzeppelin-midnight-apps/math-contracts ``` ## Usage @@ -137,13 +137,13 @@ import { MathU64Witnesses, sqrtBigint, type MathU64ContractPrivateState -} from '@midnight-dapps/math-contracts'; +} from '@openzeppelin-midnight-apps/math-contracts'; ``` ### Square Root Calculation ```typescript -import { sqrtBigint } from '@midnight-dapps/math-contracts'; +import { sqrtBigint } from '@openzeppelin-midnight-apps/math-contracts'; // Calculate square root of a bigint const result = sqrtBigint(16n); // Returns 4n @@ -153,7 +153,7 @@ const largeNumber = sqrtBigint(1000000000000n); // Efficient for large numbers ### Witness Operations ```typescript -import { MathU64Witnesses } from '@midnight-dapps/math-contracts'; +import { MathU64Witnesses } from '@openzeppelin-midnight-apps/math-contracts'; // Create witness implementations const witnesses = MathU64Witnesses(); @@ -314,6 +314,6 @@ ISC License - see package.json for details. ## Related Packages -- `@midnight-dapps/compact` - Core Compact framework +- `@openzeppelin-midnight-apps/compact` - Core Compact framework - `@midnight-ntwrk/compact-runtime` - Runtime utilities - `@midnight-ntwrk/zswap` - ZK-SNARK operations diff --git a/contracts/math/biome.json b/contracts/math/biome.json index 98ebf7d2..d4a1d599 100644 --- a/contracts/math/biome.json +++ b/contracts/math/biome.json @@ -1,3 +1,3 @@ { - "extends": ["@midnight-dapps/biome-config"] + "extends": ["@openzeppelin-midnight-apps/biome-config"] } diff --git a/contracts/math/package.json b/contracts/math/package.json index b220cf79..f226064b 100644 --- a/contracts/math/package.json +++ b/contracts/math/package.json @@ -1,5 +1,5 @@ { - "name": "@midnight-dapps/math-contracts", + "name": "@openzeppelin-midnight-apps/math", "version": "1.0.0-alpha.1", "private": true, "description": "Compacts contracts math operations.", @@ -29,8 +29,8 @@ "vitest": "^3.1.4" }, "dependencies": { - "@midnight-dapps/biome-config": "workspace:^", - "@midnight-dapps/compact": "workspace:^", + "@openzeppelin-midnight-apps/biome-config": "workspace:^", + "@openzeppelin-midnight-apps/compact": "workspace:^", "@midnight-ntwrk/compact-runtime": "^0.8.1", "@midnight-ntwrk/midnight-js-network-id": "^2.0.1", "@midnight-ntwrk/zswap": "^4.0.0" diff --git a/contracts/math/src/Index.compact b/contracts/math/src/Index.compact index c24dfeb6..a0a7dcc4 100644 --- a/contracts/math/src/Index.compact +++ b/contracts/math/src/Index.compact @@ -6,7 +6,7 @@ pragma language_version >= 0.15.0; * @description Re-exports ledger-related types and state from Math.compact for use in contracts and TypeScript. * * This module serves as a centralized export point for all mathematical types, constants, and result structures - * used throughout the midnight-dapps math contracts. It provides a clean interface for importing the necessary + * used throughout the midnight-apps math contracts. It provides a clean interface for importing the necessary * components without having to import from multiple individual files. * * The module re-exports: diff --git a/contracts/math/src/index.ts b/contracts/math/src/index.ts index 1b0e720c..a87fafef 100644 --- a/contracts/math/src/index.ts +++ b/contracts/math/src/index.ts @@ -1,5 +1,5 @@ /** - * @module @midnight-dapps/math-contract + * @module @openzeppelin-midnight-apps/math-contract * @description Main entry point for the Math contract package, exporting private state utilities, witness implementations, and type definitions. */ diff --git a/contracts/shielded-token/.lintstagedrc.json b/contracts/shielded-token/.lintstagedrc.json new file mode 100644 index 00000000..8a7175ab --- /dev/null +++ b/contracts/shielded-token/.lintstagedrc.json @@ -0,0 +1,3 @@ +{ + "*.{ts,md}": ["biome format --write", "biome check --write"] +} diff --git a/contracts/shielded-token/README.md b/contracts/shielded-token/README.md new file mode 100644 index 00000000..bbb8e7a1 --- /dev/null +++ b/contracts/shielded-token/README.md @@ -0,0 +1,3 @@ +# Shielded Fungible Token Contract + +This package contains the Shielded Fungible Token contract implementation for Midnight DApps. diff --git a/contracts/shielded-token/biome.json b/contracts/shielded-token/biome.json new file mode 100644 index 00000000..a4b15425 --- /dev/null +++ b/contracts/shielded-token/biome.json @@ -0,0 +1,4 @@ +{ + "$schema": "https://biomejs.dev/schemas/1.9.4/schema.json", + "extends": ["@openzeppelin-midnight-apps/biome-config"] +} diff --git a/contracts/shielded-token/package.json b/contracts/shielded-token/package.json new file mode 100644 index 00000000..0d791daa --- /dev/null +++ b/contracts/shielded-token/package.json @@ -0,0 +1,48 @@ +{ + "name": "@openzeppelin-midnight-apps/shielded-token", + "version": "1.0.0-alpha.1", + "private": true, + "description": "Shielded Fungible Token contract for Midnight DApps", + "type": "module", + "main": "dist/index.js", + "types": "dist/index.d.ts", + "exports": { + ".": { + "types": "./dist/index.d.ts", + "default": "./dist/index.js" + }, + "./artifacts/*": "./dist/artifacts/*", + "./keys/*": "./dist/artifacts/ShieldedToken/keys/*", + "./zkir/*": "./dist/artifacts/ShieldedToken/zkir/*" + }, + "scripts": { + "compact": "pnpm exec compact-compiler", + "compact:fast": "pnpm exec compact-compiler --skip-zk", + "build": "pnpm exec compact-builder && tsc", + "test": "vitest run --printConsoleTrace", + "types": "tsc -p tsconfig.build.json --noEmit", + "fmt": "biome format --write", + "lint": "biome lint", + "lint:fix": "biome check --write", + "precommit": "lint-staged --no-stash && pnpm run types" + }, + "keywords": ["midnight", "shielded", "token", "fungible"], + "author": "", + "license": "ISC", + "packageManager": "pnpm@10.4.1", + "devDependencies": { + "@types/node": "^22.15.29", + "typescript": "^5.8.3", + "vitest": "^3.1.4" + }, + "dependencies": { + "@openzeppelin-midnight-apps/biome-config": "workspace:^", + "@openzeppelin-midnight-apps/compact": "workspace:^", + "@openzeppelin-midnight-apps/compact-std": "workspace:^", + "@openzeppelin-midnight-apps/math": "workspace:^", + "@midnight-ntwrk/compact-runtime": "^0.8.1", + "@midnight-ntwrk/midnight-js-network-id": "^2.0.1", + "@midnight-ntwrk/zswap": "^4.0.0", + "@midnight-ntwrk/wallet-sdk-address-format": "2.0.0" + } +} diff --git a/contracts/shielded-token/src/ShieldedFungibleToken.compact b/contracts/shielded-token/src/ShieldedFungibleToken.compact new file mode 100644 index 00000000..2a8e729d --- /dev/null +++ b/contracts/shielded-token/src/ShieldedFungibleToken.compact @@ -0,0 +1,56 @@ +// SPDX-License-Identifier: MIT + +pragma language_version >= 0.15.0; + +import CompactStandardLibrary; + +import "./openzeppelin/ShieldedERC20" prefix ShieldedFungibleToken_; + +export { + ShieldedFungibleToken__nonce, + ShieldedFungibleToken__domain, +}; + +constructor( + nonce_: Bytes<32>, + name_: Opaque<"string">, + symbol_: Opaque<"string">, + domain_: Bytes<32> +) { + const decimals = 18; + ShieldedFungibleToken_initialize(nonce_, name_, symbol_, decimals, domain_); +} + +export circuit name(): Opaque<"string"> { + return ShieldedFungibleToken_name(); +} + +export circuit symbol(): Opaque<"string"> { + return ShieldedFungibleToken_symbol(); +} + +export circuit decimals(): Uint<8> { + return ShieldedFungibleToken_decimals(); +} + +export circuit totalSupply(): Uint<128> { + return ShieldedFungibleToken_totalSupply(); +} + +export circuit type(): Bytes<32> { + return ShieldedFungibleToken_type(); +} + +export circuit mint( + recipient: Either, + amount: Uint<128> +): CoinInfo { + return ShieldedFungibleToken_mint(recipient, amount); +} + +export circuit burn( + coin: CoinInfo, + amount: Uint<128> +): SendResult { + return ShieldedFungibleToken_burn(coin, amount); +} diff --git a/contracts/shielded-token/src/index.ts b/contracts/shielded-token/src/index.ts new file mode 100644 index 00000000..4d210e31 --- /dev/null +++ b/contracts/shielded-token/src/index.ts @@ -0,0 +1,21 @@ +// Export contract types and interfaces from ShieldedFungibleToken +// biome-ignore lint/performance/noBarrelFile: entrypoint module +export { + type Witnesses, + type ImpureCircuits, + type PureCircuits, + type Circuits, + type Ledger, + type ContractReferenceLocations, + contractReferenceLocations, + Contract, + ledger, + pureCircuits, +} from './artifacts/ShieldedFungibleToken/contract/index.cjs'; + +// Export witnesses +export { + ShieldedFungibleTokenPrivateState, + ShieldedFungibleTokenWitnesses, + type IShieldedFungibleTokenWitnesses, +} from './witnesses/index.js'; diff --git a/contracts/shielded-token/src/openzeppelin/ShieldedERC20.compact b/contracts/shielded-token/src/openzeppelin/ShieldedERC20.compact new file mode 100644 index 00000000..b4d1254c --- /dev/null +++ b/contracts/shielded-token/src/openzeppelin/ShieldedERC20.compact @@ -0,0 +1,169 @@ +// SPDX-License-Identifier: MIT + +pragma language_version >= 0.15.0; + +/** + * @module ShieldedToken (archived until further notice, DO NOT USE IN PRODUCTION) + * @description A shielded token module. + * + * @notice This module utilizes the existing coin infrastructure of Midnight. + * Due to the current limitations of the network, this module should NOT be used. + * + * Some of the limitations include: + * + * - No custom spend logic. Once users receive tokens, there's no mechanism to + * enforce any token behaviors. This is a big issue with stable coins, for instance. + * Most stable coins want the ability to pause functionality and/or freeze assets from + * specific addresses. This is currently not possible. + * + * - Cannot guarantee proper total supply accounting. The total supply of a given token + * is stored in the contract state. There's nothing to prevent users from burning + * tokens manually by directly sending them to the burn address. This breaks the + * total supply accounting (and potentially many other mechanisms). + * + * @notice This module will be revisited when the Midnight network can offer solutions to these + * issues. Until then, the recommendation is to use unshielded tokens. + * + * @dev Future ideas to consider: + * + * - Provide a self-minting mechanism. + * - Enable the Shielded contract itself to transfer. + * - Should this be a part of the Shielded module itself or as an extension? + */ +module ShieldedERC20 { // DO NOT USE IN PRODUCTION! + import CompactStandardLibrary; + import Utils prefix Utils_; + import "../../node_modules/@openzeppelin-midnight-apps/math/dist/Bytes32" prefix Bytes32_; + + // Public state + export ledger _counter: Counter; + export ledger _nonce: Bytes<32>; + export ledger _totalSupply: Uint<128>; + export sealed ledger _domain: Bytes<32>; + export sealed ledger _name: Opaque<"string">; + export sealed ledger _symbol: Opaque<"string">; + export sealed ledger _decimals: Uint<8>; + export ledger _type: Bytes<32>; + + /** + * @description Initializes the contract by setting the initial nonce + * and the metadata. + * + * @return {[]} - None. + */ + export circuit initialize( + initNonce: Bytes<32>, + name_: Opaque<"string">, + symbol_: Opaque<"string">, + decimals_ :Uint<8>, + domain_: Bytes<32> + ): [] { + _nonce = initNonce; + _domain = domain_; + _name = name_; + _symbol = symbol_; + _decimals = decimals_; + } + + /** + * @description Returns the token name. + * + * @return {Maybe>} - The token name. + */ + export circuit name(): Opaque<"string"> { + return _name; + } + + /** + * @description Returns the symbol of the token. + * + * @return {Maybe>} - The token name. + */ + export circuit symbol(): Opaque<"string"> { + return _symbol; + } + + /** + * @description Returns the number of decimals used to get its user representation. + * + * @return {Uint<8>} - The account's token balance. + */ + export circuit decimals(): Uint<8> { + return _decimals; + } + + /** + * @description Returns the type of the token. + * + * @return {Bytes<32>} - The type of the token. + */ + export circuit type(): Bytes<32> { + return _type; + } + + /** + * @description Returns the value of tokens in existence. + * @notice The total supply accounting mechanism cannot be guaranteed to be accurate. + * There is nothing to prevent users from directly sending tokens to the burn + * address without going through the contract; thus, tokens will be burned + * but the accounted supply will not change. + * + * @return {Uint<64>} - The total supply of tokens. + */ + export circuit totalSupply(): Uint<128> { + return _totalSupply; + } + + /** + * @description Mints `amount` of tokens to `recipient`. + * @dev This circuit does not include access control meaning anyone can call it. + * + * @param {recipient} - The ZswapCoinPublicKey or ContractAddress that receives the minted tokens. + * @param {amount} - The value of tokens minted. + * @return {CoinInfo} - The description of the newly created coin. + */ + export circuit mint(recipient: Either, amount: Uint<128>): CoinInfo { + assert !Utils_isKeyOrAddressZero(recipient) "ShieldedToken: invalid recipient"; + + // TODO: Is that correct here? Do we need to increment after minting? not before? + _counter.increment(1); + const newNonce = evolve_nonce(_counter, _nonce); + _nonce = newNonce; + const ret = mint_token(_domain, amount, _nonce, recipient); + // TODO: find a better solution for detecting the type + if (Bytes32_isZero(_type)) { + _type = ret.color; + } + _totalSupply = _totalSupply + amount as Uint<128>; + return ret; + } + + /** + * @description Destroys `amount` of `coin` by sending it to the burn address. + * @dev This circuit does not include access control meaning anyone can call it. + * @throws Will throw if `coin` color is not this contract's token type. + * @throws Will throw if `amount` is less than `coin` value. + * + * @param {coin} - The coin description that will be burned. + * @param {amount} - The value of `coin` that will be burned. + * @return {SendResult} - The output of sending tokens to the burn address. This may include change from + * spending the output if available. + */ + export circuit burn(coin: CoinInfo, amount: Uint<128>): SendResult { + assert coin.color == _type "ShieldedToken: token not created from this contract"; + assert coin.value >= amount "ShieldedToken: insufficient token amount to burn"; + + receive(coin); + _totalSupply = _totalSupply - amount; + + const sendRes = send_immediate(coin, burn_address(), amount); + if (sendRes.change.is_some) { + // tmp for only zswap because we should be able to handle contracts burning tokens + // and returning change. + const caller = left(own_public_key()); + send_immediate(sendRes.change.value, caller, sendRes.change.value.value); + } + + return sendRes; + } +} diff --git a/contracts/shielded-token/src/openzeppelin/Utils.compact b/contracts/shielded-token/src/openzeppelin/Utils.compact new file mode 100644 index 00000000..d9df8ced --- /dev/null +++ b/contracts/shielded-token/src/openzeppelin/Utils.compact @@ -0,0 +1,105 @@ +// SPDX-License-Identifier: MIT + +pragma language_version >= 0.15.0; + +module Utils { + import CompactStandardLibrary; + + /** + * @description Returns whether `keyOrAddress` is the zero address. + * + * @param {keyOrAddress} - The target value to check, either a ZswapCoinPublicKey or a ContractAddress. + * @return {Boolean} - Returns true if `keyOrAddress` is zero. + */ + export pure circuit isKeyOrAddressZero(keyOrAddress: Either): Boolean { + if (keyOrAddress.is_left) { + return keyOrAddress == eitherZeroZPK(); + } else { + return keyOrAddress == eitherZeroContractAddress(); + } + } + + /** + * @description Returns a zero-filled 32-byte array. + * + * @return {Bytes<32>} - A 32-byte array filled with zeros. + */ + export pure circuit zeroBytes(): Bytes<32> { + return pad(32, ""); + } + + /** + * @description Returns a zero Zswap coin public key. + * + * @return {ZswapCoinPublicKey} - A ZswapCoinPublicKey with all bytes set to zero. + */ + export pure circuit zeroZPK(): ZswapCoinPublicKey { + return ZswapCoinPublicKey { bytes: zeroBytes() }; + } + + /** + * @description Returns the caller's Zswap coin public key. + * + * @return {ZswapCoinPublicKey} - The public key of the caller. + */ + export circuit callerZPK(): ZswapCoinPublicKey { + return own_public_key(); + } + + /** + * @description Returns the current contract's address. + * + * @return {ContractAddress} - The address of this contract. + */ + export circuit thisAddress(): ContractAddress { + return kernel.self(); + } + + /** + * @description Returns the sender's public key as the left variant of an Either type, representing + * a ZswapCoinPublicKey. + * + * @returns Either The sender’s public key as the left variant. + */ + export circuit eitherCaller(): Either { + return left(callerZPK()); + } + + /** + * @description Returns a zero Zswap coin public key as the left variant of an Either type. + * + * @return {Either} - Zero ZswapCoinPublicKey as the left variant. + */ + export pure circuit eitherZeroZPK(): Either { + return left(zeroZPK()); + } + + /** + * @description Returns a zero contract address as the right variant of an Either type. + * + * @return {Either} - Zero ContractAddress as the right variant. + */ + export pure circuit eitherZeroContractAddress(): Either { + return right(ContractAddress{ zeroBytes() }); + } + + /** + * @description Wraps a Zswap coin public key as the left variant of an Either type. + * + * @param {pk} - The Zswap coin public key to wrap. + * @return {Either} - The public key as the left variant. + */ + export circuit eitherZPK(pk: ZswapCoinPublicKey): Either { + return left(pk); + } + + /** + * @description Returns the contract’s address as the right variant of an Either type, representing + * a ContractAddress. + * + * @returns Either The contract’s address as the right variant. + */ + export circuit eitherThisAddress(): Either { + return right(thisAddress()); + } +} diff --git a/contracts/shielded-token/src/test/ShieldedFungibleToken.test.ts b/contracts/shielded-token/src/test/ShieldedFungibleToken.test.ts new file mode 100644 index 00000000..9fd3b3dd --- /dev/null +++ b/contracts/shielded-token/src/test/ShieldedFungibleToken.test.ts @@ -0,0 +1,371 @@ +import { encodeTokenType, tokenType } from '@midnight-ntwrk/compact-runtime'; +import { + NetworkId, + getZswapNetworkId, + setNetworkId, +} from '@midnight-ntwrk/midnight-js-network-id'; +import { + MidnightBech32m, + ShieldedAddress, +} from '@midnight-ntwrk/wallet-sdk-address-format'; +import type { + CoinInfo, + ContractAddress, + Either, + ZswapCoinPublicKey, +} from '@openzeppelin-midnight-apps/compact-std'; +import { beforeEach, describe, expect, it } from 'vitest'; +import { ShieldedFungibleTokenSimulator } from './ShieldedFungibleTokenSimulator'; + +const NONCE = new Uint8Array(32).fill(0x01); +const DOMAIN = new Uint8Array(32).fill(0x42); + +// Static addresses for testing +const ADMIN = + 'mn_shield-addr_test1yz95e4uu8j88t7vjvjnq8ywv9z7rg4dyvas98zhlktqtdhj600nqxqyja8d99chz8mmek4g4swsz6ldws3dlx9gzza96434w83kc8grj2umx4xe6'; +const USER = + 'mn_shield-addr_test16fcl2lpnahvlq8v79tmwwlplhecg3s08axcunwpkezzgk59kf20qxq9jwznuslc496azd0ety7f4y5t5sgw89r6wfdsgars6ufj4zf936sprk8a9'; + +setNetworkId(NetworkId.TestNet); + +// Helper function to create Either for hex addresses +const createEitherFromHex = ( + hexString: string, +): Either => { + const bech32mAddress = MidnightBech32m.parse(hexString); + const shieldedAddress = ShieldedAddress.codec.decode( + getZswapNetworkId(), + bech32mAddress, + ); + const coinPublicKeyBytes = shieldedAddress.coinPublicKey.data; + return { + is_left: true, + left: { bytes: coinPublicKeyBytes }, + right: { bytes: new Uint8Array(32) }, + }; +}; + +describe('ShieldedFungibleToken', () => { + let token: ShieldedFungibleTokenSimulator; + + const setup = () => { + token = new ShieldedFungibleTokenSimulator( + NONCE, + 'Test Token', + 'TEST', + DOMAIN, + ); + }; + + beforeEach(setup); + + describe('constructor and initialization', () => { + it('should initialize with correct token properties', () => { + expect(token.name()).toBe('Test Token'); + expect(token.symbol()).toBe('TEST'); + expect(token.decimals()).toBe(18n); + expect(token.totalSupply()).toBe(0n); + }); + + it('should have a valid contract address', () => { + expect(token.contractAddress).toBeDefined(); + expect(typeof token.contractAddress).toBe('string'); + expect(token.contractAddress.length).toBeGreaterThan(0); + }); + + it('should have a valid sender', () => { + expect(token.sender).toBeDefined(); + expect(typeof token.sender).toBe('string'); + expect(token.sender.length).toBeGreaterThan(0); + }); + }); + + describe('mint functionality', () => { + it('should initialize token type in constructor and maintain it', () => { + const recipient = createEitherFromHex(USER); + const amount = 1000n; + + // Check token type is not set before lazy mint + const initialType = token.type(); + expect(initialType).toEqual(new Uint8Array(32)); // Should be all zeros + + // Perform first mint + const coin = token.mint(recipient, amount); + + // Calculate the expected token type from domain and contract address + const publicState = token.getCurrentPublicState(); + const expectedType = encodeTokenType( + tokenType( + publicState.shieldedFungibleToken_Domain, + token.contractAddress, + ), + ); + expect(expectedType).not.toEqual(initialType); + + // Verify minted coin color matches the calculated token type + expect(coin.color).toEqual(expectedType); + expect(token.type()).toEqual(expectedType); + + // Second mint should have the same color as the expected type + const coin2 = token.mint(recipient, 500n); + expect(coin2.color).toEqual(expectedType); + + // Type should remain unchanged after subsequent mints + expect(token.type()).toEqual(expectedType); + }); + + it('should mint tokens to a recipient', () => { + const recipient = createEitherFromHex(USER); + const amount = 1000n; + + const coin = token.mint(recipient, amount); + + // Verify the minted coin + const publicState = token.getCurrentPublicState(); + const expectedType = encodeTokenType( + tokenType( + publicState.shieldedFungibleToken_Domain, + token.contractAddress, + ), + ); + expect(coin.color).toEqual(expectedType); + expect(coin.value).toEqual(amount); + + // Verify total supply increased + expect(token.totalSupply()).toEqual(amount); + }); + + it('should mint tokens to multiple recipients', () => { + const recipient1 = createEitherFromHex(ADMIN); + const recipient2 = createEitherFromHex(USER); + const amount1 = 500n; + const amount2 = 300n; + + const coin1 = token.mint(recipient1, amount1); + const coin2 = token.mint(recipient2, amount2); + + // Verify individual coins + expect(coin1.value).toBe(amount1); + expect(coin2.value).toBe(amount2); + + // Verify total supply + expect(token.totalSupply()).toBe(amount1 + amount2); + }); + + it('should handle minting zero amount', () => { + const recipient = createEitherFromHex(USER); + const amount = 0n; + + const coin = token.mint(recipient, amount); + + expect(coin.value).toBe(0n); + expect(token.totalSupply()).toBe(0n); + }); + + it('should handle minting large amounts', () => { + const recipient = createEitherFromHex(USER); + const amount = 1_000_000n; + + const coin = token.mint(recipient, amount); + + expect(coin.value).toBe(amount); + expect(token.totalSupply()).toBe(amount); + }); + + it('should mint tokens to contract address', () => { + const contractAddress: Either = { + is_left: false, + left: { bytes: new Uint8Array(32) }, + right: { bytes: new Uint8Array(32).fill(0xaa) }, + }; + const amount = 1000n; + + const coin = token.mint(contractAddress, amount); + + expect(coin.value).toBe(amount); + expect(token.totalSupply()).toBe(amount); + }); + + it('should generate unique nonces for each mint', () => { + const recipient = createEitherFromHex(USER); + + // Mint multiple coins and collect their nonces + const coin1 = token.mint(recipient, 100n); + const coin2 = token.mint(recipient, 200n); + const coin3 = token.mint(recipient, 300n); + + // Each coin should have a unique nonce + expect(coin1.nonce).not.toEqual(coin2.nonce); + expect(coin1.nonce).not.toEqual(coin3.nonce); + expect(coin2.nonce).not.toEqual(coin3.nonce); + + // Nonces should be properly defined (32 bytes each) + expect(coin1.nonce).toBeInstanceOf(Uint8Array); + expect(coin1.nonce.length).toBe(32); + expect(coin2.nonce).toBeInstanceOf(Uint8Array); + expect(coin2.nonce.length).toBe(32); + expect(coin3.nonce).toBeInstanceOf(Uint8Array); + expect(coin3.nonce.length).toBe(32); + }); + }); + + describe('burn functionality', () => { + let mintedCoin: CoinInfo; + + beforeEach(() => { + // Setup: mint some tokens for burning tests + const recipient = createEitherFromHex(USER); + mintedCoin = token.mint(recipient, 1000n); + }); + + it('should burn tokens and return change', () => { + const burnAmount = 300n; + + const result = token.burn(mintedCoin, burnAmount); + + // Verify burn result + expect(result.sent.value).toBe(burnAmount); + expect(result.change.is_some).toBe(true); + expect(result.change.value.value).toBe(700n); // 1000 - 300 + + // Verify total supply decreased + expect(token.totalSupply()).toBe(700n); + }); + + it('should burn entire coin amount', () => { + const burnAmount = 1000n; // Burn entire coin + + const result = token.burn(mintedCoin, burnAmount); + + expect(result.sent.value).toBe(burnAmount); + expect(result.change.is_some).toBe(false); + expect(token.totalSupply()).toBe(0n); + }); + + it('should handle burning zero amount', () => { + const burnAmount = 0n; + + const result = token.burn(mintedCoin, burnAmount); + + expect(result.sent.value).toBe(0n); + expect(result.change.is_some).toBe(true); + expect(result.change.value.value).toBe(1000n); // Original amount + expect(token.totalSupply()).toBe(1000n); // Unchanged + }); + + it('should fail when burning more than available', () => { + const burnAmount = 1500n; // More than available + + expect(() => { + token.burn(mintedCoin, burnAmount); + }).toThrow('ShieldedToken: insufficient token amount to burn'); + }); + + it('should fail when burning a coin with incorrect token type', () => { + // Create a coin with a different token type (wrong color) + const incorrectCoin: CoinInfo = { + color: new Uint8Array(32).fill(0xff), // Different color + nonce: mintedCoin.nonce, + value: 100n, + }; + + expect(() => { + token.burn(incorrectCoin, 50n); + }).toThrow('ShieldedToken: token not created from this contract'); + }); + }); + + describe('mint and burn integration', () => { + it('should handle mint-burn-mint cycle correctly', () => { + const recipient = createEitherFromHex(USER); + + // First mint + const coin1 = token.mint(recipient, 1000n); + expect(token.totalSupply()).toBe(1000n); + + // Burn some tokens + const burnResult = token.burn(coin1, 300n); + expect(token.totalSupply()).toBe(700n); + + // Verify burn result + expect(burnResult.sent.value).toEqual(300n); + expect(burnResult.change.is_some).toEqual(true); + expect(burnResult.change.value.value).toEqual(700n); + + // Second mint + token.mint(recipient, 500n); + expect(token.totalSupply()).toEqual(1200n); + }); + + it('should handle multiple burns from same coin', () => { + const recipient = createEitherFromHex(USER); + const coin = token.mint(recipient, 1000n); + + // First burn + const result1 = token.burn(coin, 200n); + expect(token.totalSupply()).toBe(800n); + expect(result1.sent.value).toBe(200n); + + // Second burn (using change from first burn) + const result2 = token.burn(result1.change.value, 300n); + expect(token.totalSupply()).toBe(500n); + expect(result2.sent.value).toBe(300n); + + // Third burn + const result3 = token.burn(result2.change.value, 100n); + expect(token.totalSupply()).toBe(400n); + expect(result3.sent.value).toBe(100n); + expect(result3.change.value.value).toBe(400n); + }); + }); + + describe('edge cases and error handling', () => { + it('should handle very large amounts', () => { + const recipient = createEitherFromHex(USER); + const largeAmount = 2n ** 64n - 1n; + + const coin = token.mint(recipient, largeAmount); + expect(coin.value).toBe(largeAmount); + expect(token.totalSupply()).toBe(largeAmount); + }); + + it('should handle smallest amounts', () => { + const recipient = createEitherFromHex(USER); + const smallestAmount = 1n; + + const coin = token.mint(recipient, smallestAmount); + expect(coin.value).toBe(smallestAmount); + expect(token.totalSupply()).toBe(smallestAmount); + }); + + it('should maintain state consistency', () => { + const recipient = createEitherFromHex(USER); + + // Perform multiple operations + const coin1 = token.mint(recipient, 1000n); + token.mint(recipient, 500n); + token.burn(coin1, 300n); + token.mint(recipient, 200n); + + // Verify final state + expect(token.totalSupply()).toBe(1400n); // 1000 + 500 - 300 + 200 + expect(token.name()).toBe('Test Token'); + expect(token.symbol()).toBe('TEST'); + expect(token.decimals()).toBe(18n); + }); + }); + + describe('contract state management', () => { + it('should maintain circuit context through operations', () => { + const recipient = createEitherFromHex(USER); + const initialContext = token.circuitContext; + + // Perform operation + token.mint(recipient, 1000n); + + // Context should be updated + expect(token.circuitContext).not.toBe(initialContext); + expect(token.circuitContext).toBeDefined(); + }); + }); +}); diff --git a/contracts/shielded-token/src/test/ShieldedFungibleTokenSimulator.ts b/contracts/shielded-token/src/test/ShieldedFungibleTokenSimulator.ts new file mode 100644 index 00000000..0cb1dc91 --- /dev/null +++ b/contracts/shielded-token/src/test/ShieldedFungibleTokenSimulator.ts @@ -0,0 +1,139 @@ +import { + type CircuitContext, + type ContractState, + QueryContext, + constructorContext, +} from '@midnight-ntwrk/compact-runtime'; +import { + sampleCoinPublicKey, + sampleContractAddress, +} from '@midnight-ntwrk/zswap'; +import type { + CoinInfo, + ContractAddress, + Either, + ZswapCoinPublicKey, +} from '@openzeppelin-midnight-apps/compact-std'; +import { + Contract, + type Ledger, + ledger, +} from '../artifacts/ShieldedFungibleToken/contract/index.cjs'; +import type { IContractSimulator } from '../types/test'; +import { + ShieldedFungibleTokenPrivateState, + ShieldedFungibleTokenWitnesses, +} from '../witnesses'; + +export class ShieldedFungibleTokenSimulator + implements IContractSimulator +{ + readonly contract: Contract; + readonly contractAddress: string; + readonly sender = sampleCoinPublicKey(); + circuitContext: CircuitContext; + + constructor( + nonce: Uint8Array, + name: string, + symbol: string, + domain: Uint8Array, + sender = sampleCoinPublicKey(), + ) { + this.contract = new Contract( + ShieldedFungibleTokenWitnesses(), + ); + const { + currentPrivateState, + currentContractState, + currentZswapLocalState, + } = this.contract.initialState( + constructorContext(ShieldedFungibleTokenPrivateState.generate(), sender), + nonce, + name, + symbol, + domain, + ); + this.circuitContext = { + currentPrivateState, + currentZswapLocalState, + originalState: currentContractState, + transactionContext: new QueryContext( + currentContractState.data, + sampleContractAddress(), + ), + }; + this.contractAddress = this.circuitContext.transactionContext.address; + } + + public getCurrentPublicState(): Ledger { + return ledger(this.circuitContext.transactionContext.state); + } + + public getCurrentPrivateState(): ShieldedFungibleTokenPrivateState { + return this.circuitContext.currentPrivateState; + } + + public getCurrentContractState(): ContractState { + return this.circuitContext.originalState; + } + + public name(): string { + const result = this.contract.circuits.name(this.circuitContext); + this.circuitContext = result.context; + return result.result; + } + + public symbol(): string { + const result = this.contract.circuits.symbol(this.circuitContext); + this.circuitContext = result.context; + return result.result; + } + + public decimals(): bigint { + const result = this.contract.circuits.decimals(this.circuitContext); + this.circuitContext = result.context; + return result.result; + } + + public totalSupply(): bigint { + const result = this.contract.circuits.totalSupply(this.circuitContext); + this.circuitContext = result.context; + return result.result; + } + + public type(): Uint8Array { + const result = this.contract.circuits.type(this.circuitContext); + this.circuitContext = result.context; + return result.result; + } + + public mint( + recipient: Either, + amount: bigint, + ): CoinInfo { + const result = this.contract.circuits.mint( + this.circuitContext, + recipient, + amount, + ); + this.circuitContext = result.context; + return result.result; + } + + public burn( + coin: CoinInfo, + amount: bigint, + ): { + change: { is_some: boolean; value: CoinInfo }; + sent: CoinInfo; + } { + const result = this.contract.circuits.burn( + this.circuitContext, + coin, + amount, + ); + this.circuitContext = result.context; + return result.result; + } +} diff --git a/contracts/shielded-token/src/types/index.ts b/contracts/shielded-token/src/types/index.ts new file mode 100644 index 00000000..511548ad --- /dev/null +++ b/contracts/shielded-token/src/types/index.ts @@ -0,0 +1,2 @@ +export type { IContractSimulator } from './test'; +export type { EmptyState } from './state'; diff --git a/contracts/shielded-token/src/types/state.ts b/contracts/shielded-token/src/types/state.ts new file mode 100644 index 00000000..93fe0b12 --- /dev/null +++ b/contracts/shielded-token/src/types/state.ts @@ -0,0 +1,2 @@ +// TODO: that could be shared in a unified test utils package +export type EmptyState = Record; diff --git a/contracts/shielded-token/src/types/test.ts b/contracts/shielded-token/src/types/test.ts new file mode 100644 index 00000000..a5e26e23 --- /dev/null +++ b/contracts/shielded-token/src/types/test.ts @@ -0,0 +1,33 @@ +import type { + CircuitContext, + CoinPublicKey, + ContractState, +} from '@midnight-ntwrk/compact-runtime'; + +// TODO: used in two places contracts/access and contracts/data-structure, +// Should be moved to a separate package in packages/ maybe one package for +// useful test utils and types. +/** + * Generic interface for mock contract implementations. + * @template PrivateState - The type of the contract's private state. + * @template L - The type of the contract's ledger (public state). + */ +export interface IContractSimulator { + /** The contract's deployed address. */ + readonly contractAddress: string; + + /** The admin's public key. */ + readonly sender: CoinPublicKey; + + /** The current circuit context. */ + circuitContext: CircuitContext; + + /** Retrieves the current ledger state. */ + getCurrentPublicState(): Ledger; + + /** Retrieves the current private state. */ + getCurrentPrivateState(): PrivateState; + + /** Retrieves the current contract state. */ + getCurrentContractState(): ContractState; +} diff --git a/contracts/shielded-token/src/witnesses/index.ts b/contracts/shielded-token/src/witnesses/index.ts new file mode 100644 index 00000000..8d38864c --- /dev/null +++ b/contracts/shielded-token/src/witnesses/index.ts @@ -0,0 +1,36 @@ +import type { Ledger } from '../artifacts/ShieldedFungibleToken/contract/index.cjs'; +import type { EmptyState } from '../types/state'; + +export type IShieldedFungibleTokenWitnesses<_L, _P> = Record; + +/** + * @description Represents the private state of the ShieldedFungibleToken module. + * @remarks No persistent state is needed beyond what's computed on-demand, so this is minimal. + */ +export type ShieldedFungibleTokenPrivateState = EmptyState; + +/** + * @description Utility object for managing the private state of the ShieldedFungibleToken module. + */ +export const ShieldedFungibleTokenPrivateState = { + /** + * @description Generates a new private state. + * @returns A fresh ShieldedFungibleTokenPrivateState instance (empty for now). + */ + generate: (): ShieldedFungibleTokenPrivateState => { + return {}; + }, +}; + +/** + * @description Factory function creating witness implementations for ShieldedFungibleToken module operations. + * @returns An object implementing the IShieldedFungibleTokenWitnesses interface for ShieldedFungibleTokenPrivateState. + */ +export const ShieldedFungibleTokenWitnesses = + (): IShieldedFungibleTokenWitnesses< + Ledger, + ShieldedFungibleTokenPrivateState + > => ({ + // Currently no custom witnesses are needed for ShieldedFungibleToken + // All operations are handled by the underlying ShieldedERC20 module + }); diff --git a/contracts/shielded-token/tsconfig.build.json b/contracts/shielded-token/tsconfig.build.json new file mode 100644 index 00000000..f1132509 --- /dev/null +++ b/contracts/shielded-token/tsconfig.build.json @@ -0,0 +1,5 @@ +{ + "extends": "./tsconfig.json", + "exclude": ["src/test/**/*.ts"], + "compilerOptions": {} +} diff --git a/contracts/shielded-token/tsconfig.json b/contracts/shielded-token/tsconfig.json new file mode 100644 index 00000000..2d5c53e5 --- /dev/null +++ b/contracts/shielded-token/tsconfig.json @@ -0,0 +1,21 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "ESNext", + "moduleResolution": "bundler", + "isolatedModules": true, + "noEmit": false, + "strict": true, + "skipLibCheck": true, + "esModuleInterop": true, + "allowSyntheticDefaultImports": true, + "forceConsistentCasingInFileNames": true, + "declaration": true, + "outDir": "dist", + "rootDir": "src", + "lib": ["ES2022"], + "types": ["node"] + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist", "src/artifacts"] +} diff --git a/contracts/shielded-token/vitest.config.ts b/contracts/shielded-token/vitest.config.ts new file mode 100644 index 00000000..a864d813 --- /dev/null +++ b/contracts/shielded-token/vitest.config.ts @@ -0,0 +1,10 @@ +import { defineConfig } from 'vitest/config'; + +export default defineConfig({ + test: { + globals: true, + environment: 'node', + include: ['src/**/*.test.ts'], + exclude: ['node_modules', 'dist'], + }, +}); diff --git a/contracts/structs/biome.json b/contracts/structs/biome.json index 98ebf7d2..d4a1d599 100644 --- a/contracts/structs/biome.json +++ b/contracts/structs/biome.json @@ -1,3 +1,3 @@ { - "extends": ["@midnight-dapps/biome-config"] + "extends": ["@openzeppelin-midnight-apps/biome-config"] } diff --git a/contracts/structs/package.json b/contracts/structs/package.json index a3b96ffe..60953dbd 100644 --- a/contracts/structs/package.json +++ b/contracts/structs/package.json @@ -1,5 +1,5 @@ { - "name": "@midnight-dapps/structs-contracts", + "name": "@openzeppelin-midnight-apps/structs", "version": "1.0.0-alpha.1", "private": true, "description": "Compacts contracts for Data structures i.e. Queue", @@ -24,8 +24,8 @@ "vitest": "^3.1.4" }, "dependencies": { - "@midnight-dapps/biome-config": "workspace:^", - "@midnight-dapps/compact": "workspace:^", + "@openzeppelin-midnight-apps/biome-config": "workspace:^", + "@openzeppelin-midnight-apps/compact": "workspace:^", "@midnight-ntwrk/compact-runtime": "^0.8.1", "@midnight-ntwrk/midnight-js-network-id": "^2.0.1", "@midnight-ntwrk/zswap": "^4.0.0" diff --git a/contracts/structs/src/index.ts b/contracts/structs/src/index.ts index cc7a94d8..e76dcfa6 100644 --- a/contracts/structs/src/index.ts +++ b/contracts/structs/src/index.ts @@ -1,5 +1,5 @@ /** - * @module @midnight-dapps/structs-contract + * @module @openzeppelin-midnight-apps/structs-contract * @description Main entry point for the AccessControl contract package, exporting private state utilities, witness implementations, and type definitions. */ diff --git a/package.json b/package.json index 55c7687c..87ea1d22 100644 --- a/package.json +++ b/package.json @@ -1,5 +1,5 @@ { - "name": "midnight-dapps", + "name": "midnight-apps", "version": "1.0.0", "private": true, "description": "Experimental sample projects on top of Midnight using Compact", @@ -19,7 +19,10 @@ "fmt": "turbo run fmt", "lint": "turbo run lint", "lint:fix": "turbo run lint:fix", - "types": "pnpm compact:fast && turbo run types", + "types:contracts": "turbo run types --filter './contracts/*'", + "types:apps": "turbo run types --filter './apps/*'", + "types:packages": "turbo run types --filter './packages/*'", + "types": "turbo run types", "precommit": "turbo run precommit" }, "keywords": [], diff --git a/packages/biome-config/package.json b/packages/biome-config/package.json index 4445617c..413ca92f 100644 --- a/packages/biome-config/package.json +++ b/packages/biome-config/package.json @@ -1,5 +1,5 @@ { - "name": "@midnight-dapps/biome-config", + "name": "@openzeppelin-midnight-apps/biome-config", "version": "1.0.0", "type": "module", "private": true, diff --git a/packages/compact-std/biome.json b/packages/compact-std/biome.json index 98ebf7d2..d4a1d599 100644 --- a/packages/compact-std/biome.json +++ b/packages/compact-std/biome.json @@ -1,3 +1,3 @@ { - "extends": ["@midnight-dapps/biome-config"] + "extends": ["@openzeppelin-midnight-apps/biome-config"] } diff --git a/packages/compact-std/package.json b/packages/compact-std/package.json index 142bdab1..3bfe13ba 100644 --- a/packages/compact-std/package.json +++ b/packages/compact-std/package.json @@ -1,5 +1,5 @@ { - "name": "@midnight-dapps/compact-std", + "name": "@openzeppelin-midnight-apps/compact-std", "version": "1.0.0-alpha.1", "private": true, "description": "", @@ -31,8 +31,8 @@ "vitest": "^3.1.4" }, "dependencies": { - "@midnight-dapps/biome-config": "workspace:^", - "@midnight-dapps/compact": "workspace:^", + "@openzeppelin-midnight-apps/biome-config": "workspace:^", + "@openzeppelin-midnight-apps/compact": "workspace:^", "@midnight-ntwrk/compact-runtime": "^0.8.1", "@midnight-ntwrk/midnight-js-network-id": "^2.0.1", "@midnight-ntwrk/zswap": "^4.0.0" diff --git a/packages/compact-std/src/index.test.ts b/packages/compact-std/src/index.test.ts index c326de19..933a4817 100644 --- a/packages/compact-std/src/index.test.ts +++ b/packages/compact-std/src/index.test.ts @@ -13,7 +13,7 @@ import type { ZswapCoinPublicKey, } from './index'; -describe('@midnight-dapps/compact-std', () => { +describe('@openzeppelin-midnight-apps/compact-std', () => { it('should export Maybe type correctly', () => { const maybeNumber: Maybe = { is_some: true, value: 42 }; expectTypeOf(maybeNumber).toEqualTypeOf<{ diff --git a/packages/compact-std/src/index.ts b/packages/compact-std/src/index.ts index a5ad7494..8cce1fdb 100644 --- a/packages/compact-std/src/index.ts +++ b/packages/compact-std/src/index.ts @@ -1,5 +1,5 @@ /** - * @module @midnight-dapps/compact-stdlib + * @module @openzeppelin-midnight-apps/compact-stdlib * @description Re-exports custom structs from CompactStandardLibrary for use in TypeScript code. * Excludes standard runtime types from @midnight-ntwrk/compact-runtime. */ diff --git a/packages/compact/biome.json b/packages/compact/biome.json index 98ebf7d2..d4a1d599 100644 --- a/packages/compact/biome.json +++ b/packages/compact/biome.json @@ -1,3 +1,3 @@ { - "extends": ["@midnight-dapps/biome-config"] + "extends": ["@openzeppelin-midnight-apps/biome-config"] } diff --git a/packages/compact/package.json b/packages/compact/package.json index 4c7367cd..07e3f3df 100644 --- a/packages/compact/package.json +++ b/packages/compact/package.json @@ -1,5 +1,5 @@ { - "name": "@midnight-dapps/compact", + "name": "@openzeppelin-midnight-apps/compact", "version": "1.2.0", "private": true, "keywords": ["compact", "compiler"], @@ -24,7 +24,7 @@ }, "packageManager": "pnpm@10.4.1", "devDependencies": { - "@midnight-dapps/biome-config": "workspace:^", + "@openzeppelin-midnight-apps/biome-config": "workspace:^", "@types/node": "^22.15.29", "typescript": "^5.8.3", "vitest": "^3.1.4" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 35e1736a..836a1a86 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -228,7 +228,7 @@ importers: specifier: ^3.25.67 version: 3.25.67 devDependencies: - '@midnight-dapps/biome-config': + '@openzeppelin-midnight-apps/biome-config': specifier: workspace:^ version: link:../../packages/biome-config '@types/node': @@ -252,13 +252,13 @@ importers: contracts/access: dependencies: - '@midnight-dapps/biome-config': + '@openzeppelin-midnight-apps/biome-config': specifier: workspace:^ version: link:../../packages/biome-config - '@midnight-dapps/compact': + '@openzeppelin-midnight-apps/compact': specifier: workspace:^ version: link:../../packages/compact - '@midnight-dapps/compact-std': + '@openzeppelin-midnight-apps/compact-std': specifier: workspace:^ version: link:../../packages/compact-std '@midnight-ntwrk/compact-runtime': @@ -283,10 +283,10 @@ importers: contracts/math: dependencies: - '@midnight-dapps/biome-config': + '@openzeppelin-midnight-apps/biome-config': specifier: workspace:^ version: link:../../packages/biome-config - '@midnight-dapps/compact': + '@openzeppelin-midnight-apps/compact': specifier: workspace:^ version: link:../../packages/compact '@midnight-ntwrk/compact-runtime': @@ -309,12 +309,49 @@ importers: specifier: ^3.1.4 version: 3.1.4(@types/node@22.15.29)(jiti@2.4.2)(lightningcss@1.30.1)(yaml@2.8.0) + contracts/shielded-token: + dependencies: + '@openzeppelin-midnight-apps/biome-config': + specifier: workspace:^ + version: link:../../packages/biome-config + '@openzeppelin-midnight-apps/compact': + specifier: workspace:^ + version: link:../../packages/compact + '@openzeppelin-midnight-apps/compact-std': + specifier: workspace:^ + version: link:../../packages/compact-std + '@openzeppelin-midnight-apps/math': + specifier: workspace:^ + version: link:../math + '@midnight-ntwrk/compact-runtime': + specifier: ^0.8.1 + version: 0.8.1 + '@midnight-ntwrk/midnight-js-network-id': + specifier: ^2.0.1 + version: 2.0.1 + '@midnight-ntwrk/wallet-sdk-address-format': + specifier: 2.0.0 + version: 2.0.0(@midnight-ntwrk/zswap@4.0.0) + '@midnight-ntwrk/zswap': + specifier: ^4.0.0 + version: 4.0.0 + devDependencies: + '@types/node': + specifier: ^22.15.29 + version: 22.15.29 + typescript: + specifier: ^5.8.3 + version: 5.8.3 + vitest: + specifier: ^3.1.4 + version: 3.1.4(@types/node@22.15.29)(jiti@2.4.2)(lightningcss@1.30.1)(yaml@2.8.0) + contracts/structs: dependencies: - '@midnight-dapps/biome-config': + '@openzeppelin-midnight-apps/biome-config': specifier: workspace:^ version: link:../../packages/biome-config - '@midnight-dapps/compact': + '@openzeppelin-midnight-apps/compact': specifier: workspace:^ version: link:../../packages/compact '@midnight-ntwrk/compact-runtime': @@ -351,7 +388,7 @@ importers: specifier: ^8.2.0 version: 8.2.0 devDependencies: - '@midnight-dapps/biome-config': + '@openzeppelin-midnight-apps/biome-config': specifier: workspace:^ version: link:../biome-config '@types/node': @@ -366,10 +403,10 @@ importers: packages/compact-std: dependencies: - '@midnight-dapps/biome-config': + '@openzeppelin-midnight-apps/biome-config': specifier: workspace:^ version: link:../biome-config - '@midnight-dapps/compact': + '@openzeppelin-midnight-apps/compact': specifier: workspace:^ version: link:../compact '@midnight-ntwrk/compact-runtime': @@ -903,6 +940,11 @@ packages: peerDependencies: rxjs: 7.x + '@midnight-ntwrk/wallet-sdk-address-format@2.0.0': + resolution: {integrity: sha512-S/3uIfXXUcklYocO0UPey+15uI095CFuCyJRYgxnTmkIXrgjOn8IfSHnReHDtE6JKkvdN2EXlYVt4ufRdCMuPA==} + peerDependencies: + '@midnight-ntwrk/zswap': 4.0.0 + '@midnight-ntwrk/zswap@3.0.6': resolution: {integrity: sha512-OtJxo8Y4uW4SM13wcGmWpXEqsfyiIUeLqZgMBiNmRZ6ZwMHlytunohN/cRb2cSh62lq2eEw/NdVxIFYI25y88A==} @@ -1839,6 +1881,9 @@ packages: cpu: [x64] os: [win32] + '@scure/base@1.2.6': + resolution: {integrity: sha512-g/nm5FgUa//MCj1gV09zTJTaM6KBAHqLN907YVQqf7zC49+DcO4B1so4ZX07Ef10Twr6nuqYEH9GEggFXA4Fmg==} + '@standard-schema/spec@1.0.0': resolution: {integrity: sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA==} @@ -4088,6 +4133,11 @@ snapshots: '@midnight-ntwrk/zswap': 4.0.0 rxjs: 7.8.2 + '@midnight-ntwrk/wallet-sdk-address-format@2.0.0(@midnight-ntwrk/zswap@4.0.0)': + dependencies: + '@midnight-ntwrk/zswap': 4.0.0 + '@scure/base': 1.2.6 + '@midnight-ntwrk/zswap@3.0.6': {} '@midnight-ntwrk/zswap@4.0.0': {} @@ -4979,6 +5029,8 @@ snapshots: '@rollup/rollup-win32-x64-msvc@4.41.1': optional: true + '@scure/base@1.2.6': {} + '@standard-schema/spec@1.0.0': {} '@standard-schema/utils@0.3.0': {}