diff --git a/.agent/rules/solidity_zksync.md b/.agent/rules/solidity_zksync.md
new file mode 100644
index 00000000..642f1082
--- /dev/null
+++ b/.agent/rules/solidity_zksync.md
@@ -0,0 +1,33 @@
+# Solidity & ZkSync Development Standards
+
+## Toolchain & Environment
+- **Primary Tool**: `forge` (ZkSync fork). Use for compilation, testing, and generic scripting.
+- **Secondary Tool**: `hardhat`. Use only when `forge` encounters compatibility issues (e.g., complex deployments, specific plugin needs).
+- **Network Target**: ZkSync Era (Layer 2).
+- **Solidity Version**: `^0.8.20` (or `0.8.24` if strictly supported by the zk-compiler).
+
+## Modern Solidity Best Practices
+- **Safety First**:
+ - **Checks-Effects-Interactions (CEI)** pattern must be strictly followed.
+ - When a contract requires an owner (e.g., admin-configurable parameters), prefer `Ownable2Step` over `Ownable`. Do **not** add ownership to contracts that don't need it — many contracts are fully permissionless by design.
+ - Prefer `ReentrancyGuard` for external calls where appropriate.
+- **Gas & Efficiency**:
+ - Use **Custom Errors** (`error MyError();`) instead of `require` strings.
+ - Use `mapping` over arrays for membership checks where possible.
+ - Minimize on-chain storage; use events for off-chain indexing.
+
+## Testing Standards
+- **Framework**: Foundry (Forge).
+- **Methodology**:
+ - **Unit Tests**: Comprehensive coverage for all functions.
+ - **Fuzz Testing**: Required for arithmetic and purely functional logic.
+ - **Invariant Testing**: Define invariants for stateful system properties.
+- **Naming Convention**:
+ - `test_Description`
+ - `testFuzz_Description`
+ - `test_RevertIf_Condition`
+
+## ZkSync Specifics
+- **System Contracts**: Be aware of ZkSync system contracts (e.g., `ContractDeployer`, `L2EthToken`) when interacting with low-level features.
+- **Gas Model**: Account for ZkSync's different gas metering if performing low-level optimization.
+- **Compiler Differences**: Be mindful of differences between `solc` and `zksolc` (e.g., `create2` address derivation).
diff --git a/.cspell.json b/.cspell.json
index c9909576..1a4a2a62 100644
--- a/.cspell.json
+++ b/.cspell.json
@@ -12,7 +12,8 @@
"deployments-zk",
"cache_hardhat-zk",
"zkout",
- "clk-gateway/src/validators.test.ts"
+ "clk-gateway/src/validators.test.ts",
+ "src/swarms/doc/iso3166-2"
],
"ignoreWords": [
"NODL",
@@ -60,6 +61,39 @@
"Frontends",
"testuser",
"testhandle",
- "douglasacost"
+ "douglasacost",
+ "IBEACON",
+ "AABBCCDD",
+ "SSTORE",
+ "Permissionless",
+ "Reentrancy",
+ "SFID",
+ "EXTCODECOPY",
+ "solady",
+ "SLOAD",
+ "Bitmask",
+ "mstore",
+ "MBOND",
+ "USCA",
+ "USNY",
+ "usca",
+ "UUPS",
+ "reinitializer",
+ "Reinitializer",
+ "Initializable",
+ "bitshift",
+ "timelock",
+ "iface",
+ "pkill",
+ "Blockscout",
+ "REINIT",
+ "reinit",
+ "EDDYSTONE",
+ "Eddystone",
+ "Estimote",
+ "backgrounded",
+ "reconstructable",
+ "Württemberg",
+ "delegatecall"
]
}
diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md
new file mode 100644
index 00000000..37ae67c2
--- /dev/null
+++ b/.github/copilot-instructions.md
@@ -0,0 +1,50 @@
+# Solidity & ZkSync Development Standards
+
+## Toolchain & Environment
+
+- **Primary Tool**: `forge` (ZkSync fork). Use for compilation, testing, and generic scripting.
+- **Secondary Tool**: `hardhat`. Use only when `forge` encounters compatibility issues (e.g., complex deployments, specific plugin needs).
+- **Network Target**: ZkSync Era (Layer 2).
+- **Solidity Version**: `^0.8.20` (or `0.8.24` if strictly supported by the zk-compiler).
+
+## Modern Solidity Best Practices
+
+- **Safety First**:
+ - **Checks-Effects-Interactions (CEI)** pattern must be strictly followed.
+ - Use `Ownable2Step` over `Ownable` for privileged access.
+ - Prefer `ReentrancyGuard` for external calls where appropriate.
+- **Gas & Efficiency**:
+ - Use **Custom Errors** (`error MyError();`) instead of `require` strings.
+ - Use `mapping` over arrays for membership checks where possible.
+ - Minimize on-chain storage; use events for off-chain indexing.
+
+## Testing Standards
+
+- **Framework**: Foundry (Forge).
+- **Methodology**:
+ - **Unit Tests**: Comprehensive coverage for all functions.
+ - **Fuzz Testing**: Required for arithmetic and purely functional logic.
+ - **Invariant Testing**: Define invariants for stateful system properties.
+- **Naming Convention**:
+ - `test_Description`
+ - `testFuzz_Description`
+ - `test_RevertIf_Condition`
+
+## ZkSync Specifics
+
+- **System Contracts**: Be aware of ZkSync system contracts (e.g., `ContractDeployer`, `L2EthToken`) when interacting with low-level features.
+- **Gas Model**: Account for ZkSync's different gas metering if performing low-level optimization.
+- **Compiler Differences**: Be mindful of differences between `solc` and `zksolc` (e.g., `create2` address derivation).
+
+## L1-Only Contracts (No --zksync flag)
+
+The following contracts use opcodes/patterns incompatible with ZkSync Era and must be built/tested **without** the `--zksync` flag:
+
+- **SwarmRegistryL1**: Uses `SSTORE2` (relies on `EXTCODECOPY` which is unsupported on ZkSync).
+
+For these contracts, use:
+
+```bash
+forge build --match-path src/swarms/SwarmRegistryL1.sol
+forge test --match-path test/SwarmRegistryL1.t.sol
+```
diff --git a/.github/workflows/checks.yml b/.github/workflows/checks.yml
index 64ac1373..620436fe 100644
--- a/.github/workflows/checks.yml
+++ b/.github/workflows/checks.yml
@@ -34,4 +34,107 @@ jobs:
run: yarn lint
- name: Run tests
- run: forge test --zksync
+ run: forge test
+
+ Coverage:
+ runs-on: ubuntu-latest
+ if: github.event_name == 'pull_request' && github.base_ref == 'main'
+ container:
+ image: ghcr.io/nodlecode/devcontainer-rollup
+ options: --user root
+
+ steps:
+ - name: Checkout repository
+ uses: actions/checkout@v4
+ with:
+ submodules: recursive
+
+ - name: Install dependencies
+ run: yarn
+
+ - name: Run coverage
+ run: forge coverage --match-path "test/{Swarm*,ServiceProvider,FleetIdentity}*.t.sol" --ir-minimum --report lcov --report-file coverage.lcov
+
+ - name: Upload coverage report
+ uses: actions/upload-artifact@v4
+ with:
+ name: coverage-report
+ path: coverage.lcov
+ retention-days: 30
+
+ - name: Install lcov
+ run: apt-get update && apt-get install -y lcov
+
+ - name: Report coverage to PR
+ uses: zgosalvez/github-actions-report-lcov@v4
+ with:
+ coverage-files: coverage.lcov
+ github-token: ${{ secrets.GITHUB_TOKEN }}
+ update-comment: true
+ working-directory: ./
+
+ - name: Check line coverage threshold
+ run: |
+ # Extract line coverage from lcov report for src/swarms/ contracts only
+ # Parse lcov format: find swarm file sections and sum their LF/LH values
+ LINES_FOUND=$(awk '
+ /^SF:.*src\/swarms\// { in_swarm = 1 }
+ /^end_of_record/ { in_swarm = 0 }
+ in_swarm && /^LF:/ { sum += substr($0, 4) }
+ END { print sum+0 }
+ ' coverage.lcov)
+
+ LINES_HIT=$(awk '
+ /^SF:.*src\/swarms\// { in_swarm = 1 }
+ /^end_of_record/ { in_swarm = 0 }
+ in_swarm && /^LH:/ { sum += substr($0, 4) }
+ END { print sum+0 }
+ ' coverage.lcov)
+
+ if [ "$LINES_FOUND" -eq 0 ]; then
+ echo "Error: No lines found in coverage report for src/swarms/"
+ exit 1
+ fi
+
+ COVERAGE=$(awk "BEGIN {printf \"%.2f\", ($LINES_HIT / $LINES_FOUND) * 100}")
+ echo "Swarms line coverage: $COVERAGE% ($LINES_HIT / $LINES_FOUND lines)"
+
+ # Check if coverage is below 95%
+ THRESHOLD=95
+ if awk "BEGIN {exit !($COVERAGE < $THRESHOLD)}"; then
+ echo "Error: Line coverage ($COVERAGE%) is below the required threshold ($THRESHOLD%)"
+ exit 1
+ fi
+
+ echo "Coverage check passed: $COVERAGE% >= $THRESHOLD%"
+
+ Specification-PDF:
+ runs-on: ubuntu-latest
+ if: github.event_name == 'pull_request' && github.base_ref == 'main'
+
+ steps:
+ - name: Checkout repository
+ uses: actions/checkout@v4
+
+ - name: Setup Node.js
+ uses: actions/setup-node@v4
+ with:
+ node-version: 20
+
+ - name: Install npm dependencies
+ working-directory: src/swarms/doc/spec
+ run: npm install --no-save @mermaid-js/mermaid-cli md-to-pdf
+
+ - name: Install Puppeteer browser
+ run: npx puppeteer browsers install chrome
+
+ - name: Build specification PDF
+ working-directory: src/swarms/doc/spec
+ run: bash build.sh
+
+ - name: Upload specification PDF
+ uses: actions/upload-artifact@v4
+ with:
+ name: swarm-specification
+ path: src/swarms/doc/spec/swarm-specification.pdf
+ retention-days: 30
diff --git a/.gitmodules b/.gitmodules
index 9540dda6..9717df7c 100644
--- a/.gitmodules
+++ b/.gitmodules
@@ -10,3 +10,9 @@
[submodule "lib/era-contracts"]
path = lib/era-contracts
url = https://github.com/matter-labs/era-contracts
+[submodule "lib/solady"]
+ path = lib/solady
+ url = https://github.com/vectorized/solady
+[submodule "lib/openzeppelin-contracts-upgradeable"]
+ path = lib/openzeppelin-contracts-upgradeable
+ url = https://github.com/OpenZeppelin/openzeppelin-contracts-upgradeable
diff --git a/foundry.lock b/foundry.lock
new file mode 100644
index 00000000..599c1c01
--- /dev/null
+++ b/foundry.lock
@@ -0,0 +1,26 @@
+{
+ "lib/forge-std": {
+ "rev": "1eea5bae12ae557d589f9f0f0edae2faa47cb262"
+ },
+ "lib/zksync-storage-proofs": {
+ "rev": "4b20401ce44c1ec966a29d893694f65db885304b"
+ },
+ "lib/solady": {
+ "tag": {
+ "name": "v0.1.26",
+ "rev": "acd959aa4bd04720d640bf4e6a5c71037510cc4b"
+ }
+ },
+ "lib/era-contracts": {
+ "rev": "84d5e3716f645909e8144c7d50af9dd6dd9ded62"
+ },
+ "lib/openzeppelin-contracts-upgradeable": {
+ "tag": {
+ "name": "v5.6.1",
+ "rev": "7bf4727aacdbfaa0f36cbd664654d0c9e1dc52bf"
+ }
+ },
+ "lib/openzeppelin-contracts": {
+ "rev": "e4f70216d759d8e6a64144a9e1f7bbeed78e7079"
+ }
+}
\ No newline at end of file
diff --git a/foundry.toml b/foundry.toml
index b05ff1b9..9b7806cf 100644
--- a/foundry.toml
+++ b/foundry.toml
@@ -8,3 +8,21 @@ solc = "0.8.26"
# necessary as some of the zksync contracts are big
via_ir = true
+optimizer = true
+optimizer_runs = 200
+
+[lint]
+# Exclude ERC20 transfer warning - false positive for ERC721.transferFrom in tests
+exclude_lints = ["erc20-unchecked-transfer"]
+
+[profile.zksync]
+src = "src"
+out = "zkout"
+libs = ["lib"]
+solc = "0.8.26"
+via_ir = true
+optimizer = true
+optimizer_runs = 200
+# Exclude L1-only contracts that use SSTORE2/EXTCODECOPY
+ignored_error_codes = []
+ignored_warnings_from = []
diff --git a/hardhat-deploy/DeploySwarmUpgradeable.ts b/hardhat-deploy/DeploySwarmUpgradeable.ts
new file mode 100644
index 00000000..d764105c
--- /dev/null
+++ b/hardhat-deploy/DeploySwarmUpgradeable.ts
@@ -0,0 +1,209 @@
+import { Provider, Wallet } from "zksync-ethers";
+import { Deployer } from "@matterlabs/hardhat-zksync";
+import { ethers } from "ethers";
+import { HardhatRuntimeEnvironment } from "hardhat/types";
+import "@matterlabs/hardhat-zksync-node/dist/type-extensions";
+import "@matterlabs/hardhat-zksync-verify/dist/src/type-extensions";
+import * as dotenv from "dotenv";
+import { deployContract } from "./utils";
+
+dotenv.config({ path: ".env-test" });
+
+/**
+ * Deploys the upgradeable swarm contracts on ZkSync Era
+ *
+ * Required environment variables:
+ * - DEPLOYER_PRIVATE_KEY: Private key for deployment
+ * - BOND_TOKEN: Address of the ERC20 bond token
+ * - BASE_BOND: Base bond amount in wei
+ * - OWNER: (optional) Owner address, defaults to deployer
+ */
+module.exports = async function (hre: HardhatRuntimeEnvironment) {
+ const bondToken = process.env.BOND_TOKEN!;
+ const baseBond = BigInt(process.env.BASE_BOND!);
+ const countryMultiplier = BigInt(process.env.COUNTRY_MULTIPLIER || "0");
+
+ const rpcUrl = hre.network.config.url!;
+ const provider = new Provider(rpcUrl);
+ const wallet = new Wallet(process.env.DEPLOYER_PRIVATE_KEY!, provider);
+ const deployer = new Deployer(hre, wallet);
+
+ const owner = process.env.OWNER || wallet.address;
+
+ console.log("=== Deploying Upgradeable Swarm Contracts on ZkSync ===");
+ console.log("Bond Token:", bondToken);
+ console.log("Base Bond:", baseBond.toString());
+ console.log("Country Multiplier:", countryMultiplier.toString());
+ console.log("Owner:", owner);
+ console.log("Deployer:", wallet.address);
+ console.log("");
+
+ // 1. Deploy ServiceProviderUpgradeable Implementation
+ console.log("1. Deploying ServiceProviderUpgradeable...");
+ const serviceProviderArtifact = await deployer.loadArtifact(
+ "ServiceProviderUpgradeable",
+ );
+ const serviceProviderImpl = await deployer.deploy(
+ serviceProviderArtifact,
+ [],
+ );
+ await serviceProviderImpl.waitForDeployment();
+ const serviceProviderImplAddr = await serviceProviderImpl.getAddress();
+ console.log(" Implementation:", serviceProviderImplAddr);
+
+ // Deploy ServiceProvider Proxy
+ const serviceProviderInitData =
+ serviceProviderImpl.interface.encodeFunctionData("initialize", [owner]);
+ const proxyArtifact = await deployer.loadArtifact("ERC1967Proxy");
+ const serviceProviderProxy = await deployer.deploy(proxyArtifact, [
+ serviceProviderImplAddr,
+ serviceProviderInitData,
+ ]);
+ await serviceProviderProxy.waitForDeployment();
+ const serviceProviderProxyAddr = await serviceProviderProxy.getAddress();
+ console.log(" Proxy:", serviceProviderProxyAddr);
+ console.log("");
+
+ // 2. Deploy FleetIdentityUpgradeable Implementation
+ console.log("2. Deploying FleetIdentityUpgradeable...");
+ const fleetIdentityArtifact = await deployer.loadArtifact(
+ "FleetIdentityUpgradeable",
+ );
+ const fleetIdentityImpl = await deployer.deploy(fleetIdentityArtifact, []);
+ await fleetIdentityImpl.waitForDeployment();
+ const fleetIdentityImplAddr = await fleetIdentityImpl.getAddress();
+ console.log(" Implementation:", fleetIdentityImplAddr);
+
+ // Deploy FleetIdentity Proxy
+ const fleetIdentityInitData = fleetIdentityImpl.interface.encodeFunctionData(
+ "initialize",
+ [owner, bondToken, baseBond.toString(), countryMultiplier.toString()],
+ );
+ const fleetIdentityProxy = await deployer.deploy(proxyArtifact, [
+ fleetIdentityImplAddr,
+ fleetIdentityInitData,
+ ]);
+ await fleetIdentityProxy.waitForDeployment();
+ const fleetIdentityProxyAddr = await fleetIdentityProxy.getAddress();
+ console.log(" Proxy:", fleetIdentityProxyAddr);
+ console.log("");
+
+ // 3. Deploy SwarmRegistryUniversalUpgradeable Implementation
+ console.log("3. Deploying SwarmRegistryUniversalUpgradeable...");
+ const swarmRegistryArtifact = await deployer.loadArtifact(
+ "SwarmRegistryUniversalUpgradeable",
+ );
+ const swarmRegistryImpl = await deployer.deploy(swarmRegistryArtifact, []);
+ await swarmRegistryImpl.waitForDeployment();
+ const swarmRegistryImplAddr = await swarmRegistryImpl.getAddress();
+ console.log(" Implementation:", swarmRegistryImplAddr);
+
+ // Deploy SwarmRegistry Proxy
+ const swarmRegistryInitData = swarmRegistryImpl.interface.encodeFunctionData(
+ "initialize",
+ [fleetIdentityProxyAddr, serviceProviderProxyAddr, owner],
+ );
+ const swarmRegistryProxy = await deployer.deploy(proxyArtifact, [
+ swarmRegistryImplAddr,
+ swarmRegistryInitData,
+ ]);
+ await swarmRegistryProxy.waitForDeployment();
+ const swarmRegistryProxyAddr = await swarmRegistryProxy.getAddress();
+ console.log(" Proxy:", swarmRegistryProxyAddr);
+ console.log("");
+
+ // Summary
+ console.log("=== Deployment Complete ===");
+ console.log("ServiceProvider Implementation:", serviceProviderImplAddr);
+ console.log("ServiceProvider Proxy:", serviceProviderProxyAddr);
+ console.log("FleetIdentity Implementation:", fleetIdentityImplAddr);
+ console.log("FleetIdentity Proxy:", fleetIdentityProxyAddr);
+ console.log("SwarmRegistry Implementation:", swarmRegistryImplAddr);
+ console.log("SwarmRegistry Proxy:", swarmRegistryProxyAddr);
+ console.log("");
+
+ // Verify contracts
+ console.log("=== Verifying Contracts ===");
+ try {
+ console.log("Verifying ServiceProviderUpgradeable Implementation...");
+ await hre.run("verify:verify", {
+ address: serviceProviderImplAddr,
+ contract:
+ "src/swarms/ServiceProviderUpgradeable.sol:ServiceProviderUpgradeable",
+ constructorArguments: [],
+ });
+ } catch (e: any) {
+ console.log("Verification failed or already verified:", e.message);
+ }
+
+ try {
+ console.log("Verifying FleetIdentityUpgradeable Implementation...");
+ await hre.run("verify:verify", {
+ address: fleetIdentityImplAddr,
+ contract:
+ "src/swarms/FleetIdentityUpgradeable.sol:FleetIdentityUpgradeable",
+ constructorArguments: [],
+ });
+ } catch (e: any) {
+ console.log("Verification failed or already verified:", e.message);
+ }
+
+ try {
+ console.log(
+ "Verifying SwarmRegistryUniversalUpgradeable Implementation...",
+ );
+ await hre.run("verify:verify", {
+ address: swarmRegistryImplAddr,
+ contract:
+ "src/swarms/SwarmRegistryUniversalUpgradeable.sol:SwarmRegistryUniversalUpgradeable",
+ constructorArguments: [],
+ });
+ } catch (e: any) {
+ console.log("Verification failed or already verified:", e.message);
+ }
+
+ try {
+ console.log("Verifying ServiceProvider Proxy...");
+ await hre.run("verify:verify", {
+ address: serviceProviderProxyAddr,
+ contract:
+ "lib/openzeppelin-contracts/contracts/proxy/ERC1967/ERC1967Proxy.sol:ERC1967Proxy",
+ constructorArguments: [serviceProviderImplAddr, serviceProviderInitData],
+ });
+ } catch (e: any) {
+ console.log("Verification failed or already verified:", e.message);
+ }
+
+ try {
+ console.log("Verifying FleetIdentity Proxy...");
+ await hre.run("verify:verify", {
+ address: fleetIdentityProxyAddr,
+ contract:
+ "lib/openzeppelin-contracts/contracts/proxy/ERC1967/ERC1967Proxy.sol:ERC1967Proxy",
+ constructorArguments: [fleetIdentityImplAddr, fleetIdentityInitData],
+ });
+ } catch (e: any) {
+ console.log("Verification failed or already verified:", e.message);
+ }
+
+ try {
+ console.log("Verifying SwarmRegistry Proxy...");
+ await hre.run("verify:verify", {
+ address: swarmRegistryProxyAddr,
+ contract:
+ "lib/openzeppelin-contracts/contracts/proxy/ERC1967/ERC1967Proxy.sol:ERC1967Proxy",
+ constructorArguments: [swarmRegistryImplAddr, swarmRegistryInitData],
+ });
+ } catch (e: any) {
+ console.log("Verification failed or already verified:", e.message);
+ }
+
+ console.log("");
+ console.log("=== Add these to .env-test: ===");
+ console.log(`SERVICE_PROVIDER_PROXY=${serviceProviderProxyAddr}`);
+ console.log(`SERVICE_PROVIDER_IMPL=${serviceProviderImplAddr}`);
+ console.log(`FLEET_IDENTITY_PROXY=${fleetIdentityProxyAddr}`);
+ console.log(`FLEET_IDENTITY_IMPL=${fleetIdentityImplAddr}`);
+ console.log(`SWARM_REGISTRY_PROXY=${swarmRegistryProxyAddr}`);
+ console.log(`SWARM_REGISTRY_IMPL=${swarmRegistryImplAddr}`);
+};
diff --git a/hardhat.config.ts b/hardhat.config.ts
index 52905bda..866b817b 100644
--- a/hardhat.config.ts
+++ b/hardhat.config.ts
@@ -43,6 +43,10 @@ const config: HardhatUserConfig = {
settings: {
// find all available options in the official documentation
// https://era.zksync.io/docs/tools/hardhat/hardhat-zksync-solc.html#configuration
+ optimizer: {
+ enabled: true,
+ runs: 200,
+ },
},
},
solidity: {
diff --git a/lib/openzeppelin-contracts-upgradeable b/lib/openzeppelin-contracts-upgradeable
new file mode 160000
index 00000000..7bf4727a
--- /dev/null
+++ b/lib/openzeppelin-contracts-upgradeable
@@ -0,0 +1 @@
+Subproject commit 7bf4727aacdbfaa0f36cbd664654d0c9e1dc52bf
diff --git a/lib/solady b/lib/solady
new file mode 160000
index 00000000..acd959aa
--- /dev/null
+++ b/lib/solady
@@ -0,0 +1 @@
+Subproject commit acd959aa4bd04720d640bf4e6a5c71037510cc4b
diff --git a/ops/deploy_swarm_contracts_l1.sh b/ops/deploy_swarm_contracts_l1.sh
new file mode 100755
index 00000000..94840954
--- /dev/null
+++ b/ops/deploy_swarm_contracts_l1.sh
@@ -0,0 +1,403 @@
+#!/bin/bash
+# =============================================================================
+# deploy_swarm_contracts_l1.sh
+#
+# Automated deployment script for Swarm contracts (ServiceProvider, FleetIdentity,
+# SwarmRegistryL1) to Ethereum L1.
+#
+# OVERVIEW:
+# ---------
+# This script deploys the upgradeable Swarm contracts to Ethereum L1 using Foundry.
+# Unlike ZkSync deployments, we use Forge here because:
+# 1. Forge is stable and well-tested for L1 deployments
+# 2. SwarmRegistryL1 uses SSTORE2 (EXTCODECOPY) which works on L1
+# 3. No need for special compiler or Hardhat plugins
+#
+# L1 vs ZKSYNC:
+# -------------
+# - L1: Uses SwarmRegistryL1Upgradeable with SSTORE2 for storage proofs
+# - ZkSync: Uses SwarmRegistryUniversalUpgradeable (no SSTORE2)
+#
+# SSTORE2 relies on EXTCODECOPY opcode which is NOT supported on ZkSync Era.
+# For ZkSync deployments, use deploy_swarm_contracts_zksync.sh instead.
+#
+# CONTRACT ARCHITECTURE:
+# ----------------------
+# - ServiceProviderUpgradeable: Registry for service providers (no dependencies)
+# - FleetIdentityUpgradeable: NFT-based fleet identity with bonding (depends on ERC20 bond token)
+# - SwarmRegistryL1Upgradeable: Main registry with SSTORE2 (depends on ServiceProvider & FleetIdentity)
+#
+# USAGE:
+# ------
+# # For testnet (dry run - simulation only):
+# ./ops/deploy_swarm_contracts_l1.sh testnet
+#
+# # For testnet (actual deployment):
+# ./ops/deploy_swarm_contracts_l1.sh testnet --broadcast
+#
+# # For mainnet (actual deployment):
+# ./ops/deploy_swarm_contracts_l1.sh mainnet --broadcast
+#
+# REQUIRED ENVIRONMENT VARIABLES:
+# -------------------------------
+# The script loads from .env-test (testnet) or .env-prod (mainnet):
+# - DEPLOYER_PRIVATE_KEY: Private key with ETH for gas
+# - BOND_TOKEN: Address of the ERC20 bond token (NODL)
+# - BASE_BOND: Bond amount in wei (e.g., 100000000000000000000 for 100 NODL)
+# - OWNER: (optional) Contract owner address, defaults to deployer
+# - L1_RPC: RPC URL for L1 (Sepolia or Mainnet)
+# - ETHERSCAN_API_KEY: For contract verification
+#
+# =============================================================================
+
+set -e # Exit on any error
+
+# =============================================================================
+# Configuration
+# =============================================================================
+
+SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
+PROJECT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
+
+# Parse arguments
+NETWORK="${1:-testnet}"
+BROADCAST="${2:-}"
+
+# Network-specific configuration
+case "$NETWORK" in
+ testnet)
+ ENV_FILE=".env-test"
+ CHAIN_ID="11155111" # Sepolia
+ EXPLORER_URL="https://sepolia.etherscan.io"
+ VERIFIER_URL="https://api-sepolia.etherscan.io/api"
+ ;;
+ mainnet)
+ ENV_FILE=".env-prod"
+ CHAIN_ID="1" # Ethereum Mainnet
+ EXPLORER_URL="https://etherscan.io"
+ VERIFIER_URL="https://api.etherscan.io/api"
+ ;;
+ *)
+ echo "Error: Unknown network '$NETWORK'. Use 'testnet' or 'mainnet'."
+ exit 1
+ ;;
+esac
+
+# Colors for output
+RED='\033[0;31m'
+GREEN='\033[0;32m'
+YELLOW='\033[1;33m'
+BLUE='\033[0;34m'
+NC='\033[0m' # No Color
+
+# =============================================================================
+# Helper Functions
+# =============================================================================
+
+log_info() {
+ echo -e "${BLUE}[INFO]${NC} $1"
+}
+
+log_success() {
+ echo -e "${GREEN}[SUCCESS]${NC} $1"
+}
+
+log_warning() {
+ echo -e "${YELLOW}[WARNING]${NC} $1"
+}
+
+log_error() {
+ echo -e "${RED}[ERROR]${NC} $1"
+}
+
+# =============================================================================
+# Pre-flight Checks
+# =============================================================================
+
+preflight_checks() {
+ log_info "Running pre-flight checks..."
+
+ cd "$PROJECT_ROOT"
+
+ # Check required tools
+ if ! command -v forge &> /dev/null; then
+ log_error "forge not found. Please install foundry."
+ exit 1
+ fi
+
+ if ! command -v cast &> /dev/null; then
+ log_error "cast not found. Please install foundry."
+ exit 1
+ fi
+
+ # Check env file exists
+ if [ ! -f "$ENV_FILE" ]; then
+ log_error "Environment file '$ENV_FILE' not found."
+ exit 1
+ fi
+
+ # Load environment variables
+ set -a
+ source "$ENV_FILE"
+ set +a
+
+ # Validate required variables
+ if [ -z "$DEPLOYER_PRIVATE_KEY" ]; then
+ log_error "DEPLOYER_PRIVATE_KEY not set in $ENV_FILE"
+ exit 1
+ fi
+
+ if [ -z "$BOND_TOKEN" ] && [ -z "$NODL" ]; then
+ log_error "BOND_TOKEN or NODL not set in $ENV_FILE"
+ exit 1
+ fi
+
+ if [ -z "$L1_RPC" ]; then
+ log_error "L1_RPC not set in $ENV_FILE"
+ exit 1
+ fi
+
+ # Set defaults
+ export BOND_TOKEN="${BOND_TOKEN:-$NODL}"
+ export BASE_BOND="${BASE_BOND:-100000000000000000000}" # 100 NODL default
+
+ log_success "Pre-flight checks passed"
+}
+
+# =============================================================================
+# Step 1: Build Contracts
+# =============================================================================
+#
+# Standard Forge build - no special flags needed for L1.
+# All contracts including SwarmRegistryL1 (with SSTORE2) compile normally.
+#
+# =============================================================================
+
+build_contracts() {
+ log_info "Building contracts with Forge..."
+
+ forge build
+
+ log_success "Build complete"
+}
+
+# =============================================================================
+# Step 2: Deploy Contracts
+# =============================================================================
+#
+# Uses Forge script to deploy all contracts in the correct order:
+# 1. ServiceProviderUpgradeable
+# 2. FleetIdentityUpgradeable
+# 3. SwarmRegistryL1Upgradeable
+#
+# =============================================================================
+
+deploy_contracts() {
+ log_info "Deploying contracts to L1 ($NETWORK)..."
+
+ FORGE_ARGS=(
+ "script"
+ "script/DeploySwarmUpgradeable.s.sol:DeploySwarmUpgradeableL1"
+ "--rpc-url" "$L1_RPC"
+ "--chain-id" "$CHAIN_ID"
+ )
+
+ if [ "$BROADCAST" = "--broadcast" ]; then
+ FORGE_ARGS+=("--broadcast")
+
+ # Add verification if API key is available
+ if [ -n "$ETHERSCAN_API_KEY" ]; then
+ FORGE_ARGS+=("--verify" "--verifier-url" "$VERIFIER_URL")
+ fi
+ else
+ log_warning "DRY RUN MODE - Add '--broadcast' to actually deploy"
+ log_info "Would deploy with:"
+ log_info " BOND_TOKEN: $BOND_TOKEN"
+ log_info " BASE_BOND: $BASE_BOND"
+ log_info " OWNER: ${OWNER:-deployer}"
+ log_info " RPC: $L1_RPC"
+ fi
+
+ # Run the deployment
+ forge "${FORGE_ARGS[@]}" 2>&1 | tee /tmp/deploy-output-$$.txt
+
+ if [ "$BROADCAST" = "--broadcast" ]; then
+ # Extract deployed addresses from output
+ SERVICE_PROVIDER_PROXY=$(grep "ServiceProvider Proxy:" /tmp/deploy-output-$$.txt | awk '{print $NF}')
+ SERVICE_PROVIDER_IMPL=$(grep "ServiceProvider Implementation:" /tmp/deploy-output-$$.txt | grep -v "Proxy" | awk '{print $NF}')
+ FLEET_IDENTITY_PROXY=$(grep "FleetIdentity Proxy:" /tmp/deploy-output-$$.txt | awk '{print $NF}')
+ FLEET_IDENTITY_IMPL=$(grep "FleetIdentity Implementation:" /tmp/deploy-output-$$.txt | grep -v "Proxy" | awk '{print $NF}')
+ SWARM_REGISTRY_PROXY=$(grep "SwarmRegistry Proxy:" /tmp/deploy-output-$$.txt | awk '{print $NF}')
+ SWARM_REGISTRY_IMPL=$(grep "SwarmRegistry Implementation:" /tmp/deploy-output-$$.txt | grep -v "Proxy" | awk '{print $NF}')
+
+ # Validate we got addresses
+ if [ -z "$SERVICE_PROVIDER_PROXY" ] || [ -z "$FLEET_IDENTITY_PROXY" ] || [ -z "$SWARM_REGISTRY_PROXY" ]; then
+ log_warning "Could not extract all addresses from output"
+ log_info "Check /tmp/deploy-output-$$.txt for details"
+ else
+ log_success "Deployment complete!"
+ fi
+ fi
+
+ rm -f /tmp/deploy-output-$$.txt
+}
+
+# =============================================================================
+# Step 3: Verify Deployment
+# =============================================================================
+
+verify_deployment() {
+ if [ "$BROADCAST" != "--broadcast" ]; then
+ return 0
+ fi
+
+ if [ -z "$SERVICE_PROVIDER_PROXY" ]; then
+ log_warning "Skipping verification - addresses not extracted"
+ return 0
+ fi
+
+ log_info "Verifying deployment..."
+
+ # Test ServiceProvider
+ log_info "Testing ServiceProvider proxy..."
+ SP_OWNER=$(cast call "$SERVICE_PROVIDER_PROXY" "owner()(address)" --rpc-url "$L1_RPC")
+ log_success "ServiceProvider owner: $SP_OWNER"
+
+ # Test FleetIdentity
+ log_info "Testing FleetIdentity proxy..."
+ FI_OWNER=$(cast call "$FLEET_IDENTITY_PROXY" "owner()(address)" --rpc-url "$L1_RPC")
+ FI_BOND=$(cast call "$FLEET_IDENTITY_PROXY" "BASE_BOND()(uint256)" --rpc-url "$L1_RPC")
+ FI_TOKEN=$(cast call "$FLEET_IDENTITY_PROXY" "BOND_TOKEN()(address)" --rpc-url "$L1_RPC")
+ log_success "FleetIdentity owner: $FI_OWNER"
+ log_success "FleetIdentity BASE_BOND: $FI_BOND"
+ log_success "FleetIdentity BOND_TOKEN: $FI_TOKEN"
+
+ # Test SwarmRegistry
+ log_info "Testing SwarmRegistry proxy..."
+ SR_OWNER=$(cast call "$SWARM_REGISTRY_PROXY" "owner()(address)" --rpc-url "$L1_RPC")
+ log_success "SwarmRegistry owner: $SR_OWNER"
+
+ log_success "All contracts verified successfully!"
+}
+
+# =============================================================================
+# Step 4: Update Environment File
+# =============================================================================
+
+update_env_file() {
+ if [ "$BROADCAST" != "--broadcast" ]; then
+ return 0
+ fi
+
+ if [ -z "$SERVICE_PROVIDER_PROXY" ]; then
+ return 0
+ fi
+
+ log_info "Updating $ENV_FILE with deployed addresses..."
+
+ TIMESTAMP=$(date +%Y-%m-%d)
+
+ # Check if swarm contracts section already exists
+ if grep -q "SERVICE_PROVIDER_L1_PROXY" "$ENV_FILE"; then
+ log_warning "L1 Swarm contract addresses already exist in $ENV_FILE"
+ log_warning "Please manually update the addresses:"
+ echo ""
+ echo "SERVICE_PROVIDER_L1_PROXY=$SERVICE_PROVIDER_PROXY"
+ echo "SERVICE_PROVIDER_L1_IMPL=$SERVICE_PROVIDER_IMPL"
+ echo "FLEET_IDENTITY_L1_PROXY=$FLEET_IDENTITY_PROXY"
+ echo "FLEET_IDENTITY_L1_IMPL=$FLEET_IDENTITY_IMPL"
+ echo "SWARM_REGISTRY_L1_PROXY=$SWARM_REGISTRY_PROXY"
+ echo "SWARM_REGISTRY_L1_IMPL=$SWARM_REGISTRY_IMPL"
+ return 0
+ fi
+
+ # Append new addresses
+ cat >> "$ENV_FILE" << EOF
+
+# Swarm Contracts L1 (Ethereum - deployed $TIMESTAMP)
+SERVICE_PROVIDER_L1_PROXY=$SERVICE_PROVIDER_PROXY
+SERVICE_PROVIDER_L1_IMPL=$SERVICE_PROVIDER_IMPL
+FLEET_IDENTITY_L1_PROXY=$FLEET_IDENTITY_PROXY
+FLEET_IDENTITY_L1_IMPL=$FLEET_IDENTITY_IMPL
+SWARM_REGISTRY_L1_PROXY=$SWARM_REGISTRY_PROXY
+SWARM_REGISTRY_L1_IMPL=$SWARM_REGISTRY_IMPL
+EOF
+
+ log_success "Environment file updated"
+}
+
+# =============================================================================
+# Step 5: Print Summary
+# =============================================================================
+
+print_summary() {
+ echo ""
+ echo "=============================================="
+ echo " DEPLOYMENT SUMMARY (L1)"
+ echo "=============================================="
+ echo ""
+ echo "Network: $NETWORK (Chain ID: $CHAIN_ID)"
+ echo "Explorer: $EXPLORER_URL"
+ echo ""
+
+ if [ "$BROADCAST" != "--broadcast" ]; then
+ echo "Mode: DRY RUN (no contracts deployed)"
+ echo ""
+ echo "To deploy for real, run:"
+ echo " $0 $NETWORK --broadcast"
+ return 0
+ fi
+
+ if [ -z "$SERVICE_PROVIDER_PROXY" ]; then
+ echo "Check deployment output for contract addresses."
+ return 0
+ fi
+
+ echo "Deployed Contracts:"
+ echo "-------------------"
+ echo ""
+ echo "ServiceProvider:"
+ echo " Proxy: $SERVICE_PROVIDER_PROXY"
+ echo " Implementation: $SERVICE_PROVIDER_IMPL"
+ echo " Explorer: $EXPLORER_URL/address/$SERVICE_PROVIDER_PROXY"
+ echo ""
+ echo "FleetIdentity:"
+ echo " Proxy: $FLEET_IDENTITY_PROXY"
+ echo " Implementation: $FLEET_IDENTITY_IMPL"
+ echo " Explorer: $EXPLORER_URL/address/$FLEET_IDENTITY_PROXY"
+ echo ""
+ echo "SwarmRegistryL1:"
+ echo " Proxy: $SWARM_REGISTRY_PROXY"
+ echo " Implementation: $SWARM_REGISTRY_IMPL"
+ echo " Explorer: $EXPLORER_URL/address/$SWARM_REGISTRY_PROXY"
+ echo ""
+ echo "Configuration:"
+ echo " Owner: ${OWNER:-deployer}"
+ echo " Bond Token: $BOND_TOKEN"
+ echo " Base Bond: $BASE_BOND wei"
+ echo ""
+ echo "=============================================="
+}
+
+# =============================================================================
+# Main Execution
+# =============================================================================
+
+main() {
+ echo ""
+ echo "=============================================="
+ echo " Ethereum L1 Swarm Contracts Deployment"
+ echo "=============================================="
+ echo ""
+
+ cd "$PROJECT_ROOT"
+
+ preflight_checks
+ build_contracts
+ deploy_contracts
+ verify_deployment
+ update_env_file
+ print_summary
+}
+
+main "$@"
diff --git a/ops/deploy_swarm_contracts_zksync.sh b/ops/deploy_swarm_contracts_zksync.sh
new file mode 100755
index 00000000..360b018b
--- /dev/null
+++ b/ops/deploy_swarm_contracts_zksync.sh
@@ -0,0 +1,534 @@
+#!/bin/bash
+# =============================================================================
+# deploy_swarm_contracts_zksync.sh
+#
+# Automated deployment script for Swarm contracts (ServiceProvider, FleetIdentity,
+# SwarmRegistryUniversal) to ZkSync Era.
+#
+# OVERVIEW:
+# ---------
+# This script deploys the upgradeable Swarm contracts to ZkSync Era using Foundry.
+# We use Forge with --zksync flag which compiles and deploys using zksolc.
+#
+# Requirements:
+# - foundry-zksync fork must be installed (foundryup-zksync)
+#
+# WHY WE TEMPORARILY MOVE L1 CONTRACTS:
+# -------------------------------------
+# SwarmRegistryL1 and related contracts use SSTORE2 library which relies on
+# EXTCODECOPY opcode - this opcode is NOT supported on ZkSync Era's zkEVM.
+# Even if we don't deploy these contracts, the ZkSync compiler fails when it
+# encounters them in the codebase. By temporarily moving them out, we allow
+# the ZkSync compiler to build only compatible contracts.
+#
+# CONTRACT ARCHITECTURE:
+# ----------------------
+# - ServiceProviderUpgradeable: Registry for service providers (no dependencies)
+# - FleetIdentityUpgradeable: NFT-based fleet identity with bonding (depends on ERC20 bond token)
+# - SwarmRegistryUniversalUpgradeable: Main registry (depends on ServiceProvider & FleetIdentity)
+#
+# Each contract is deployed as:
+# 1. Implementation contract (the actual logic)
+# 2. ERC1967Proxy pointing to the implementation (the user-facing address)
+#
+# USAGE:
+# ------
+# # For testnet (dry run - simulation only):
+# ./ops/deploy_swarm_contracts_zksync.sh testnet
+#
+# # For testnet (actual deployment):
+# ./ops/deploy_swarm_contracts_zksync.sh testnet --broadcast
+#
+# # For mainnet (actual deployment):
+# ./ops/deploy_swarm_contracts_zksync.sh mainnet --broadcast
+#
+# REQUIRED ENVIRONMENT VARIABLES:
+# -------------------------------
+# The script loads from .env-test (testnet) or .env-prod (mainnet):
+# - DEPLOYER_PRIVATE_KEY: Private key with ETH for gas
+# - NODL: Address of the NODL token (used as bond token)
+# - BASE_BOND: Bond amount in wei (e.g., 100000000000000000000 for 100 NODL)
+# - OWNER: (optional) Contract owner address, defaults to deployer
+#
+# =============================================================================
+
+set -e # Exit on any error
+
+# =============================================================================
+# Configuration
+# =============================================================================
+
+SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
+PROJECT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
+
+# Parse arguments
+NETWORK="${1:-testnet}"
+BROADCAST="${2:-}"
+
+# Network-specific configuration
+case "$NETWORK" in
+ testnet)
+ ENV_FILE=".env-test"
+ HARDHAT_NETWORK="zkSyncSepoliaTestnet"
+ EXPLORER_URL="https://sepolia.explorer.zksync.io"
+ ;;
+ mainnet)
+ ENV_FILE=".env-prod"
+ HARDHAT_NETWORK="zkSyncMainnet"
+ EXPLORER_URL="https://explorer.zksync.io"
+ ;;
+ *)
+ echo "Error: Unknown network '$NETWORK'. Use 'testnet' or 'mainnet'."
+ exit 1
+ ;;
+esac
+
+# Colors for output
+RED='\033[0;31m'
+GREEN='\033[0;32m'
+YELLOW='\033[1;33m'
+BLUE='\033[0;34m'
+NC='\033[0m' # No Color
+
+# =============================================================================
+# Helper Functions
+# =============================================================================
+
+log_info() {
+ echo -e "${BLUE}[INFO]${NC} $1"
+}
+
+log_success() {
+ echo -e "${GREEN}[SUCCESS]${NC} $1"
+}
+
+log_warning() {
+ echo -e "${YELLOW}[WARNING]${NC} $1"
+}
+
+log_error() {
+ echo -e "${RED}[ERROR]${NC} $1"
+}
+
+# =============================================================================
+# Pre-flight Checks
+# =============================================================================
+
+preflight_checks() {
+ log_info "Running pre-flight checks..."
+
+ cd "$PROJECT_ROOT"
+
+ # Check required tools
+ if ! command -v forge &> /dev/null; then
+ log_error "forge not found. Please install foundry-zksync."
+ exit 1
+ fi
+
+ # Check forge has zksync support
+ if ! forge --version | grep -q "zksync"; then
+ log_error "forge does not have ZkSync support. Install with: foundryup-zksync"
+ exit 1
+ fi
+
+ if ! command -v cast &> /dev/null; then
+ log_error "cast not found. Please install foundry."
+ exit 1
+ fi
+
+ # Check env file exists
+ if [ ! -f "$ENV_FILE" ]; then
+ log_error "Environment file '$ENV_FILE' not found."
+ exit 1
+ fi
+
+ # Load environment variables
+ set -a
+ source "$ENV_FILE"
+ set +a
+
+ # Validate required variables
+ if [ -z "$DEPLOYER_PRIVATE_KEY" ]; then
+ log_error "DEPLOYER_PRIVATE_KEY not set in $ENV_FILE"
+ exit 1
+ fi
+
+ if [ -z "$NODL" ]; then
+ log_error "NODL (bond token address) not set in $ENV_FILE"
+ exit 1
+ fi
+
+ # Ensure DEPLOYER_PRIVATE_KEY has 0x prefix (required by forge vm.envUint)
+ if [[ "$DEPLOYER_PRIVATE_KEY" != 0x* ]]; then
+ export DEPLOYER_PRIVATE_KEY="0x${DEPLOYER_PRIVATE_KEY}"
+ fi
+
+ # Set defaults
+ export BOND_TOKEN="${BOND_TOKEN:-$NODL}"
+ export BASE_BOND="${BASE_BOND:-100000000000000000000}" # 100 NODL default
+
+ log_success "Pre-flight checks passed"
+}
+
+# =============================================================================
+# Step 1: Temporarily Move L1-Incompatible Contracts
+# =============================================================================
+#
+# Why: ZkSync's zksolc compiler fails on contracts using SSTORE2/EXTCODECOPY.
+# These opcodes are not supported on zkEVM. Even if we skip these contracts
+# during deployment, the compiler still tries to process them.
+#
+# What we move:
+# - SwarmRegistryL1Upgradeable.sol (uses SSTORE2 for storage proofs)
+# - SwarmRegistryL1.t.sol (tests for L1 registry)
+# - upgrade-demo/ (contains L1 upgrade tests)
+# - DeploySwarmUpgradeable.s.sol (Forge script that imports L1 contracts)
+# - UpgradeSwarm.s.sol (imports SwarmRegistryL1)
+#
+# =============================================================================
+
+L1_BACKUP_DIR="/tmp/rollup-l1-backup-zksync-deploy"
+
+move_l1_contracts() {
+ log_info "Moving L1-incompatible contracts to temporary location..."
+
+ # First, restore any files from a previous failed run
+ if [ -d "$L1_BACKUP_DIR" ]; then
+ log_warning "Found previous backup, restoring first..."
+ restore_l1_contracts 2>/dev/null || true
+ fi
+
+ mkdir -p "$L1_BACKUP_DIR"
+
+ # Move contracts that use SSTORE2/EXTCODECOPY
+ [ -f "src/swarms/SwarmRegistryL1Upgradeable.sol" ] && \
+ mv "src/swarms/SwarmRegistryL1Upgradeable.sol" "$L1_BACKUP_DIR/"
+
+ # Move L1-only test files
+ [ -f "test/SwarmRegistryL1.t.sol" ] && \
+ mv "test/SwarmRegistryL1.t.sol" "$L1_BACKUP_DIR/"
+
+ [ -d "test/upgrade-demo" ] && \
+ mv "test/upgrade-demo" "$L1_BACKUP_DIR/"
+
+ # Move Forge deploy script (it imports L1 contracts)
+ [ -f "script/DeploySwarmUpgradeable.s.sol" ] && \
+ mv "script/DeploySwarmUpgradeable.s.sol" "$L1_BACKUP_DIR/"
+
+ # Move upgrade script (imports SwarmRegistryL1)
+ [ -f "script/UpgradeSwarm.s.sol" ] && \
+ mv "script/UpgradeSwarm.s.sol" "$L1_BACKUP_DIR/"
+
+ log_success "L1 contracts moved to $L1_BACKUP_DIR"
+}
+
+restore_l1_contracts() {
+ log_info "Restoring L1 contracts from backup..."
+
+ [ -f "$L1_BACKUP_DIR/SwarmRegistryL1Upgradeable.sol" ] && \
+ mv "$L1_BACKUP_DIR/SwarmRegistryL1Upgradeable.sol" "src/swarms/"
+
+ [ -f "$L1_BACKUP_DIR/SwarmRegistryL1.t.sol" ] && \
+ mv "$L1_BACKUP_DIR/SwarmRegistryL1.t.sol" "test/"
+
+ [ -d "$L1_BACKUP_DIR/upgrade-demo" ] && \
+ mv "$L1_BACKUP_DIR/upgrade-demo" "test/"
+
+ [ -f "$L1_BACKUP_DIR/DeploySwarmUpgradeable.s.sol" ] && \
+ mv "$L1_BACKUP_DIR/DeploySwarmUpgradeable.s.sol" "script/"
+
+ [ -f "$L1_BACKUP_DIR/UpgradeSwarm.s.sol" ] && \
+ mv "$L1_BACKUP_DIR/UpgradeSwarm.s.sol" "script/"
+
+ rm -rf "$L1_BACKUP_DIR"
+
+ log_success "L1 contracts restored"
+}
+
+# Ensure cleanup on exit
+trap restore_l1_contracts EXIT
+
+# =============================================================================
+# Step 2: Build Contracts with Forge ZkSync
+# =============================================================================
+#
+# Why Forge with --zksync:
+# - Uses foundry-zksync fork with zksolc compiler
+# - Consistent tooling with L1 deployments
+# - Faster builds than Hardhat
+#
+# Note: We skip test files as they may exceed ZkSync bytecode limits
+#
+# =============================================================================
+
+compile_contracts() {
+ log_info "Compiling contracts with Forge for ZkSync..."
+
+ # Build with zksolc compiler, skip test files (may exceed bytecode limits)
+ forge build --zksync --skip test
+
+ log_success "Compilation complete"
+}
+
+# =============================================================================
+# Step 3: Deploy Contracts
+# =============================================================================
+#
+# Deployment order matters due to dependencies:
+# 1. ServiceProviderUpgradeable - No dependencies
+# 2. FleetIdentityUpgradeable - Requires bond token address
+# 3. SwarmRegistryUniversalUpgradeable - Requires both ServiceProvider & FleetIdentity
+#
+# Each deployment creates:
+# - Implementation contract (the logic)
+# - ERC1967Proxy (the user-facing address that delegates to implementation)
+#
+# The proxy pattern allows future upgrades without changing the contract address.
+#
+# =============================================================================
+
+deploy_contracts() {
+ log_info "Deploying contracts to ZkSync ($NETWORK)..."
+
+ # Get RPC URL based on network
+ if [ "$NETWORK" = "mainnet" ]; then
+ RPC_URL="${L2_RPC:-https://mainnet.era.zksync.io}"
+ CHAIN_ID="324"
+ else
+ RPC_URL="${L2_RPC:-https://rpc.ankr.com/zksync_era_sepolia}"
+ CHAIN_ID="300"
+ fi
+
+ FORGE_ARGS=(
+ "script"
+ "script/DeploySwarmUpgradeableZkSync.s.sol:DeploySwarmUpgradeableZkSync"
+ "--rpc-url" "$RPC_URL"
+ "--chain-id" "$CHAIN_ID"
+ "--zksync"
+ )
+
+ if [ "$BROADCAST" = "--broadcast" ]; then
+ FORGE_ARGS+=("--broadcast" "--slow")
+
+ # Add ZkSync-specific verification
+ if [ -n "$L2_VERIFIER_URL" ]; then
+ FORGE_ARGS+=("--verify" "--verifier" "zksync" "--verifier-url" "$L2_VERIFIER_URL")
+ fi
+ else
+ log_warning "DRY RUN MODE - Add '--broadcast' to actually deploy"
+ log_info "Would deploy with:"
+ log_info " BOND_TOKEN: $BOND_TOKEN"
+ log_info " BASE_BOND: $BASE_BOND"
+ log_info " OWNER: ${OWNER:-deployer}"
+ log_info " RPC: $RPC_URL"
+ return 0
+ fi
+
+ DEPLOY_LOG="/tmp/deploy-output-$$.txt"
+
+ # Run the deployment
+ forge "${FORGE_ARGS[@]}" 2>&1 | tee "$DEPLOY_LOG"
+
+ if [ "$BROADCAST" = "--broadcast" ]; then
+ # Extract deployed addresses from output
+ # The Solidity script outputs lines like: "ServiceProvider Proxy: 0x..."
+ SERVICE_PROVIDER_PROXY=$(grep -o 'ServiceProvider Proxy: 0x[0-9a-fA-F]*' "$DEPLOY_LOG" | grep -o '0x[0-9a-fA-F]*')
+ SERVICE_PROVIDER_IMPL=$(grep -o 'ServiceProvider Implementation: 0x[0-9a-fA-F]*' "$DEPLOY_LOG" | grep -o '0x[0-9a-fA-F]*')
+ FLEET_IDENTITY_PROXY=$(grep -o 'FleetIdentity Proxy: 0x[0-9a-fA-F]*' "$DEPLOY_LOG" | grep -o '0x[0-9a-fA-F]*')
+ FLEET_IDENTITY_IMPL=$(grep -o 'FleetIdentity Implementation: 0x[0-9a-fA-F]*' "$DEPLOY_LOG" | grep -o '0x[0-9a-fA-F]*')
+ SWARM_REGISTRY_PROXY=$(grep -o 'SwarmRegistry Proxy: 0x[0-9a-fA-F]*' "$DEPLOY_LOG" | grep -o '0x[0-9a-fA-F]*')
+ SWARM_REGISTRY_IMPL=$(grep -o 'SwarmRegistry Implementation: 0x[0-9a-fA-F]*' "$DEPLOY_LOG" | grep -o '0x[0-9a-fA-F]*')
+
+ # Validate we got addresses
+ if [ -z "$SERVICE_PROVIDER_PROXY" ] || [ -z "$FLEET_IDENTITY_PROXY" ] || [ -z "$SWARM_REGISTRY_PROXY" ]; then
+ log_error "Could not extract all addresses from output"
+ log_info "Full output saved to: $DEPLOY_LOG"
+ cat "$DEPLOY_LOG"
+ exit 1
+ else
+ log_success "Deployment complete!"
+ fi
+ fi
+
+ rm -f "$DEPLOY_LOG"
+}
+
+# =============================================================================
+# Step 4: Verify Deployment
+# =============================================================================
+#
+# We verify the deployment by calling view functions on each proxy:
+# - owner() - Confirms the proxy is initialized and returns expected owner
+# - For FleetIdentity: BASE_BOND() and BOND_TOKEN() confirm initialization params
+#
+# This ensures:
+# 1. Proxies are correctly pointing to implementations
+# 2. Initialize functions were called successfully
+# 3. Constructor/initializer parameters are correct
+#
+# =============================================================================
+
+verify_deployment() {
+ if [ "$BROADCAST" != "--broadcast" ]; then
+ return 0
+ fi
+
+ log_info "Verifying deployment..."
+
+ # Get RPC URL based on network
+ if [ "$NETWORK" = "mainnet" ]; then
+ RPC_URL="${L2_RPC:-https://mainnet.era.zksync.io}"
+ else
+ RPC_URL="${L2_RPC:-https://rpc.ankr.com/zksync_era_sepolia}"
+ fi
+
+ # Test ServiceProvider
+ log_info "Testing ServiceProvider proxy..."
+ SP_OWNER=$(cast call "$SERVICE_PROVIDER_PROXY" "owner()(address)" --rpc-url "$RPC_URL")
+ log_success "ServiceProvider owner: $SP_OWNER"
+
+ # Test FleetIdentity
+ log_info "Testing FleetIdentity proxy..."
+ FI_OWNER=$(cast call "$FLEET_IDENTITY_PROXY" "owner()(address)" --rpc-url "$RPC_URL")
+ FI_BOND=$(cast call "$FLEET_IDENTITY_PROXY" "BASE_BOND()(uint256)" --rpc-url "$RPC_URL")
+ FI_TOKEN=$(cast call "$FLEET_IDENTITY_PROXY" "BOND_TOKEN()(address)" --rpc-url "$RPC_URL")
+ log_success "FleetIdentity owner: $FI_OWNER"
+ log_success "FleetIdentity BASE_BOND: $FI_BOND"
+ log_success "FleetIdentity BOND_TOKEN: $FI_TOKEN"
+
+ # Test SwarmRegistry
+ log_info "Testing SwarmRegistry proxy..."
+ SR_OWNER=$(cast call "$SWARM_REGISTRY_PROXY" "owner()(address)" --rpc-url "$RPC_URL")
+ log_success "SwarmRegistry owner: $SR_OWNER"
+
+ log_success "All contracts verified successfully!"
+}
+
+# =============================================================================
+# Step 5: Update Environment File
+# =============================================================================
+#
+# We append the deployed contract addresses to the environment file.
+# This allows future scripts to reference these contracts.
+#
+# Format follows existing conventions in the env file.
+#
+# =============================================================================
+
+update_env_file() {
+ if [ "$BROADCAST" != "--broadcast" ]; then
+ return 0
+ fi
+
+ log_info "Updating $ENV_FILE with deployed addresses..."
+
+ TIMESTAMP=$(date +%Y-%m-%d)
+
+ # Check if swarm contracts section already exists
+ if grep -q "SERVICE_PROVIDER_PROXY" "$ENV_FILE"; then
+ log_info "Updating existing swarm contract addresses in $ENV_FILE..."
+
+ # Remove old swarm contracts block (comment + 7 lines of variables)
+ sed -i.bak '/^# Swarm Contracts/,/^$/d' "$ENV_FILE"
+ # Also remove any straggling individual lines that weren't in a block
+ sed -i.bak '/^SERVICE_PROVIDER_PROXY=/d' "$ENV_FILE"
+ sed -i.bak '/^SERVICE_PROVIDER_IMPL=/d' "$ENV_FILE"
+ sed -i.bak '/^FLEET_IDENTITY_PROXY=/d' "$ENV_FILE"
+ sed -i.bak '/^FLEET_IDENTITY_IMPL=/d' "$ENV_FILE"
+ sed -i.bak '/^SWARM_REGISTRY_PROXY=/d' "$ENV_FILE"
+ sed -i.bak '/^SWARM_REGISTRY_IMPL=/d' "$ENV_FILE"
+ sed -i.bak '/^BASE_BOND=/d' "$ENV_FILE"
+ # Clean up trailing blank lines
+ sed -i.bak -e :a -e '/^\n*$/{$d;N;ba' -e '}' "$ENV_FILE"
+ rm -f "${ENV_FILE}.bak"
+ fi
+
+ # Append new addresses
+ cat >> "$ENV_FILE" << EOF
+
+# Swarm Contracts (ZkSync Era - deployed $TIMESTAMP)
+SERVICE_PROVIDER_PROXY=$SERVICE_PROVIDER_PROXY
+SERVICE_PROVIDER_IMPL=$SERVICE_PROVIDER_IMPL
+FLEET_IDENTITY_PROXY=$FLEET_IDENTITY_PROXY
+FLEET_IDENTITY_IMPL=$FLEET_IDENTITY_IMPL
+SWARM_REGISTRY_PROXY=$SWARM_REGISTRY_PROXY
+SWARM_REGISTRY_IMPL=$SWARM_REGISTRY_IMPL
+BASE_BOND=$BASE_BOND
+EOF
+
+ log_success "Environment file updated"
+}
+
+# =============================================================================
+# Step 6: Print Summary
+# =============================================================================
+
+print_summary() {
+ echo ""
+ echo "=============================================="
+ echo " DEPLOYMENT SUMMARY"
+ echo "=============================================="
+ echo ""
+ echo "Network: $NETWORK ($HARDHAT_NETWORK)"
+ echo "Explorer: $EXPLORER_URL"
+ echo ""
+
+ if [ "$BROADCAST" != "--broadcast" ]; then
+ echo "Mode: DRY RUN (no contracts deployed)"
+ echo ""
+ echo "To deploy for real, run:"
+ echo " $0 $NETWORK --broadcast"
+ return 0
+ fi
+
+ echo "Deployed Contracts:"
+ echo "-------------------"
+ echo ""
+ echo "ServiceProvider:"
+ echo " Proxy: $SERVICE_PROVIDER_PROXY"
+ echo " Implementation: $SERVICE_PROVIDER_IMPL"
+ echo " Explorer: $EXPLORER_URL/address/$SERVICE_PROVIDER_PROXY"
+ echo ""
+ echo "FleetIdentity:"
+ echo " Proxy: $FLEET_IDENTITY_PROXY"
+ echo " Implementation: $FLEET_IDENTITY_IMPL"
+ echo " Explorer: $EXPLORER_URL/address/$FLEET_IDENTITY_PROXY"
+ echo ""
+ echo "SwarmRegistry:"
+ echo " Proxy: $SWARM_REGISTRY_PROXY"
+ echo " Implementation: $SWARM_REGISTRY_IMPL"
+ echo " Explorer: $EXPLORER_URL/address/$SWARM_REGISTRY_PROXY"
+ echo ""
+ echo "Configuration:"
+ echo " Owner: ${OWNER:-deployer}"
+ echo " Bond Token: $BOND_TOKEN"
+ echo " Base Bond: $BASE_BOND wei"
+ echo ""
+ echo "=============================================="
+}
+
+# =============================================================================
+# Main Execution
+# =============================================================================
+
+main() {
+ echo ""
+ echo "=============================================="
+ echo " ZkSync Swarm Contracts Deployment"
+ echo "=============================================="
+ echo ""
+
+ cd "$PROJECT_ROOT"
+
+ preflight_checks
+ move_l1_contracts
+ compile_contracts
+ deploy_contracts
+ verify_deployment
+ update_env_file
+ print_summary
+
+ # Note: restore_l1_contracts is called automatically via trap on EXIT
+}
+
+main "$@"
diff --git a/package.json b/package.json
index 4e02a715..b379f7ef 100644
--- a/package.json
+++ b/package.json
@@ -10,7 +10,8 @@
"clean": "hardhat clean",
"lint": "solhint 'src/**/*.sol'",
"spellcheck": "cspell --config .cspell.json",
- "register-names": "ts-node utils/batch-register.ts"
+ "register-names": "ts-node utils/batch-register.ts",
+ "postinstall": "node -e \"var f='yarn.lock',s=require('fs');if(s.existsSync(f)){var c=s.readFileSync(f,'utf8'),n=c.replace(/git[+]ssh:\\/\\/git@github[.]com/g,'git+https://github.com');if(c!==n)s.writeFileSync(f,n)}\""
},
"packageManager": "yarn@1.22.19",
"engines": {
diff --git a/remappings.txt b/remappings.txt
index 1e950773..5d942fdf 100644
--- a/remappings.txt
+++ b/remappings.txt
@@ -1 +1,3 @@
-@openzeppelin=lib/openzeppelin-contracts/
\ No newline at end of file
+@openzeppelin/contracts/=lib/openzeppelin-contracts/contracts/
+@openzeppelin/contracts-upgradeable/=lib/openzeppelin-contracts-upgradeable/contracts/
+solady/=lib/solady/src/
\ No newline at end of file
diff --git a/script/DeploySwarmUpgradeable.s.sol b/script/DeploySwarmUpgradeable.s.sol
new file mode 100644
index 00000000..3a9b080b
--- /dev/null
+++ b/script/DeploySwarmUpgradeable.s.sol
@@ -0,0 +1,110 @@
+// SPDX-License-Identifier: BSD-3-Clause-Clear
+
+pragma solidity ^0.8.24;
+
+import {Script, console} from "forge-std/Script.sol";
+import {ERC1967Proxy} from "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol";
+
+import {ServiceProviderUpgradeable} from "../src/swarms/ServiceProviderUpgradeable.sol";
+import {FleetIdentityUpgradeable} from "../src/swarms/FleetIdentityUpgradeable.sol";
+import {SwarmRegistryL1Upgradeable} from "../src/swarms/SwarmRegistryL1Upgradeable.sol";
+
+/**
+ * @title DeploySwarmUpgradeableL1
+ * @notice Deployment script for the upgradeable swarm contracts on Ethereum L1.
+ * @dev This script deploys SwarmRegistryL1 which uses SSTORE2 for storage proofs.
+ * SSTORE2 relies on EXTCODECOPY which is NOT supported on ZkSync Era.
+ * For ZkSync deployments, use DeploySwarmUpgradeableZkSync.s.sol instead.
+ *
+ * Deploy order matters due to dependencies:
+ * 1. ServiceProviderUpgradeable (no dependencies)
+ * 2. FleetIdentityUpgradeable (depends on bond token)
+ * 3. SwarmRegistryL1Upgradeable (depends on 1 & 2)
+ *
+ * Usage:
+ * # Dry run (simulation)
+ * forge script script/DeploySwarmUpgradeable.s.sol --rpc-url $L1_RPC
+ *
+ * # Deploy with broadcast
+ * forge script script/DeploySwarmUpgradeable.s.sol --rpc-url $L1_RPC --broadcast --verify
+ *
+ * Environment Variables:
+ * - DEPLOYER_PRIVATE_KEY: Private key for deployment
+ * - BOND_TOKEN: Address of the ERC20 bond token
+ * - BASE_BOND: Base bond amount in wei
+ * - OWNER: Owner address for upgrade authorization (defaults to deployer)
+ */
+contract DeploySwarmUpgradeableL1 is Script {
+ // Deployment artifacts
+ address public serviceProviderProxy;
+ address public serviceProviderImpl;
+ address public fleetIdentityProxy;
+ address public fleetIdentityImpl;
+ address public swarmRegistryProxy;
+ address public swarmRegistryImpl;
+
+ function run() external {
+ // Load environment variables
+ uint256 deployerPrivateKey = vm.envUint("DEPLOYER_PRIVATE_KEY");
+ address bondToken = vm.envAddress("BOND_TOKEN");
+ uint256 baseBond = vm.envUint("BASE_BOND");
+ uint256 countryMultiplier = vm.envOr("COUNTRY_MULTIPLIER", uint256(0)); // 0 means use the default
+ address owner = vm.envOr("OWNER", vm.addr(deployerPrivateKey));
+
+ console.log("=== Deploying Upgradeable Swarm Contracts (L1) ===");
+ console.log("Bond Token:", bondToken);
+ console.log("Base Bond:", baseBond);
+ console.log("Owner:", owner);
+ console.log("Registry Type: L1 (SSTORE2)");
+ console.log("");
+
+ vm.startBroadcast(deployerPrivateKey);
+
+ // 1. Deploy ServiceProviderUpgradeable
+ console.log("1. Deploying ServiceProviderUpgradeable...");
+ serviceProviderImpl = address(new ServiceProviderUpgradeable());
+ console.log(" Implementation:", serviceProviderImpl);
+
+ bytes memory serviceProviderInitData =
+ abi.encodeWithSelector(ServiceProviderUpgradeable.initialize.selector, owner);
+ serviceProviderProxy = address(new ERC1967Proxy(serviceProviderImpl, serviceProviderInitData));
+ console.log(" Proxy:", serviceProviderProxy);
+ console.log("");
+
+ // 2. Deploy FleetIdentityUpgradeable
+ console.log("2. Deploying FleetIdentityUpgradeable...");
+ fleetIdentityImpl = address(new FleetIdentityUpgradeable());
+ console.log(" Implementation:", fleetIdentityImpl);
+
+ bytes memory fleetIdentityInitData =
+ abi.encodeWithSelector(FleetIdentityUpgradeable.initialize.selector, owner, bondToken, baseBond, countryMultiplier);
+ fleetIdentityProxy = address(new ERC1967Proxy(fleetIdentityImpl, fleetIdentityInitData));
+ console.log(" Proxy:", fleetIdentityProxy);
+ console.log("");
+
+ // 3. Deploy SwarmRegistryL1Upgradeable
+ console.log("3. Deploying SwarmRegistryL1Upgradeable...");
+ swarmRegistryImpl = address(new SwarmRegistryL1Upgradeable());
+ console.log(" Implementation:", swarmRegistryImpl);
+
+ bytes memory swarmRegistryInitData = abi.encodeWithSelector(
+ SwarmRegistryL1Upgradeable.initialize.selector, fleetIdentityProxy, serviceProviderProxy, owner
+ );
+ swarmRegistryProxy = address(new ERC1967Proxy(swarmRegistryImpl, swarmRegistryInitData));
+ console.log(" Proxy:", swarmRegistryProxy);
+
+ vm.stopBroadcast();
+
+ // Summary - format matches shell script expectations for address parsing
+ console.log("");
+ console.log("=== Deployment Complete ===");
+ console.log("ServiceProvider Implementation:", serviceProviderImpl);
+ console.log("ServiceProvider Proxy:", serviceProviderProxy);
+ console.log("FleetIdentity Implementation:", fleetIdentityImpl);
+ console.log("FleetIdentity Proxy:", fleetIdentityProxy);
+ console.log("SwarmRegistry Implementation:", swarmRegistryImpl);
+ console.log("SwarmRegistry Proxy:", swarmRegistryProxy);
+ console.log("");
+ console.log("Save these proxy addresses for future upgrades!");
+ }
+}
diff --git a/script/DeploySwarmUpgradeableZkSync.s.sol b/script/DeploySwarmUpgradeableZkSync.s.sol
new file mode 100644
index 00000000..cdb590fa
--- /dev/null
+++ b/script/DeploySwarmUpgradeableZkSync.s.sol
@@ -0,0 +1,98 @@
+// SPDX-License-Identifier: BSD-3-Clause-Clear
+
+pragma solidity ^0.8.24;
+
+import {Script, console} from "forge-std/Script.sol";
+import {ERC1967Proxy} from "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol";
+
+import {ServiceProviderUpgradeable} from "../src/swarms/ServiceProviderUpgradeable.sol";
+import {FleetIdentityUpgradeable} from "../src/swarms/FleetIdentityUpgradeable.sol";
+import {SwarmRegistryUniversalUpgradeable} from "../src/swarms/SwarmRegistryUniversalUpgradeable.sol";
+
+/**
+ * @title DeploySwarmUpgradeableZkSync
+ * @notice Deployment script for the upgradeable swarm contracts on ZkSync Era.
+ * @dev This script excludes SwarmRegistryL1 which uses SSTORE2 (incompatible with ZkSync).
+ *
+ * Usage:
+ * forge script script/DeploySwarmUpgradeableZkSync.s.sol --rpc-url $L2_RPC --broadcast --verify --zksync
+ *
+ * Environment Variables:
+ * - DEPLOYER_PRIVATE_KEY: Private key for deployment
+ * - BOND_TOKEN: Address of the ERC20 bond token
+ * - BASE_BOND: Base bond amount in wei
+ * - OWNER: Owner address for upgrade authorization (defaults to deployer)
+ */
+contract DeploySwarmUpgradeableZkSync is Script {
+ // Deployment artifacts
+ address public serviceProviderProxy;
+ address public serviceProviderImpl;
+ address public fleetIdentityProxy;
+ address public fleetIdentityImpl;
+ address public swarmRegistryProxy;
+ address public swarmRegistryImpl;
+
+ function run() external {
+ // Load environment variables
+ uint256 deployerPrivateKey = vm.envUint("DEPLOYER_PRIVATE_KEY");
+ address bondToken = vm.envAddress("BOND_TOKEN");
+ uint256 baseBond = vm.envUint("BASE_BOND");
+ uint256 countryMultiplier = vm.envOr("COUNTRY_MULTIPLIER", uint256(0)); // 0 means use the default
+ address owner = vm.envOr("OWNER", vm.addr(deployerPrivateKey));
+
+ console.log("=== Deploying Upgradeable Swarm Contracts on ZkSync ===");
+ console.log("Bond Token:", bondToken);
+ console.log("Base Bond:", baseBond);
+ console.log("Owner:", owner);
+ console.log("");
+
+ vm.startBroadcast(deployerPrivateKey);
+
+ // 1. Deploy ServiceProviderUpgradeable
+ console.log("1. Deploying ServiceProviderUpgradeable...");
+ serviceProviderImpl = address(new ServiceProviderUpgradeable());
+ console.log(" Implementation:", serviceProviderImpl);
+
+ bytes memory serviceProviderInitData =
+ abi.encodeWithSelector(ServiceProviderUpgradeable.initialize.selector, owner);
+ serviceProviderProxy = address(new ERC1967Proxy(serviceProviderImpl, serviceProviderInitData));
+ console.log(" Proxy:", serviceProviderProxy);
+ console.log("");
+
+ // 2. Deploy FleetIdentityUpgradeable
+ console.log("2. Deploying FleetIdentityUpgradeable...");
+ fleetIdentityImpl = address(new FleetIdentityUpgradeable());
+ console.log(" Implementation:", fleetIdentityImpl);
+
+ bytes memory fleetIdentityInitData =
+ abi.encodeWithSelector(FleetIdentityUpgradeable.initialize.selector, owner, bondToken, baseBond, countryMultiplier);
+ fleetIdentityProxy = address(new ERC1967Proxy(fleetIdentityImpl, fleetIdentityInitData));
+ console.log(" Proxy:", fleetIdentityProxy);
+ console.log("");
+
+ // 3. Deploy SwarmRegistryUniversalUpgradeable
+ console.log("3. Deploying SwarmRegistryUniversalUpgradeable...");
+ swarmRegistryImpl = address(new SwarmRegistryUniversalUpgradeable());
+ console.log(" Implementation:", swarmRegistryImpl);
+
+ bytes memory swarmRegistryInitData = abi.encodeWithSelector(
+ SwarmRegistryUniversalUpgradeable.initialize.selector, fleetIdentityProxy, serviceProviderProxy, owner
+ );
+ swarmRegistryProxy = address(new ERC1967Proxy(swarmRegistryImpl, swarmRegistryInitData));
+ console.log(" Proxy:", swarmRegistryProxy);
+
+ vm.stopBroadcast();
+
+ // Summary
+ console.log("");
+ console.log("=== Deployment Complete ===");
+ console.log("ServiceProvider Proxy:", serviceProviderProxy);
+ console.log("ServiceProvider Implementation:", serviceProviderImpl);
+ console.log("FleetIdentity Proxy:", fleetIdentityProxy);
+ console.log("FleetIdentity Implementation:", fleetIdentityImpl);
+ console.log("SwarmRegistry Proxy:", swarmRegistryProxy);
+ console.log("SwarmRegistry Implementation:", swarmRegistryImpl);
+ console.log("");
+ console.log("Save these proxy addresses for future upgrades!");
+ }
+}
diff --git a/script/UpgradeSwarm.s.sol b/script/UpgradeSwarm.s.sol
new file mode 100644
index 00000000..3b25d5e4
--- /dev/null
+++ b/script/UpgradeSwarm.s.sol
@@ -0,0 +1,201 @@
+// SPDX-License-Identifier: BSD-3-Clause-Clear
+
+pragma solidity ^0.8.24;
+
+import {Script, console} from "forge-std/Script.sol";
+
+import {ServiceProviderUpgradeable} from "../src/swarms/ServiceProviderUpgradeable.sol";
+import {FleetIdentityUpgradeable} from "../src/swarms/FleetIdentityUpgradeable.sol";
+import {SwarmRegistryUniversalUpgradeable} from "../src/swarms/SwarmRegistryUniversalUpgradeable.sol";
+import {SwarmRegistryL1Upgradeable} from "../src/swarms/SwarmRegistryL1Upgradeable.sol";
+
+/**
+ * @title UpgradeSwarm
+ * @notice Script for upgrading deployed swarm contracts to new implementations.
+ *
+ * @dev **Storage Migration Rules:**
+ * 1. Never remove or reorder existing storage variables
+ * 2. Only append new variables at the end (reduce __gap accordingly)
+ * 3. Never change variable types
+ * 4. Use `reinitializer(n)` for version-specific initialization
+ *
+ * **Pre-Upgrade Checklist:**
+ * 1. Run `forge inspect NewContract storageLayout` and compare to V1
+ * 2. Ensure all tests pass with new implementation
+ * 3. Test upgrade on fork: `forge script ... --fork-url $RPC_URL`
+ * 4. Verify new implementation on block explorer
+ *
+ * Usage:
+ * # Upgrade ServiceProvider
+ * CONTRACT_TYPE=ServiceProvider PROXY_ADDRESS=0x... \
+ * forge script script/UpgradeSwarm.s.sol --rpc-url $RPC_URL --broadcast
+ *
+ * # Upgrade FleetIdentity
+ * CONTRACT_TYPE=FleetIdentity PROXY_ADDRESS=0x... \
+ * forge script script/UpgradeSwarm.s.sol --rpc-url $RPC_URL --broadcast
+ *
+ * # Upgrade SwarmRegistryUniversal
+ * CONTRACT_TYPE=SwarmRegistryUniversal PROXY_ADDRESS=0x... \
+ * forge script script/UpgradeSwarm.s.sol --rpc-url $RPC_URL --broadcast
+ *
+ * # Upgrade SwarmRegistryL1
+ * CONTRACT_TYPE=SwarmRegistryL1 PROXY_ADDRESS=0x... \
+ * forge script script/UpgradeSwarm.s.sol --rpc-url $RPC_URL --broadcast
+ *
+ * Environment Variables:
+ * - DEPLOYER_PRIVATE_KEY: Private key of contract owner
+ * - PROXY_ADDRESS: Address of the proxy to upgrade
+ * - CONTRACT_TYPE: One of "ServiceProvider", "FleetIdentity", "SwarmRegistryUniversal", "SwarmRegistryL1"
+ * - REINIT_DATA: Optional ABI-encoded data for reinitializer call (e.g., for V2 migration)
+ */
+contract UpgradeSwarm is Script {
+ function run() external {
+ uint256 deployerPrivateKey = vm.envUint("DEPLOYER_PRIVATE_KEY");
+ address proxyAddress = vm.envAddress("PROXY_ADDRESS");
+ string memory contractType = vm.envString("CONTRACT_TYPE");
+ bytes memory reinitData = vm.envOr("REINIT_DATA", bytes(""));
+
+ console.log("=== Upgrading Swarm Contract ===");
+ console.log("Contract Type:", contractType);
+ console.log("Proxy Address:", proxyAddress);
+ console.log("Has Reinit Data:", reinitData.length > 0);
+ console.log("");
+
+ vm.startBroadcast(deployerPrivateKey);
+
+ address newImpl;
+
+ if (keccak256(bytes(contractType)) == keccak256("ServiceProvider")) {
+ newImpl = _upgradeServiceProvider(proxyAddress, reinitData);
+ } else if (keccak256(bytes(contractType)) == keccak256("FleetIdentity")) {
+ newImpl = _upgradeFleetIdentity(proxyAddress, reinitData);
+ } else if (keccak256(bytes(contractType)) == keccak256("SwarmRegistryUniversal")) {
+ newImpl = _upgradeSwarmRegistryUniversal(proxyAddress, reinitData);
+ } else if (keccak256(bytes(contractType)) == keccak256("SwarmRegistryL1")) {
+ newImpl = _upgradeSwarmRegistryL1(proxyAddress, reinitData);
+ } else {
+ revert("Invalid CONTRACT_TYPE. Use: ServiceProvider, FleetIdentity, SwarmRegistryUniversal, SwarmRegistryL1");
+ }
+
+ vm.stopBroadcast();
+
+ console.log("");
+ console.log("=== Upgrade Complete ===");
+ console.log("New Implementation:", newImpl);
+ console.log("Proxy (unchanged):", proxyAddress);
+ }
+
+ function _upgradeServiceProvider(address proxy, bytes memory reinitData) internal returns (address impl) {
+ console.log("Deploying new ServiceProviderUpgradeable implementation...");
+ impl = address(new ServiceProviderUpgradeable());
+ console.log("New implementation:", impl);
+
+ ServiceProviderUpgradeable proxyContract = ServiceProviderUpgradeable(proxy);
+
+ if (reinitData.length > 0) {
+ console.log("Calling upgradeToAndCall with reinitializer...");
+ proxyContract.upgradeToAndCall(impl, reinitData);
+ } else {
+ console.log("Calling upgradeToAndCall...");
+ proxyContract.upgradeToAndCall(impl, "");
+ }
+ }
+
+ function _upgradeFleetIdentity(address proxy, bytes memory reinitData) internal returns (address impl) {
+ console.log("Deploying new FleetIdentityUpgradeable implementation...");
+ impl = address(new FleetIdentityUpgradeable());
+ console.log("New implementation:", impl);
+
+ FleetIdentityUpgradeable proxyContract = FleetIdentityUpgradeable(proxy);
+
+ if (reinitData.length > 0) {
+ console.log("Calling upgradeToAndCall with reinitializer...");
+ proxyContract.upgradeToAndCall(impl, reinitData);
+ } else {
+ console.log("Calling upgradeToAndCall...");
+ proxyContract.upgradeToAndCall(impl, "");
+ }
+ }
+
+ function _upgradeSwarmRegistryUniversal(address proxy, bytes memory reinitData) internal returns (address impl) {
+ console.log("Deploying new SwarmRegistryUniversalUpgradeable implementation...");
+ impl = address(new SwarmRegistryUniversalUpgradeable());
+ console.log("New implementation:", impl);
+
+ SwarmRegistryUniversalUpgradeable proxyContract = SwarmRegistryUniversalUpgradeable(proxy);
+
+ if (reinitData.length > 0) {
+ console.log("Calling upgradeToAndCall with reinitializer...");
+ proxyContract.upgradeToAndCall(impl, reinitData);
+ } else {
+ console.log("Calling upgradeToAndCall...");
+ proxyContract.upgradeToAndCall(impl, "");
+ }
+ }
+
+ function _upgradeSwarmRegistryL1(address proxy, bytes memory reinitData) internal returns (address impl) {
+ console.log("Deploying new SwarmRegistryL1Upgradeable implementation...");
+ impl = address(new SwarmRegistryL1Upgradeable());
+ console.log("New implementation:", impl);
+
+ SwarmRegistryL1Upgradeable proxyContract = SwarmRegistryL1Upgradeable(proxy);
+
+ if (reinitData.length > 0) {
+ console.log("Calling upgradeToAndCall with reinitializer...");
+ proxyContract.upgradeToAndCall(impl, reinitData);
+ } else {
+ console.log("Calling upgradeToAndCall...");
+ proxyContract.upgradeToAndCall(impl, "");
+ }
+ }
+}
+
+/**
+ * @title ExampleV2Migration
+ * @notice Example of how to add a V2 reinitializer function and migrate storage.
+ *
+ * @dev When you need to add new storage to an existing contract:
+ *
+ * 1. Add new storage variables ABOVE the __gap (reduce gap size accordingly)
+ * 2. Add a reinitializer function:
+ *
+ * ```solidity
+ * function initializeV2(uint256 newParam) external reinitializer(2) {
+ * _newParamIntroducedInV2 = newParam;
+ * }
+ * ```
+ *
+ * 3. Generate reinit calldata:
+ * ```bash
+ * cast calldata "initializeV2(uint256)" 12345
+ * ```
+ *
+ * 4. Pass to upgrade script:
+ * ```bash
+ * REINIT_DATA=0x... forge script ...
+ * ```
+ *
+ * Example V2 contract structure (do not deploy this, it's documentation):
+ *
+ * contract ServiceProviderUpgradeableV2 is ServiceProviderUpgradeable {
+ * // New V2 storage - add ABOVE __gap
+ * mapping(uint256 => uint256) public providerScores;
+ *
+ * // Reduce __gap from 49 to 48 (added 1 slot)
+ * uint256[48] private __gap;
+ *
+ * // V2 reinitializer
+ * function initializeV2() external reinitializer(2) {
+ * // Initialize any V2-specific state here
+ * }
+ *
+ * // New V2 functions
+ * function setProviderScore(uint256 tokenId, uint256 score) external {
+ * if (ownerOf(tokenId) != msg.sender) revert NotTokenOwner();
+ * providerScores[tokenId] = score;
+ * }
+ * }
+ */
+contract ExampleV2Migration {
+ // This contract is documentation only
+}
diff --git a/src/swarms/FleetIdentityUpgradeable.sol b/src/swarms/FleetIdentityUpgradeable.sol
new file mode 100644
index 00000000..32bd2af6
--- /dev/null
+++ b/src/swarms/FleetIdentityUpgradeable.sol
@@ -0,0 +1,1087 @@
+// SPDX-License-Identifier: BSD-3-Clause-Clear
+
+pragma solidity ^0.8.24;
+
+import {ERC721EnumerableUpgradeable} from
+ "@openzeppelin/contracts-upgradeable/token/ERC721/extensions/ERC721EnumerableUpgradeable.sol";
+import {Ownable2StepUpgradeable} from "@openzeppelin/contracts-upgradeable/access/Ownable2StepUpgradeable.sol";
+import {UUPSUpgradeable} from "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol";
+import {Initializable} from "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
+import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
+import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
+import {ReentrancyGuard} from "solady/utils/ReentrancyGuard.sol";
+
+import {RegistrationLevel} from "./interfaces/SwarmTypes.sol";
+
+/**
+ * @title FleetIdentityUpgradeable
+ * @notice UUPS-upgradeable ERC-721 with ERC721Enumerable representing ownership of a BLE fleet,
+ * secured by an ERC-20 bond organized into geometric tiers.
+ *
+ * @dev **Upgrade Pattern:**
+ * - Uses OpenZeppelin UUPS proxy pattern for upgradeability.
+ * - Only the contract owner can authorize upgrades.
+ * - Storage layout must be preserved across upgrades (append-only).
+ *
+ * **Storage Migration Example (V1 → V2):**
+ * ```solidity
+ * function initializeV2(uint256 newParam) external reinitializer(2) {
+ * _newParamIntroducedInV2 = newParam;
+ * }
+ * ```
+ *
+ * **Two-level geographic registration**
+ *
+ * Fleets register at exactly one level:
+ * - Country — regionKey = countryCode (ISO 3166-1 numeric, 1-999)
+ * - Admin Area — regionKey = (countryCode << 10) | adminCode (>= 1024)
+ *
+ * Each regionKey has its **own independent tier namespace** — tier indices
+ * start at 0 for every region. The first fleet in any region always pays
+ * the level-appropriate bond (LOCAL: BASE_BOND, COUNTRY: BASE_BOND * 16).
+ *
+ * **Economic Model**
+ *
+ * - Tier capacity: 10 members per tier (unified across levels)
+ * - Local bond: BASE_BOND * 2^tier
+ * - Country bond: BASE_BOND * COUNTRY_BOND_MULTIPLIER * 2^tier (16× local)
+ *
+ * **TokenID Encoding**
+ *
+ * TokenID = (regionKey << 128) | uuid
+ * - Bits 0-127: UUID (bytes16 Proximity UUID)
+ * - Bits 128-159: Region key (32-bit country or admin-area code)
+ */
+contract FleetIdentityUpgradeable is
+ Initializable,
+ ERC721EnumerableUpgradeable,
+ Ownable2StepUpgradeable,
+ UUPSUpgradeable,
+ ReentrancyGuard
+{
+ using SafeERC20 for IERC20;
+
+ // ──────────────────────────────────────────────
+ // Errors
+ // ──────────────────────────────────────────────
+ error InvalidUUID();
+ error NotTokenOwner();
+ error MaxTiersReached();
+ error TierFull();
+ error TargetTierNotHigher();
+ error TargetTierNotLower();
+ error TargetTierSameAsCurrent();
+ error InvalidCountryCode();
+ error InvalidAdminCode();
+ error AdminAreaRequired();
+ error UuidLevelMismatch();
+ error UuidAlreadyOwned();
+ error UuidNotOwned();
+ error NotUuidOwner();
+ error NotOperator();
+ error NotOwnerOrOperator();
+ error InvalidBaseBond();
+ error InvalidMultiplier();
+ error InvalidBondToken();
+
+ // ──────────────────────────────────────────────
+ // Constants
+ // ──────────────────────────────────────────────
+
+ /// @notice Unified tier capacity for all levels.
+ uint256 public constant TIER_CAPACITY = 10;
+
+ /// @notice Default country bond multiplier when not explicitly set (16× local).
+ uint256 public constant DEFAULT_COUNTRY_BOND_MULTIPLIER = 16;
+
+ /// @notice Default base bond for tier 0.
+ uint256 public constant DEFAULT_BASE_BOND = 1e18;
+
+ /// @notice Hard cap on tier count per region.
+ uint256 public constant MAX_TIERS = 24;
+
+ /// @notice Maximum UUIDs returned by buildHighestBondedUuidBundle.
+ uint256 public constant MAX_BONDED_UUID_BUNDLE_SIZE = 20;
+
+ /// @notice ISO 3166-1 numeric upper bound for country codes.
+ uint16 internal constant MAX_COUNTRY_CODE = 999;
+
+ /// @notice Upper bound for admin-area codes within a country.
+ uint16 internal constant MAX_ADMIN_CODE = 255;
+
+ /// @dev Bit shift for packing countryCode into an admin-area region key.
+ uint256 private constant ADMIN_SHIFT = 10;
+ /// @dev Bitmask for extracting adminCode from an admin-area region key.
+ uint32 private constant ADMIN_CODE_MASK = 0x3FF;
+
+ /// @notice Region key for owned-only UUIDs (not registered in any region).
+ uint32 public constant OWNED_REGION_KEY = 0;
+
+ // ──────────────────────────────────────────────
+ // Storage (V1) - Order matters for upgrades!
+ // ──────────────────────────────────────────────
+
+ /// @notice The ERC-20 token used for bonds (e.g. NODL).
+ /// @dev In non-upgradeable version this was immutable. Now stored in proxy storage.
+ IERC20 private _bondToken;
+
+ /// @notice Base bond for tier 0 in any region. Tier K requires BASE_BOND * 2^K.
+ /// @dev In non-upgradeable version this was immutable. Now stored in proxy storage.
+ uint256 private _baseBond;
+
+ // ──────────────────────────────────────────────
+ // Region-namespaced tier data
+ // ──────────────────────────────────────────────
+
+ /// @notice regionKey -> number of tiers opened in that region.
+ mapping(uint32 => uint256) public regionTierCount;
+
+ /// @notice regionKey -> tierIndex -> list of token IDs.
+ mapping(uint32 => mapping(uint256 => uint256[])) internal _regionTierMembers;
+
+ /// @notice Token ID -> index within its tier's member array (for O(1) removal).
+ mapping(uint256 => uint256) internal _indexInTier;
+
+ // ──────────────────────────────────────────────
+ // Fleet data
+ // ──────────────────────────────────────────────
+
+ /// @notice Token ID -> tier index (within its region) the fleet belongs to.
+ mapping(uint256 => uint256) public fleetTier;
+
+ // ──────────────────────────────────────────────
+ // UUID ownership tracking
+ // ──────────────────────────────────────────────
+
+ /// @notice UUID -> address that first registered a token for this UUID.
+ mapping(bytes16 => address) public uuidOwner;
+
+ /// @notice UUID -> count of active tokens for this UUID (across all regions).
+ mapping(bytes16 => uint256) public uuidTokenCount;
+
+ /// @notice UUID -> registration level.
+ mapping(bytes16 => RegistrationLevel) public uuidLevel;
+
+ /// @notice UUID -> operator address for tier maintenance.
+ mapping(bytes16 => address) public uuidOperator;
+
+ /// @notice UUID -> total tier bonds across all registered regions.
+ mapping(bytes16 => uint256) public uuidTotalTierBonds;
+
+ // ──────────────────────────────────────────────
+ // Bond Snapshots (for safe parameter reconfiguration)
+ // ──────────────────────────────────────────────
+
+ /// @notice Configurable country bond multiplier. 0 = use DEFAULT_COUNTRY_BOND_MULTIPLIER (16).
+ /// @dev Can be updated by owner via setCountryBondMultiplier().
+ uint256 private _countryBondMultiplier;
+
+ /// @notice tokenId -> tier-0 equivalent bond paid at registration.
+ /// @dev Stores baseBond (for local) or baseBond*multiplier (for country).
+ /// Actual tier K bond = tokenTier0Bond[tokenId] << K.
+ mapping(uint256 => uint256) public tokenTier0Bond;
+
+ /// @notice UUID -> ownership bond paid at claim/first-registration.
+ /// @dev Refunded when owned-only token is burned.
+ mapping(bytes16 => uint256) public uuidOwnershipBondPaid;
+
+ // ──────────────────────────────────────────────
+ // On-chain region indexes
+ // ──────────────────────────────────────────────
+
+ /// @dev Set of country codes with at least one active fleet.
+ uint16[] internal _activeCountries;
+ mapping(uint16 => uint256) internal _activeCountryIndex;
+
+ /// @dev Country → list of admin-area region keys with at least one active fleet.
+ mapping(uint16 => uint32[]) internal _countryAdminAreas;
+ mapping(uint32 => uint256) internal _countryAdminAreaIndex;
+
+ // ──────────────────────────────────────────────
+ // Storage Gap (for future upgrades)
+ // ──────────────────────────────────────────────
+
+ /// @dev Reserved storage slots for future upgrades.
+ /// When adding new storage in V2+, reduce this gap accordingly.
+ // solhint-disable-next-line var-name-mixedcase
+ uint256[40] private __gap;
+
+ // ──────────────────────────────────────────────
+ // Events
+ // ──────────────────────────────────────────────
+
+ event FleetRegistered(
+ address indexed owner,
+ bytes16 indexed uuid,
+ uint256 indexed tokenId,
+ uint32 regionKey,
+ uint256 tierIndex,
+ uint256 bondAmount,
+ address operator
+ );
+ event OperatorSet(
+ bytes16 indexed uuid, address indexed oldOperator, address indexed newOperator, uint256 tierExcessTransferred
+ );
+ event FleetPromoted(
+ uint256 indexed tokenId, uint256 indexed fromTier, uint256 indexed toTier, uint256 additionalBond
+ );
+ event FleetDemoted(uint256 indexed tokenId, uint256 indexed fromTier, uint256 indexed toTier, uint256 bondRefund);
+ event FleetBurned(
+ address indexed owner, uint256 indexed tokenId, uint32 indexed regionKey, uint256 tierIndex, uint256 bondRefund
+ );
+ event UuidClaimed(address indexed owner, bytes16 indexed uuid, address indexed operator);
+ event BaseBondUpdated(uint256 indexed oldBaseBond, uint256 indexed newBaseBond);
+ event CountryMultiplierUpdated(uint256 indexed oldMultiplier, uint256 indexed newMultiplier);
+
+ // ──────────────────────────────────────────────
+ // Constructor (disables initializers on implementation)
+ // ──────────────────────────────────────────────
+
+ /// @custom:oz-upgrades-unsafe-allow constructor
+ constructor() {
+ _disableInitializers();
+ }
+
+ // ──────────────────────────────────────────────
+ // Initializer (replaces constructor)
+ // ──────────────────────────────────────────────
+
+ /// @notice Initializes the contract. Must be called once via proxy.
+ /// @param owner_ The address that will own this contract and can authorize upgrades.
+ /// @param bondToken_ Address of the ERC-20 token used for bonds (required).
+ /// @param baseBond_ Base bond for tier 0 (0 = DEFAULT_BASE_BOND = 1M NODL).
+ /// @param countryMultiplier_ Country bond multiplier (0 = DEFAULT_COUNTRY_BOND_MULTIPLIER = 16).
+ function initialize(address owner_, address bondToken_, uint256 baseBond_, uint256 countryMultiplier_)
+ external
+ initializer
+ {
+ if (bondToken_ == address(0)) revert InvalidBondToken();
+
+ __ERC721_init("Swarm Fleet Identity", "SFID");
+ __ERC721Enumerable_init();
+ __Ownable_init(owner_);
+ __Ownable2Step_init();
+
+ _bondToken = IERC20(bondToken_);
+ _baseBond = baseBond_ == 0 ? DEFAULT_BASE_BOND : baseBond_;
+ _countryBondMultiplier = countryMultiplier_ == 0 ? DEFAULT_COUNTRY_BOND_MULTIPLIER : countryMultiplier_;
+ }
+
+ // ──────────────────────────────────────────────
+ // Admin Functions
+ // ──────────────────────────────────────────────
+
+ /// @notice Updates the base bond amount for future registrations.
+ /// @dev Existing tokens use their stored snapshots for refunds.
+ /// **IMPORTANT**: Verify contract solvency before increasing.
+ /// @param newBaseBond The new base bond amount (must be non-zero).
+ function setBaseBond(uint256 newBaseBond) external onlyOwner {
+ if (newBaseBond == 0) revert InvalidBaseBond();
+ uint256 oldBaseBond = _baseBond;
+ _baseBond = newBaseBond;
+ emit BaseBondUpdated(oldBaseBond, newBaseBond);
+ }
+
+ /// @notice Updates the country bond multiplier for future registrations.
+ /// @dev Existing tokens use their stored snapshots for refunds.
+ /// **IMPORTANT**: Verify contract solvency before increasing.
+ /// @param newMultiplier The new multiplier (must be non-zero).
+ function setCountryBondMultiplier(uint256 newMultiplier) external onlyOwner {
+ if (newMultiplier == 0) revert InvalidMultiplier();
+ uint256 oldMultiplier = countryBondMultiplier();
+ _countryBondMultiplier = newMultiplier;
+ emit CountryMultiplierUpdated(oldMultiplier, newMultiplier);
+ }
+
+ /// @notice Updates both bond parameters atomically for future registrations.
+ /// @dev Existing tokens use their stored snapshots for refunds.
+ /// **IMPORTANT**: Verify contract solvency before increasing.
+ /// @param newBaseBond The new base bond amount (must be non-zero).
+ /// @param newMultiplier The new country multiplier (must be non-zero).
+ function setBondParameters(uint256 newBaseBond, uint256 newMultiplier) external onlyOwner {
+ if (newBaseBond == 0) revert InvalidBaseBond();
+ if (newMultiplier == 0) revert InvalidMultiplier();
+
+ uint256 oldBaseBond = _baseBond;
+ uint256 oldMultiplier = countryBondMultiplier();
+
+ _baseBond = newBaseBond;
+ _countryBondMultiplier = newMultiplier;
+
+ emit BaseBondUpdated(oldBaseBond, newBaseBond);
+ emit CountryMultiplierUpdated(oldMultiplier, newMultiplier);
+ }
+
+ // ──────────────────────────────────────────────
+ // Public Getters for former immutables
+ // ──────────────────────────────────────────────
+
+ /// @notice Returns the bond token address.
+ function BOND_TOKEN() external view returns (IERC20) {
+ return _bondToken;
+ }
+
+ /// @notice Returns the base bond amount.
+ function BASE_BOND() external view returns (uint256) {
+ return _baseBond;
+ }
+
+ /// @notice Returns the country bond multiplier.
+ function countryBondMultiplier() public view returns (uint256) {
+ return _countryBondMultiplier;
+ }
+
+ // ══════════════════════════════════════════════
+ // Registration: Country (operator-only with tier)
+ // ══════════════════════════════════════════════
+
+ /// @notice Register a fleet under a country at a specific tier.
+ function registerFleetCountry(bytes16 uuid, uint16 countryCode, uint256 targetTier)
+ external
+ nonReentrant
+ returns (uint256 tokenId)
+ {
+ if (uuid == bytes16(0)) revert InvalidUUID();
+ if (countryCode == 0 || countryCode > MAX_COUNTRY_CODE) revert InvalidCountryCode();
+ uint32 regionKey = uint32(countryCode);
+ _validateExplicitTier(regionKey, targetTier);
+ tokenId = _register(uuid, regionKey, targetTier);
+ }
+
+ // ══════════════════════════════════════════════
+ // Registration: Admin Area (local, operator-only with tier)
+ // ══════════════════════════════════════════════
+
+ /// @notice Register a fleet under a country + admin area at a specific tier.
+ function registerFleetLocal(bytes16 uuid, uint16 countryCode, uint16 adminCode, uint256 targetTier)
+ external
+ nonReentrant
+ returns (uint256 tokenId)
+ {
+ if (uuid == bytes16(0)) revert InvalidUUID();
+ if (countryCode == 0 || countryCode > MAX_COUNTRY_CODE) revert InvalidCountryCode();
+ if (adminCode == 0 || adminCode > MAX_ADMIN_CODE) revert InvalidAdminCode();
+ uint32 regionKey = makeAdminRegion(countryCode, adminCode);
+ _validateExplicitTier(regionKey, targetTier);
+ tokenId = _register(uuid, regionKey, targetTier);
+ }
+
+ // ══════════════════════════════════════════════
+ // Promote / Demote (region-aware)
+ // ══════════════════════════════════════════════
+
+ /// @notice Promotes a fleet to the next tier within its region.
+ function promote(uint256 tokenId) external nonReentrant {
+ _promote(tokenId, fleetTier[tokenId] + 1);
+ }
+
+ /// @notice Moves a fleet to a different tier within its region.
+ function reassignTier(uint256 tokenId, uint256 targetTier) external nonReentrant {
+ uint256 currentTier = fleetTier[tokenId];
+ if (targetTier == currentTier) revert TargetTierSameAsCurrent();
+ if (targetTier > currentTier) {
+ _promote(tokenId, targetTier);
+ } else {
+ _demote(tokenId, targetTier);
+ }
+ }
+
+ // ══════════════════════════════════════════════
+ // Operator Management
+ // ══════════════════════════════════════════════
+
+ /// @notice Sets or changes the operator for a UUID.
+ function setOperator(bytes16 uuid, address newOperator) external nonReentrant {
+ if (uuidOwner[uuid] != msg.sender) revert NotUuidOwner();
+ if (uuidLevel[uuid] == RegistrationLevel.None) {
+ revert UuidNotOwned();
+ }
+
+ address oldOperator = operatorOf(uuid);
+ address storedOperator = (newOperator == msg.sender) ? address(0) : newOperator;
+ address effectiveNewOperator = (storedOperator == address(0)) ? msg.sender : storedOperator;
+
+ uint256 tierBonds = uuidTotalTierBonds[uuid];
+
+ uuidOperator[uuid] = storedOperator;
+
+ if (tierBonds > 0 && oldOperator != effectiveNewOperator) {
+ _pullBond(effectiveNewOperator, tierBonds);
+ _refundBond(oldOperator, tierBonds);
+ }
+
+ emit OperatorSet(uuid, oldOperator, effectiveNewOperator, tierBonds);
+ }
+
+ // ══════════════════════════════════════════════
+ // Burn
+ // ══════════════════════════════════════════════
+
+ /// @notice Burns the fleet NFT and refunds the bond.
+ function burn(uint256 tokenId) external nonReentrant {
+ address tokenHolder = ownerOf(tokenId);
+
+ uint32 region = tokenRegion(tokenId);
+ bytes16 uuid = tokenUuid(tokenId);
+ address owner = uuidOwner[uuid];
+ address operator = operatorOf(uuid);
+ bool isLastToken = uuidTokenCount[uuid] == 1;
+
+ if (region == OWNED_REGION_KEY) {
+ if (tokenHolder != msg.sender) revert NotTokenOwner();
+
+ // Use snapshot for accurate refund
+ uint256 ownershipBond = uuidOwnershipBondPaid[uuid];
+
+ _burn(tokenId);
+ _clearUuidOwnership(uuid);
+ _refundBond(owner, ownershipBond);
+
+ emit FleetBurned(tokenHolder, tokenId, region, 0, ownershipBond);
+ } else {
+ if (msg.sender != operator) {
+ revert NotOperator();
+ }
+
+ uint256 tier = fleetTier[tokenId];
+ // Use snapshot for accurate refund
+ uint256 tierBondAmount = _tokenTierBond(tokenId, tier);
+
+ uuidTotalTierBonds[uuid] -= tierBondAmount;
+
+ _cleanupFleetFromTier(tokenId, region, tier);
+ delete tokenTier0Bond[tokenId];
+ _burn(tokenId);
+
+ if (isLastToken) {
+ uuidLevel[uuid] = RegistrationLevel.Owned;
+ uint256 ownedTokenId = uint256(uint128(uuid));
+ _mint(owner, ownedTokenId);
+ } else {
+ uuidTokenCount[uuid]--;
+ }
+
+ _refundBond(operator, tierBondAmount);
+
+ emit FleetBurned(tokenHolder, tokenId, region, tier, tierBondAmount);
+ }
+ }
+
+ // ══════════════════════════════════════════════
+ // UUID Ownership (Owned-Only Mode)
+ // ══════════════════════════════════════════════
+
+ /// @notice Claim ownership of a UUID without registering in any region.
+ function claimUuid(bytes16 uuid, address operator) external nonReentrant returns (uint256 tokenId) {
+ if (uuid == bytes16(0)) revert InvalidUUID();
+ if (uuidOwner[uuid] != address(0)) revert UuidAlreadyOwned();
+
+ uuidOwner[uuid] = msg.sender;
+ uuidLevel[uuid] = RegistrationLevel.Owned;
+ uuidTokenCount[uuid] = 1;
+ uuidOperator[uuid] = (operator == address(0) || operator == msg.sender) ? address(0) : operator;
+
+ tokenId = uint256(uint128(uuid));
+ _mint(msg.sender, tokenId);
+
+ uuidOwnershipBondPaid[uuid] = _baseBond;
+ _pullBond(msg.sender, _baseBond);
+
+ emit UuidClaimed(msg.sender, uuid, operatorOf(uuid));
+ }
+
+ // ══════════════════════════════════════════════
+ // Views: Bond & tier helpers
+ // ══════════════════════════════════════════════
+
+ /// @notice Bond required for tier K at current parameters.
+ /// @dev Use _tokenTierBond for refund calculations on existing tokens.
+ function tierBond(uint256 tier, bool isCountry) public view returns (uint256) {
+ uint256 base = _baseBond << tier;
+ return isCountry ? base * countryBondMultiplier() : base;
+ }
+
+ /// @notice Bond for a token at a given tier based on registration-time parameters.
+ /// @dev Uses tier-0 bond stored at registration; returns 0 for non-existent tokens.
+ function _tokenTierBond(uint256 tokenId, uint256 tier) internal view returns (uint256) {
+ return tokenTier0Bond[tokenId] << tier;
+ }
+
+ /// @notice Returns the cheapest tier for local inclusion.
+ function localInclusionHint(uint16 countryCode, uint16 adminCode)
+ external
+ view
+ returns (uint256 inclusionTier, uint256 bond)
+ {
+ if (countryCode == 0 || countryCode > MAX_COUNTRY_CODE) revert InvalidCountryCode();
+ if (adminCode == 0 || adminCode > MAX_ADMIN_CODE) revert InvalidAdminCode();
+ inclusionTier = _findCheapestInclusionTier(countryCode, adminCode, false);
+ bond = tierBond(inclusionTier, false);
+ }
+
+ /// @notice Returns the cheapest tier for country inclusion.
+ function countryInclusionHint(uint16 countryCode) external view returns (uint256 inclusionTier, uint256 bond) {
+ if (countryCode == 0 || countryCode > MAX_COUNTRY_CODE) revert InvalidCountryCode();
+
+ inclusionTier = _findCheapestInclusionTier(countryCode, 0, true);
+
+ uint32[] storage countryAreas = _countryAdminAreas[countryCode];
+ uint256 len = countryAreas.length;
+ for (uint256 i = 0; i < len; ++i) {
+ uint16 admin = _adminFromRegion(countryAreas[i]);
+ uint256 t = _findCheapestInclusionTier(countryCode, admin, true);
+ if (t > inclusionTier) inclusionTier = t;
+ }
+ bond = tierBond(inclusionTier, true);
+ }
+
+ /// @notice Highest non-empty tier in a region, or 0 if none.
+ function highestActiveTier(uint32 regionKey) external view returns (uint256) {
+ uint256 tierCount = regionTierCount[regionKey];
+ if (tierCount == 0) return 0;
+ return tierCount - 1;
+ }
+
+ /// @notice Number of members in a specific tier of a region.
+ function tierMemberCount(uint32 regionKey, uint256 tier) external view returns (uint256) {
+ return _regionTierMembers[regionKey][tier].length;
+ }
+
+ /// @notice All token IDs in a specific tier of a region.
+ function getTierMembers(uint32 regionKey, uint256 tier) external view returns (uint256[] memory) {
+ return _regionTierMembers[regionKey][tier];
+ }
+
+ /// @notice All UUIDs in a specific tier of a region.
+ function getTierUuids(uint32 regionKey, uint256 tier) external view returns (bytes16[] memory uuids) {
+ uint256[] storage members = _regionTierMembers[regionKey][tier];
+ uuids = new bytes16[](members.length);
+ for (uint256 i = 0; i < members.length; ++i) {
+ uuids[i] = tokenUuid(members[i]);
+ }
+ }
+
+ /// @notice UUID for a token ID.
+ function tokenUuid(uint256 tokenId) public pure returns (bytes16) {
+ return bytes16(uint128(tokenId));
+ }
+
+ /// @notice Region key encoded in a token ID.
+ function tokenRegion(uint256 tokenId) public pure returns (uint32) {
+ return uint32(tokenId >> 128);
+ }
+
+ /// @notice Computes the deterministic token ID for a uuid+region pair.
+ function computeTokenId(bytes16 uuid, uint32 regionKey) public pure returns (uint256) {
+ return (uint256(regionKey) << 128) | uint256(uint128(uuid));
+ }
+
+ /// @notice Bond amount for a token.
+ function bonds(uint256 tokenId) external view returns (uint256) {
+ if (_ownerOf(tokenId) == address(0)) return 0;
+ uint32 region = tokenRegion(tokenId);
+ if (region == OWNED_REGION_KEY) return _baseBond;
+ return tierBond(fleetTier[tokenId], _isCountryRegion(region));
+ }
+
+ /// @notice Returns true if the UUID is in owned-only state.
+ function isOwnedOnly(bytes16 uuid) external view returns (bool) {
+ return uuidLevel[uuid] == RegistrationLevel.Owned;
+ }
+
+ /// @notice Returns the effective operator for a UUID.
+ function operatorOf(bytes16 uuid) public view returns (address operator) {
+ operator = uuidOperator[uuid];
+ if (operator == address(0)) {
+ operator = uuidOwner[uuid];
+ }
+ }
+
+ // ══════════════════════════════════════════════
+ // Views: EdgeBeaconScanner discovery
+ // ══════════════════════════════════════════════
+
+ /// @notice Builds a priority-ordered bundle of up to 20 UUIDs.
+ function buildHighestBondedUuidBundle(uint16 countryCode, uint16 adminCode)
+ external
+ view
+ returns (bytes16[] memory uuids, uint256 count)
+ {
+ if (countryCode == 0 || countryCode > MAX_COUNTRY_CODE) revert InvalidCountryCode();
+ if (adminCode == 0 || adminCode > MAX_ADMIN_CODE) revert AdminAreaRequired();
+
+ uint32 countryKey = uint32(countryCode);
+ uint32 adminKey = makeAdminRegion(countryCode, adminCode);
+
+ (uuids, count,,) = _buildHighestBondedUuidBundle(countryKey, adminKey);
+ }
+
+ /// @notice Builds a bundle containing ONLY country-level fleets.
+ function buildCountryOnlyBundle(uint16 countryCode)
+ external
+ view
+ returns (bytes16[] memory uuids, uint256 count)
+ {
+ if (countryCode == 0 || countryCode > MAX_COUNTRY_CODE) revert InvalidCountryCode();
+
+ uint32 countryKey = uint32(countryCode);
+ uint32 adminKey = makeAdminRegion(countryCode, 0);
+
+ (uuids, count,,) = _buildHighestBondedUuidBundle(countryKey, adminKey);
+ }
+
+ // ══════════════════════════════════════════════
+ // Views: Region indexes
+ // ══════════════════════════════════════════════
+
+ /// @notice Returns all country codes that have at least one active fleet.
+ /// @return Array of ISO 3166-1 numeric country codes.
+ function getActiveCountries() external view returns (uint16[] memory) {
+ return _activeCountries;
+ }
+
+ /// @notice Returns all admin-area region keys across all countries.
+ /// @return Array of encoded region keys (countryCode << 10 | adminCode).
+ function getActiveAdminAreas() external view returns (uint32[] memory) {
+ uint256 total = 0;
+ uint256 countryCount = _activeCountries.length;
+ for (uint256 i = 0; i < countryCount; ++i) {
+ total += _countryAdminAreas[_activeCountries[i]].length;
+ }
+
+ uint32[] memory result = new uint32[](total);
+ uint256 idx = 0;
+ for (uint256 i = 0; i < countryCount; ++i) {
+ uint32[] storage areas = _countryAdminAreas[_activeCountries[i]];
+ uint256 areaCount = areas.length;
+ for (uint256 j = 0; j < areaCount; ++j) {
+ result[idx++] = areas[j];
+ }
+ }
+ return result;
+ }
+
+ /// @notice Returns all active admin-area region keys for a specific country.
+ /// @param countryCode ISO 3166-1 numeric country code.
+ /// @return Array of encoded region keys for that country.
+ function getCountryAdminAreas(uint16 countryCode) external view returns (uint32[] memory) {
+ return _countryAdminAreas[countryCode];
+ }
+
+ /// @notice Encodes a country code and admin code into a region key.
+ /// @param countryCode ISO 3166-1 numeric country code (1-999).
+ /// @param adminCode Admin-area code within the country (1-255).
+ /// @return Encoded region key: (countryCode << 10) | adminCode.
+ function makeAdminRegion(uint16 countryCode, uint16 adminCode) public pure returns (uint32) {
+ return (uint32(countryCode) << uint32(ADMIN_SHIFT)) | uint32(adminCode);
+ }
+
+ // ══════════════════════════════════════════════
+ // UUPS Authorization
+ // ══════════════════════════════════════════════
+
+ /// @dev Only the owner can authorize an upgrade.
+ function _authorizeUpgrade(address newImplementation) internal override onlyOwner {}
+
+ // ══════════════════════════════════════════════
+ // Internal Functions
+ // ══════════════════════════════════════════════
+
+ function _countryFromRegion(uint32 adminRegion) internal pure returns (uint16) {
+ return uint16(adminRegion >> uint32(ADMIN_SHIFT));
+ }
+
+ function _adminFromRegion(uint32 adminRegion) internal pure returns (uint16) {
+ return uint16(adminRegion & ADMIN_CODE_MASK);
+ }
+
+ function _isCountryRegion(uint32 regionKey) internal pure returns (bool) {
+ return regionKey > 0 && regionKey <= MAX_COUNTRY_CODE;
+ }
+
+ function _pullBond(address from, uint256 amount) internal {
+ if (amount > 0) {
+ _bondToken.safeTransferFrom(from, address(this), amount);
+ }
+ }
+
+ function _refundBond(address to, uint256 amount) internal {
+ if (amount > 0) {
+ _bondToken.safeTransfer(to, amount);
+ }
+ }
+
+ function _clearUuidOwnership(bytes16 uuid) internal {
+ delete uuidOwner[uuid];
+ delete uuidTokenCount[uuid];
+ delete uuidLevel[uuid];
+ delete uuidOperator[uuid];
+ delete uuidTotalTierBonds[uuid];
+ delete uuidOwnershipBondPaid[uuid];
+ }
+
+ function _decrementUuidCount(bytes16 uuid) internal returns (uint256 newCount) {
+ newCount = uuidTokenCount[uuid] - 1;
+ if (newCount == 0) {
+ _clearUuidOwnership(uuid);
+ } else {
+ uuidTokenCount[uuid] = newCount;
+ }
+ }
+
+ function _cleanupFleetFromTier(uint256 tokenId, uint32 region, uint256 tier) internal {
+ _removeFromTier(tokenId, region, tier);
+ delete fleetTier[tokenId];
+ delete _indexInTier[tokenId];
+ _trimTierCount(region);
+ _removeFromRegionIndex(region);
+ }
+
+ function _mintFleetToken(bytes16 uuid, uint32 region, uint256 tier) internal returns (uint256 tokenId) {
+ tokenId = computeTokenId(uuid, region);
+ fleetTier[tokenId] = tier;
+ _addToTier(tokenId, region, tier);
+ _addToRegionIndex(region);
+ _mint(msg.sender, tokenId);
+ }
+
+ function _mintFleetTokenTo(address to, bytes16 uuid, uint32 region, uint256 tier)
+ internal
+ returns (uint256 tokenId)
+ {
+ tokenId = computeTokenId(uuid, region);
+ fleetTier[tokenId] = tier;
+ _addToTier(tokenId, region, tier);
+ _addToRegionIndex(region);
+ _mint(to, tokenId);
+ }
+
+ function _register(bytes16 uuid, uint32 region, uint256 targetTier) internal returns (uint256 tokenId) {
+ RegistrationLevel existingLevel = uuidLevel[uuid];
+ bool isCountry = _isCountryRegion(region);
+ RegistrationLevel targetLevel = isCountry ? RegistrationLevel.Country : RegistrationLevel.Local;
+ uint256 targetTierBond = tierBond(targetTier, isCountry);
+
+ // Store tier-0 equivalent bond for accurate refunds when parameters change
+ uint256 tier0Bond = isCountry ? _baseBond * countryBondMultiplier() : _baseBond;
+
+ if (existingLevel == RegistrationLevel.Owned) {
+ address operator = operatorOf(uuid);
+ if (operator != msg.sender) revert NotOperator();
+ address owner_ = uuidOwner[uuid];
+
+ _burn(uint256(uint128(uuid)));
+ uuidLevel[uuid] = targetLevel;
+ uuidTotalTierBonds[uuid] = targetTierBond;
+
+ tokenId = _mintFleetTokenTo(owner_, uuid, region, targetTier);
+ tokenTier0Bond[tokenId] = tier0Bond;
+
+ _pullBond(operator, targetTierBond);
+
+ emit FleetRegistered(owner_, uuid, tokenId, region, targetTier, targetTierBond, operator);
+ } else if (existingLevel == RegistrationLevel.None) {
+ uuidOwner[uuid] = msg.sender;
+ uuidLevel[uuid] = targetLevel;
+ uuidTokenCount[uuid] = 1;
+ uuidTotalTierBonds[uuid] = targetTierBond;
+
+ tokenId = _mintFleetToken(uuid, region, targetTier);
+ tokenTier0Bond[tokenId] = tier0Bond;
+ uuidOwnershipBondPaid[uuid] = _baseBond;
+
+ _pullBond(msg.sender, _baseBond + targetTierBond);
+
+ emit FleetRegistered(msg.sender, uuid, tokenId, region, targetTier, _baseBond + targetTierBond, msg.sender);
+ } else {
+ address operator = operatorOf(uuid);
+ if (operator != msg.sender) revert NotOperator();
+ if (existingLevel != targetLevel) revert UuidLevelMismatch();
+ address owner_ = uuidOwner[uuid];
+
+ uuidTokenCount[uuid]++;
+ uuidTotalTierBonds[uuid] += targetTierBond;
+
+ tokenId = _mintFleetTokenTo(owner_, uuid, region, targetTier);
+ tokenTier0Bond[tokenId] = tier0Bond;
+
+ _pullBond(operator, targetTierBond);
+
+ emit FleetRegistered(owner_, uuid, tokenId, region, targetTier, targetTierBond, operator);
+ }
+ }
+
+ function _promote(uint256 tokenId, uint256 targetTier) internal {
+ bytes16 uuid = tokenUuid(tokenId);
+ address operator = operatorOf(uuid);
+ if (operator != msg.sender) revert NotOperator();
+
+ uint32 region = tokenRegion(tokenId);
+ uint256 currentTier = fleetTier[tokenId];
+ if (targetTier <= currentTier) revert TargetTierNotHigher();
+ if (targetTier >= MAX_TIERS) revert MaxTiersReached();
+ if (_regionTierMembers[region][targetTier].length >= TIER_CAPACITY) revert TierFull();
+
+ bool isCountry = _isCountryRegion(region);
+ // Use stored tier-0 bond for current, current rate for target
+ uint256 currentBond = _tokenTierBond(tokenId, currentTier);
+ uint256 targetBond = tierBond(targetTier, isCountry);
+ uint256 additionalBond = targetBond - currentBond;
+
+ // Update tier-0 bond to current parameters since they're paying at current rates
+ tokenTier0Bond[tokenId] = isCountry ? _baseBond * countryBondMultiplier() : _baseBond;
+
+ uuidTotalTierBonds[uuid] += additionalBond;
+ _removeFromTier(tokenId, region, currentTier);
+ fleetTier[tokenId] = targetTier;
+ _addToTier(tokenId, region, targetTier);
+
+ _pullBond(operator, additionalBond);
+
+ emit FleetPromoted(tokenId, currentTier, targetTier, additionalBond);
+ }
+
+ function _demote(uint256 tokenId, uint256 targetTier) internal {
+ bytes16 uuid = tokenUuid(tokenId);
+ address operator = operatorOf(uuid);
+ if (operator != msg.sender) revert NotOperator();
+
+ uint32 region = tokenRegion(tokenId);
+ uint256 currentTier = fleetTier[tokenId];
+ if (targetTier >= currentTier) revert TargetTierNotLower();
+ if (_regionTierMembers[region][targetTier].length >= TIER_CAPACITY) revert TierFull();
+
+ // Use snapshot for accurate refund based on what was paid
+ uint256 currentBond = _tokenTierBond(tokenId, currentTier);
+ uint256 targetBond = _tokenTierBond(tokenId, targetTier);
+ uint256 refund = currentBond - targetBond;
+
+ uuidTotalTierBonds[uuid] -= refund;
+ _removeFromTier(tokenId, region, currentTier);
+ fleetTier[tokenId] = targetTier;
+ _addToTier(tokenId, region, targetTier);
+ _trimTierCount(region);
+
+ _refundBond(operator, refund);
+
+ emit FleetDemoted(tokenId, currentTier, targetTier, refund);
+ }
+
+ function _validateExplicitTier(uint32 region, uint256 targetTier) internal view {
+ if (targetTier >= MAX_TIERS) revert MaxTiersReached();
+ if (_regionTierMembers[region][targetTier].length >= TIER_CAPACITY) revert TierFull();
+ }
+
+ function _buildHighestBondedUuidBundle(uint32 countryKey, uint32 adminKey)
+ internal
+ view
+ returns (bytes16[] memory uuids, uint256 count, uint256 highestTier, uint256 lowestTier)
+ {
+ highestTier = _findMaxTierIndex(countryKey, adminKey);
+
+ uuids = new bytes16[](MAX_BONDED_UUID_BUNDLE_SIZE);
+
+ for (lowestTier = highestTier + 1; lowestTier > 0 && count < MAX_BONDED_UUID_BUNDLE_SIZE;) {
+ unchecked {
+ --lowestTier;
+ }
+
+ count = _appendTierUuids(adminKey, lowestTier, uuids, count);
+ count = _appendTierUuids(countryKey, lowestTier, uuids, count);
+ }
+
+ assembly {
+ mstore(uuids, count)
+ }
+ }
+
+ function _appendTierUuids(uint32 regionKey, uint256 tier, bytes16[] memory uuids, uint256 count)
+ internal
+ view
+ returns (uint256)
+ {
+ uint256[] storage members = _regionTierMembers[regionKey][tier];
+ uint256 len = members.length;
+ uint256 room = MAX_BONDED_UUID_BUNDLE_SIZE - count;
+ uint256 toInclude = len < room ? len : room;
+
+ for (uint256 i = 0; i < toInclude; ++i) {
+ uuids[count] = tokenUuid(members[i]);
+ unchecked {
+ ++count;
+ }
+ }
+ return count;
+ }
+
+ function _findMaxTierIndex(uint32 countryKey, uint32 adminKey) internal view returns (uint256 maxTierIndex) {
+ uint256 adminTiers = regionTierCount[adminKey];
+ uint256 countryTiers = regionTierCount[countryKey];
+
+ uint256 maxTier = adminTiers > 0 ? adminTiers - 1 : 0;
+ if (countryTiers > 0 && countryTiers - 1 > maxTier) maxTier = countryTiers - 1;
+ return maxTier;
+ }
+
+ function _findCheapestInclusionTier(uint16 countryCode, uint16 adminCode, bool isCountry)
+ internal
+ view
+ returns (uint256)
+ {
+ uint32 countryKey = uint32(countryCode);
+ uint32 adminKey = makeAdminRegion(countryCode, adminCode);
+ uint32 candidateRegion = isCountry ? countryKey : adminKey;
+
+ (, uint256 count, uint256 highestTier, uint256 lowestTier) =
+ _buildHighestBondedUuidBundle(countryKey, adminKey);
+
+ for (uint256 tier = lowestTier; tier <= highestTier; ++tier) {
+ bool tierHasCapacity = _regionTierMembers[candidateRegion][tier].length < TIER_CAPACITY;
+ bool bundleHasRoom = count < MAX_BONDED_UUID_BUNDLE_SIZE;
+
+ if (tierHasCapacity && bundleHasRoom) {
+ return tier;
+ }
+
+ uint256 adminMembers = _regionTierMembers[adminKey][tier].length;
+ uint256 countryMembers = _regionTierMembers[countryKey][tier].length;
+ uint256 tierTotal = adminMembers + countryMembers;
+ count = tierTotal > count ? 0 : count - tierTotal;
+ }
+
+ if (highestTier < MAX_TIERS - 1) {
+ return highestTier + 1;
+ }
+
+ revert MaxTiersReached();
+ }
+
+ function _addToTier(uint256 tokenId, uint32 region, uint256 tier) internal {
+ _regionTierMembers[region][tier].push(tokenId);
+ _indexInTier[tokenId] = _regionTierMembers[region][tier].length - 1;
+
+ if (tier >= regionTierCount[region]) {
+ regionTierCount[region] = tier + 1;
+ }
+ }
+
+ function _removeFromTier(uint256 tokenId, uint32 region, uint256 tier) internal {
+ uint256[] storage members = _regionTierMembers[region][tier];
+ uint256 idx = _indexInTier[tokenId];
+ uint256 lastIdx = members.length - 1;
+
+ if (idx != lastIdx) {
+ uint256 lastTokenId = members[lastIdx];
+ members[idx] = lastTokenId;
+ _indexInTier[lastTokenId] = idx;
+ }
+ members.pop();
+ }
+
+ function _trimTierCount(uint32 region) internal {
+ uint256 tierCount_ = regionTierCount[region];
+ while (tierCount_ > 0 && _regionTierMembers[region][tierCount_ - 1].length == 0) {
+ tierCount_--;
+ }
+ regionTierCount[region] = tierCount_;
+ }
+
+ function _addToRegionIndex(uint32 region) internal {
+ if (_isCountryRegion(region)) {
+ uint16 cc = uint16(region);
+ if (_activeCountryIndex[cc] == 0) {
+ _activeCountries.push(cc);
+ _activeCountryIndex[cc] = _activeCountries.length;
+ }
+ } else {
+ if (_countryAdminAreaIndex[region] == 0) {
+ uint16 cc = _countryFromRegion(region);
+ if (_activeCountryIndex[cc] == 0) {
+ _activeCountries.push(cc);
+ _activeCountryIndex[cc] = _activeCountries.length;
+ }
+ _countryAdminAreas[cc].push(region);
+ _countryAdminAreaIndex[region] = _countryAdminAreas[cc].length;
+ }
+ }
+ }
+
+ function _removeFromRegionIndex(uint32 region) internal {
+ if (regionTierCount[region] > 0) return;
+
+ if (_isCountryRegion(region)) {
+ uint16 cc = uint16(region);
+ uint256 oneIdx = _activeCountryIndex[cc];
+ if (oneIdx > 0) {
+ if (_countryAdminAreas[cc].length > 0) return;
+
+ uint256 lastIdx = _activeCountries.length - 1;
+ uint256 removeIdx = oneIdx - 1;
+ if (removeIdx != lastIdx) {
+ uint16 lastCountryCode = _activeCountries[lastIdx];
+ _activeCountries[removeIdx] = lastCountryCode;
+ _activeCountryIndex[lastCountryCode] = oneIdx;
+ }
+ _activeCountries.pop();
+ delete _activeCountryIndex[cc];
+ }
+ } else {
+ uint256 oneIdx = _countryAdminAreaIndex[region];
+ if (oneIdx > 0) {
+ uint16 cc = _countryFromRegion(region);
+ uint32[] storage countryAreas = _countryAdminAreas[cc];
+ uint256 lastIdx = countryAreas.length - 1;
+ uint256 removeIdx = oneIdx - 1;
+ if (removeIdx != lastIdx) {
+ uint32 lastArea = countryAreas[lastIdx];
+ countryAreas[removeIdx] = lastArea;
+ _countryAdminAreaIndex[lastArea] = oneIdx;
+ }
+ countryAreas.pop();
+ delete _countryAdminAreaIndex[region];
+
+ if (countryAreas.length == 0 && regionTierCount[uint32(cc)] == 0) {
+ uint256 countryOneIdx = _activeCountryIndex[cc];
+ if (countryOneIdx > 0) {
+ uint256 countryLastIdx = _activeCountries.length - 1;
+ uint256 countryRemoveIdx = countryOneIdx - 1;
+ if (countryRemoveIdx != countryLastIdx) {
+ uint16 lastCountryCode = _activeCountries[countryLastIdx];
+ _activeCountries[countryRemoveIdx] = lastCountryCode;
+ _activeCountryIndex[lastCountryCode] = countryOneIdx;
+ }
+ _activeCountries.pop();
+ delete _activeCountryIndex[cc];
+ }
+ }
+ }
+ }
+ }
+
+ // ──────────────────────────────────────────────
+ // Overrides required by ERC721Enumerable
+ // ──────────────────────────────────────────────
+
+ function _update(address to, uint256 tokenId, address auth)
+ internal
+ override(ERC721EnumerableUpgradeable)
+ returns (address)
+ {
+ address from = super._update(to, tokenId, auth);
+
+ uint32 region = tokenRegion(tokenId);
+ if (region == OWNED_REGION_KEY && from != address(0) && to != address(0)) {
+ uuidOwner[tokenUuid(tokenId)] = to;
+ }
+
+ return from;
+ }
+
+ function _increaseBalance(address account, uint128 value) internal override(ERC721EnumerableUpgradeable) {
+ super._increaseBalance(account, value);
+ }
+
+ function supportsInterface(bytes4 interfaceId) public view override(ERC721EnumerableUpgradeable) returns (bool) {
+ return super.supportsInterface(interfaceId);
+ }
+}
diff --git a/src/swarms/ServiceProviderUpgradeable.sol b/src/swarms/ServiceProviderUpgradeable.sol
new file mode 100644
index 00000000..70c3b7d4
--- /dev/null
+++ b/src/swarms/ServiceProviderUpgradeable.sol
@@ -0,0 +1,123 @@
+// SPDX-License-Identifier: BSD-3-Clause-Clear
+
+pragma solidity ^0.8.24;
+
+import {ERC721Upgradeable} from "@openzeppelin/contracts-upgradeable/token/ERC721/ERC721Upgradeable.sol";
+import {Ownable2StepUpgradeable} from "@openzeppelin/contracts-upgradeable/access/Ownable2StepUpgradeable.sol";
+import {UUPSUpgradeable} from "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol";
+import {Initializable} from "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
+
+/**
+ * @title ServiceProviderUpgradeable
+ * @notice UUPS-upgradeable ERC-721 representing ownership of a service endpoint URL.
+ * @dev TokenID = keccak256(url), guaranteeing one owner per URL.
+ *
+ * **Upgrade Pattern:**
+ * - Uses OpenZeppelin UUPS proxy pattern for upgradeability.
+ * - Only the contract owner can authorize upgrades.
+ * - Storage layout must be preserved across upgrades (append-only).
+ *
+ * **Storage Migration:**
+ * - V1 storage is automatically preserved in the proxy.
+ * - Future versions can add new storage variables at the end.
+ * - Use `reinitializer(n)` for version-specific initialization.
+ */
+contract ServiceProviderUpgradeable is
+ Initializable,
+ ERC721Upgradeable,
+ Ownable2StepUpgradeable,
+ UUPSUpgradeable
+{
+ // ──────────────────────────────────────────────
+ // Errors
+ // ──────────────────────────────────────────────
+ error EmptyURL();
+ error NotTokenOwner();
+
+ // ──────────────────────────────────────────────
+ // Storage (V1)
+ // ──────────────────────────────────────────────
+
+ /// @notice Maps TokenID -> Provider URL
+ mapping(uint256 => string) public providerUrls;
+
+ // ──────────────────────────────────────────────
+ // Storage Gap (for future upgrades)
+ // ──────────────────────────────────────────────
+
+ /// @dev Reserved storage slots for future upgrades.
+ /// When adding new storage in V2+, reduce this gap accordingly.
+ /// Example: Adding 1 new storage variable → change to __gap[48]
+ // solhint-disable-next-line var-name-mixedcase
+ uint256[49] private __gap;
+
+ // ──────────────────────────────────────────────
+ // Events
+ // ──────────────────────────────────────────────
+
+ event ProviderRegistered(address indexed owner, string url, uint256 indexed tokenId);
+ event ProviderBurned(address indexed owner, uint256 indexed tokenId);
+
+ // ──────────────────────────────────────────────
+ // Constructor (disables initializers on implementation)
+ // ──────────────────────────────────────────────
+
+ /// @custom:oz-upgrades-unsafe-allow constructor
+ constructor() {
+ _disableInitializers();
+ }
+
+ // ──────────────────────────────────────────────
+ // Initializer (replaces constructor)
+ // ──────────────────────────────────────────────
+
+ /// @notice Initializes the contract. Must be called once via proxy.
+ /// @param owner_ The address that will own this contract and can authorize upgrades.
+ function initialize(address owner_) external initializer {
+ __ERC721_init("Swarm Service Provider", "SSV");
+ __Ownable_init(owner_);
+ __Ownable2Step_init();
+ }
+
+ // ──────────────────────────────────────────────
+ // Core Functions
+ // ──────────────────────────────────────────────
+
+ /// @notice Mints a new provider NFT for the given URL.
+ /// @param url The backend service URL (must be unique).
+ /// @return tokenId The deterministic token ID derived from `url`.
+ function registerProvider(string calldata url) external returns (uint256 tokenId) {
+ if (bytes(url).length == 0) {
+ revert EmptyURL();
+ }
+
+ tokenId = uint256(keccak256(bytes(url)));
+
+ providerUrls[tokenId] = url;
+
+ _mint(msg.sender, tokenId);
+
+ emit ProviderRegistered(msg.sender, url, tokenId);
+ }
+
+ /// @notice Burns the provider NFT. Caller must be the token owner.
+ /// @param tokenId The provider token ID to burn.
+ function burn(uint256 tokenId) external {
+ if (ownerOf(tokenId) != msg.sender) {
+ revert NotTokenOwner();
+ }
+
+ delete providerUrls[tokenId];
+
+ _burn(tokenId);
+
+ emit ProviderBurned(msg.sender, tokenId);
+ }
+
+ // ──────────────────────────────────────────────
+ // UUPS Authorization
+ // ──────────────────────────────────────────────
+
+ /// @dev Only the owner can authorize an upgrade.
+ function _authorizeUpgrade(address newImplementation) internal override onlyOwner {}
+}
diff --git a/src/swarms/SwarmRegistryL1Upgradeable.sol b/src/swarms/SwarmRegistryL1Upgradeable.sol
new file mode 100644
index 00000000..eb5c92b3
--- /dev/null
+++ b/src/swarms/SwarmRegistryL1Upgradeable.sol
@@ -0,0 +1,413 @@
+// SPDX-License-Identifier: BSD-3-Clause-Clear
+
+pragma solidity ^0.8.24;
+
+// NOTE: SSTORE2 is not compatible with ZkSync Era due to EXTCODECOPY limitation.
+// For ZkSync deployment, use SwarmRegistryUniversalUpgradeable instead.
+import {SSTORE2} from "solady/utils/SSTORE2.sol";
+import {Ownable2StepUpgradeable} from "@openzeppelin/contracts-upgradeable/access/Ownable2StepUpgradeable.sol";
+import {UUPSUpgradeable} from "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol";
+import {Initializable} from "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
+import {ReentrancyGuard} from "solady/utils/ReentrancyGuard.sol";
+
+import {IFleetIdentity} from "./interfaces/IFleetIdentity.sol";
+import {IServiceProvider} from "./interfaces/IServiceProvider.sol";
+import {SwarmStatus, TagType, FingerprintSize} from "./interfaces/SwarmTypes.sol";
+
+/**
+ * @title SwarmRegistryL1Upgradeable
+ * @notice UUPS-upgradeable permissionless BLE swarm registry optimized for Ethereum L1 (uses SSTORE2 for filter storage).
+ * @dev Not compatible with ZkSync Era — use SwarmRegistryUniversalUpgradeable instead.
+ *
+ * **Upgrade Pattern:**
+ * - Uses OpenZeppelin UUPS proxy pattern for upgradeability.
+ * - Only the contract owner can authorize upgrades.
+ * - Storage layout must be preserved across upgrades (append-only).
+ *
+ * **Important:** The FleetIdentity and ServiceProvider addresses should point to
+ * **proxy addresses** (stable), not implementation addresses.
+ *
+ * **L1-Only:** This contract uses SSTORE2 which relies on EXTCODECOPY.
+ * Build/test WITHOUT --zksync flag:
+ * ```bash
+ * forge build --match-path src/swarms/SwarmRegistryL1Upgradeable.sol
+ * forge test --match-path test/SwarmRegistryL1Upgradeable.t.sol
+ * ```
+ */
+contract SwarmRegistryL1Upgradeable is Initializable, Ownable2StepUpgradeable, UUPSUpgradeable, ReentrancyGuard {
+ // ──────────────────────────────────────────────
+ // Errors
+ // ──────────────────────────────────────────────
+ error InvalidFilterSize();
+ error InvalidUuid();
+ error NotUuidOwner();
+ error ProviderDoesNotExist();
+ error NotProviderOwner();
+ error SwarmNotFound();
+ error InvalidSwarmData();
+ error SwarmAlreadyExists();
+ error SwarmNotOrphaned();
+ error SwarmOrphaned();
+
+ // ──────────────────────────────────────────────
+ // Structs (L1-specific: uses SSTORE2 pointer)
+ // ──────────────────────────────────────────────
+
+ struct Swarm {
+ bytes16 fleetUuid;
+ uint256 providerId;
+ address filterPointer; // SSTORE2 pointer
+ FingerprintSize fpSize;
+ TagType tagType;
+ SwarmStatus status;
+ }
+
+ // ──────────────────────────────────────────────
+ // Constants
+ // ──────────────────────────────────────────────
+
+ // ──────────────────────────────────────────────
+ // Storage (V1) - Order matters for upgrades!
+ // ──────────────────────────────────────────────
+
+ /// @notice The FleetIdentity contract (proxy address).
+ /// @dev In non-upgradeable version this was immutable.
+ IFleetIdentity private _fleetContract;
+
+ /// @notice The ServiceProvider contract (proxy address).
+ /// @dev In non-upgradeable version this was immutable.
+ IServiceProvider private _providerContract;
+
+ /// @notice SwarmID -> Swarm metadata
+ mapping(uint256 => Swarm) public swarms;
+
+ /// @notice UUID -> List of SwarmIDs
+ mapping(bytes16 => uint256[]) public uuidSwarms;
+
+ /// @notice SwarmID -> index in uuidSwarms[fleetUuid] (for O(1) removal)
+ mapping(uint256 => uint256) public swarmIndexInUuid;
+
+ // ──────────────────────────────────────────────
+ // Storage Gap (for future upgrades)
+ // ──────────────────────────────────────────────
+
+ /// @dev Reserved storage slots for future upgrades.
+ // solhint-disable-next-line var-name-mixedcase
+ uint256[45] private __gap;
+
+ // ──────────────────────────────────────────────
+ // Events
+ // ──────────────────────────────────────────────
+
+ event SwarmRegistered(uint256 indexed swarmId, bytes16 indexed fleetUuid, uint256 indexed providerId, address owner);
+ event SwarmStatusChanged(uint256 indexed swarmId, SwarmStatus status);
+ event SwarmProviderUpdated(uint256 indexed swarmId, uint256 indexed oldProvider, uint256 indexed newProvider);
+ event SwarmDeleted(uint256 indexed swarmId, bytes16 indexed fleetUuid, address indexed owner);
+ event SwarmPurged(uint256 indexed swarmId, bytes16 indexed fleetUuid, address indexed purgedBy);
+
+ // ──────────────────────────────────────────────
+ // Constructor (disables initializers on implementation)
+ // ──────────────────────────────────────────────
+
+ /// @custom:oz-upgrades-unsafe-allow constructor
+ constructor() {
+ _disableInitializers();
+ }
+
+ // ──────────────────────────────────────────────
+ // Initializer
+ // ──────────────────────────────────────────────
+
+ /// @notice Initializes the contract. Must be called once via proxy.
+ /// @param fleetContract_ Address of the FleetIdentity proxy contract.
+ /// @param providerContract_ Address of the ServiceProvider proxy contract.
+ /// @param owner_ The address that will own this contract and can authorize upgrades.
+ function initialize(address fleetContract_, address providerContract_, address owner_) external initializer {
+ if (fleetContract_ == address(0) || providerContract_ == address(0)) {
+ revert InvalidSwarmData();
+ }
+
+ __Ownable_init(owner_);
+ __Ownable2Step_init();
+
+ _fleetContract = IFleetIdentity(fleetContract_);
+ _providerContract = IServiceProvider(providerContract_);
+ }
+
+ // ──────────────────────────────────────────────
+ // Public Getters for former immutables
+ // ──────────────────────────────────────────────
+
+ /// @notice Returns the FleetIdentity contract address.
+ function FLEET_CONTRACT() external view returns (IFleetIdentity) {
+ return _fleetContract;
+ }
+
+ /// @notice Returns the ServiceProvider contract address.
+ function PROVIDER_CONTRACT() external view returns (IServiceProvider) {
+ return _providerContract;
+ }
+
+ // ──────────────────────────────────────────────
+ // Pure Functions
+ // ──────────────────────────────────────────────
+
+ /// @notice Derives a deterministic swarm ID.
+ function computeSwarmId(bytes16 fleetUuid, bytes calldata filterData_, FingerprintSize fpSize, TagType tagType)
+ public
+ pure
+ returns (uint256)
+ {
+ return uint256(keccak256(abi.encode(fleetUuid, filterData_, fpSize, tagType)));
+ }
+
+ // ──────────────────────────────────────────────
+ // Core Functions
+ // ──────────────────────────────────────────────
+
+ /// @notice Registers a new swarm. Caller must own the fleet UUID.
+ /// @param fleetUuid The fleet UUID (must be owned by caller)
+ /// @param providerId The service provider NFT ID
+ /// @param filterData_ The XOR filter data
+ /// @param fpSize Fingerprint size (BITS_8 or BITS_16)
+ /// @param tagType The tag type for this swarm
+ function registerSwarm(
+ bytes16 fleetUuid,
+ uint256 providerId,
+ bytes calldata filterData_,
+ FingerprintSize fpSize,
+ TagType tagType
+ ) external nonReentrant returns (uint256 swarmId) {
+ if (fleetUuid == bytes16(0)) {
+ revert InvalidUuid();
+ }
+ if (filterData_.length == 0 || filterData_.length > 24576) {
+ revert InvalidFilterSize();
+ }
+
+ if (_fleetContract.uuidOwner(fleetUuid) != msg.sender) {
+ revert NotUuidOwner();
+ }
+ try _providerContract.ownerOf(providerId) returns (address) {}
+ catch {
+ revert ProviderDoesNotExist();
+ }
+
+ swarmId = computeSwarmId(fleetUuid, filterData_, fpSize, tagType);
+
+ if (swarms[swarmId].filterPointer != address(0)) {
+ revert SwarmAlreadyExists();
+ }
+
+ Swarm storage s = swarms[swarmId];
+ s.fleetUuid = fleetUuid;
+ s.providerId = providerId;
+ s.fpSize = fpSize;
+ s.tagType = tagType;
+ s.status = SwarmStatus.REGISTERED;
+
+ uuidSwarms[fleetUuid].push(swarmId);
+ swarmIndexInUuid[swarmId] = uuidSwarms[fleetUuid].length - 1;
+
+ s.filterPointer = SSTORE2.write(filterData_);
+
+ emit SwarmRegistered(swarmId, fleetUuid, providerId, msg.sender);
+ }
+
+ /// @notice Approves a swarm. Caller must own the provider NFT.
+ function acceptSwarm(uint256 swarmId) external {
+ Swarm storage s = swarms[swarmId];
+ if (s.filterPointer == address(0)) revert SwarmNotFound();
+
+ (bool fleetValid, bool providerValid) = isSwarmValid(swarmId);
+ if (!fleetValid || !providerValid) revert SwarmOrphaned();
+
+ if (_providerContract.ownerOf(s.providerId) != msg.sender) {
+ revert NotProviderOwner();
+ }
+ s.status = SwarmStatus.ACCEPTED;
+ emit SwarmStatusChanged(swarmId, SwarmStatus.ACCEPTED);
+ }
+
+ /// @notice Rejects a swarm. Caller must own the provider NFT.
+ function rejectSwarm(uint256 swarmId) external {
+ Swarm storage s = swarms[swarmId];
+ if (s.filterPointer == address(0)) revert SwarmNotFound();
+
+ (bool fleetValid, bool providerValid) = isSwarmValid(swarmId);
+ if (!fleetValid || !providerValid) revert SwarmOrphaned();
+
+ if (_providerContract.ownerOf(s.providerId) != msg.sender) {
+ revert NotProviderOwner();
+ }
+ s.status = SwarmStatus.REJECTED;
+ emit SwarmStatusChanged(swarmId, SwarmStatus.REJECTED);
+ }
+
+ /// @notice Reassigns the service provider. Resets status to REGISTERED.
+ function updateSwarmProvider(uint256 swarmId, uint256 newProviderId) external {
+ Swarm storage s = swarms[swarmId];
+ if (s.filterPointer == address(0)) {
+ revert SwarmNotFound();
+ }
+ if (_fleetContract.uuidOwner(s.fleetUuid) != msg.sender) {
+ revert NotUuidOwner();
+ }
+ try _providerContract.ownerOf(newProviderId) returns (address) {}
+ catch {
+ revert ProviderDoesNotExist();
+ }
+
+ uint256 oldProvider = s.providerId;
+
+ s.providerId = newProviderId;
+ s.status = SwarmStatus.REGISTERED;
+
+ emit SwarmProviderUpdated(swarmId, oldProvider, newProviderId);
+ }
+
+ /// @notice Permanently deletes a swarm. Caller must own the fleet UUID.
+ function deleteSwarm(uint256 swarmId) external {
+ Swarm storage s = swarms[swarmId];
+ if (s.filterPointer == address(0)) {
+ revert SwarmNotFound();
+ }
+ if (_fleetContract.uuidOwner(s.fleetUuid) != msg.sender) {
+ revert NotUuidOwner();
+ }
+
+ bytes16 fleetUuid = s.fleetUuid;
+
+ _removeFromUuidSwarms(fleetUuid, swarmId);
+
+ delete swarms[swarmId];
+
+ emit SwarmDeleted(swarmId, fleetUuid, msg.sender);
+ }
+
+ /// @notice Returns whether the swarm's fleet UUID and provider NFT are still valid.
+ function isSwarmValid(uint256 swarmId) public view returns (bool fleetValid, bool providerValid) {
+ Swarm storage s = swarms[swarmId];
+ if (s.filterPointer == address(0)) revert SwarmNotFound();
+
+ fleetValid = _fleetContract.uuidOwner(s.fleetUuid) != address(0);
+
+ try _providerContract.ownerOf(s.providerId) returns (address) {
+ providerValid = true;
+ } catch {
+ providerValid = false;
+ }
+ }
+
+ /// @notice Permissionless-ly removes an orphaned swarm.
+ function purgeOrphanedSwarm(uint256 swarmId) external {
+ Swarm storage s = swarms[swarmId];
+ if (s.filterPointer == address(0)) revert SwarmNotFound();
+
+ (bool fleetValid, bool providerValid) = isSwarmValid(swarmId);
+ if (fleetValid && providerValid) revert SwarmNotOrphaned();
+
+ bytes16 fleetUuid = s.fleetUuid;
+
+ _removeFromUuidSwarms(fleetUuid, swarmId);
+
+ delete swarms[swarmId];
+
+ emit SwarmPurged(swarmId, fleetUuid, msg.sender);
+ }
+
+ /// @notice Returns the raw XOR filter bytes for a swarm.
+ function getFilterData(uint256 swarmId) external view returns (bytes memory) {
+ Swarm storage s = swarms[swarmId];
+ if (s.filterPointer == address(0)) {
+ revert SwarmNotFound();
+ }
+ return SSTORE2.read(s.filterPointer);
+ }
+
+ /// @notice Tests tag membership against the swarm's XOR filter.
+ function checkMembership(uint256 swarmId, bytes32 tagHash) external view returns (bool isValid) {
+ Swarm storage s = swarms[swarmId];
+ if (s.filterPointer == address(0)) {
+ revert SwarmNotFound();
+ }
+
+ (bool fleetValid, bool providerValid) = isSwarmValid(swarmId);
+ if (!fleetValid || !providerValid) revert SwarmOrphaned();
+
+ uint256 dataLen;
+ address pointer = s.filterPointer;
+ assembly {
+ dataLen := extcodesize(pointer)
+ }
+
+ // SSTORE2 adds 1 byte overhead (0x00)
+ if (dataLen > 0) {
+ unchecked {
+ --dataLen;
+ }
+ }
+
+ // For BITS_8: m = dataLen (each byte is one fingerprint)
+ // For BITS_16: m = dataLen / 2 (each 2 bytes is one fingerprint)
+ uint256 m = s.fpSize == FingerprintSize.BITS_8 ? dataLen : dataLen >> 1;
+ if (m == 0) return false;
+
+ bytes32 h = tagHash;
+
+ uint32 h1 = uint32(uint256(h)) % uint32(m);
+ uint32 h2 = uint32(uint256(h) >> 32) % uint32(m);
+ uint32 h3 = uint32(uint256(h) >> 64) % uint32(m);
+
+ // fpMask: 0xFF for BITS_8, 0xFFFF for BITS_16
+ uint256 fpMask = s.fpSize == FingerprintSize.BITS_8 ? 0xFF : 0xFFFF;
+ uint256 expectedFp = (uint256(h) >> 96) & fpMask;
+
+ uint256 f1 = _readFingerprint(pointer, h1, s.fpSize);
+ uint256 f2 = _readFingerprint(pointer, h2, s.fpSize);
+ uint256 f3 = _readFingerprint(pointer, h3, s.fpSize);
+
+ return (f1 ^ f2 ^ f3) == expectedFp;
+ }
+
+ // ──────────────────────────────────────────────
+ // UUPS Authorization
+ // ──────────────────────────────────────────────
+
+ /// @dev Only the owner can authorize an upgrade.
+ function _authorizeUpgrade(address newImplementation) internal override onlyOwner {}
+
+ // ──────────────────────────────────────────────
+ // Internal Functions
+ // ──────────────────────────────────────────────
+
+ function _removeFromUuidSwarms(bytes16 fleetUuid, uint256 swarmId) internal {
+ uint256[] storage arr = uuidSwarms[fleetUuid];
+ uint256 index = swarmIndexInUuid[swarmId];
+ uint256 lastId = arr[arr.length - 1];
+
+ arr[index] = lastId;
+ swarmIndexInUuid[lastId] = index;
+ arr.pop();
+ delete swarmIndexInUuid[swarmId];
+ }
+
+ /// @dev Reads a fingerprint from the SSTORE2 filter at the given index.
+ /// Optimized for 8-bit and 16-bit fingerprints (no loops, no variable shifts).
+ function _readFingerprint(address pointer, uint256 index, FingerprintSize fpSize)
+ internal
+ view
+ returns (uint256)
+ {
+ if (fpSize == FingerprintSize.BITS_8) {
+ // 8-bit: read single byte
+ bytes memory chunk = SSTORE2.read(pointer, index, index + 1);
+ return uint256(uint8(chunk[0]));
+ } else {
+ // 16-bit: read two consecutive bytes
+ uint256 byteIndex = index << 1; // index * 2
+ bytes memory chunk = SSTORE2.read(pointer, byteIndex, byteIndex + 2);
+ return (uint256(uint8(chunk[0])) << 8) | uint256(uint8(chunk[1]));
+ }
+ }
+}
diff --git a/src/swarms/SwarmRegistryUniversalUpgradeable.sol b/src/swarms/SwarmRegistryUniversalUpgradeable.sol
new file mode 100644
index 00000000..381a9330
--- /dev/null
+++ b/src/swarms/SwarmRegistryUniversalUpgradeable.sol
@@ -0,0 +1,416 @@
+// SPDX-License-Identifier: BSD-3-Clause-Clear
+
+pragma solidity ^0.8.24;
+
+import {Ownable2StepUpgradeable} from "@openzeppelin/contracts-upgradeable/access/Ownable2StepUpgradeable.sol";
+import {UUPSUpgradeable} from "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol";
+import {Initializable} from "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
+import {ReentrancyGuard} from "solady/utils/ReentrancyGuard.sol";
+
+import {IFleetIdentity} from "./interfaces/IFleetIdentity.sol";
+import {IServiceProvider} from "./interfaces/IServiceProvider.sol";
+import {SwarmStatus, TagType, FingerprintSize} from "./interfaces/SwarmTypes.sol";
+
+/**
+ * @title SwarmRegistryUniversalUpgradeable
+ * @notice UUPS-upgradeable permissionless BLE swarm registry compatible with all EVM chains (including ZkSync Era).
+ * @dev Uses native `bytes` storage for cross-chain compatibility.
+ *
+ * **Upgrade Pattern:**
+ * - Uses OpenZeppelin UUPS proxy pattern for upgradeability.
+ * - Only the contract owner can authorize upgrades.
+ * - Storage layout must be preserved across upgrades (append-only).
+ *
+ * **Important:** The FleetIdentity and ServiceProvider addresses should point to
+ * **proxy addresses** (stable), not implementation addresses.
+ *
+ * **Storage Migration Example (V1 → V2):**
+ * ```solidity
+ * function initializeV2(uint256 newParam) external reinitializer(2) {
+ * _newParamIntroducedInV2 = newParam;
+ * }
+ * ```
+ */
+contract SwarmRegistryUniversalUpgradeable is
+ Initializable,
+ Ownable2StepUpgradeable,
+ UUPSUpgradeable,
+ ReentrancyGuard
+{
+ // ──────────────────────────────────────────────
+ // Errors
+ // ──────────────────────────────────────────────
+ error InvalidFilterSize();
+ error InvalidUuid();
+ error NotUuidOwner();
+ error ProviderDoesNotExist();
+ error NotProviderOwner();
+ error SwarmNotFound();
+ error InvalidSwarmData();
+ error FilterTooLarge();
+ error SwarmAlreadyExists();
+ error SwarmNotOrphaned();
+ error SwarmOrphaned();
+
+ // ──────────────────────────────────────────────
+ // Structs (Universal-specific: uses native bytes storage)
+ // ──────────────────────────────────────────────
+
+ struct Swarm {
+ bytes16 fleetUuid;
+ uint256 providerId;
+ uint32 filterLength;
+ FingerprintSize fpSize;
+ TagType tagType;
+ SwarmStatus status;
+ }
+
+ // ──────────────────────────────────────────────
+ // Constants
+ // ──────────────────────────────────────────────
+
+ /// @notice Maximum filter size per swarm (24KB)
+ uint32 public constant MAX_FILTER_SIZE = 24576;
+
+ // ──────────────────────────────────────────────
+ // Storage (V1) - Order matters for upgrades!
+ // ──────────────────────────────────────────────
+
+ /// @notice The FleetIdentity contract (proxy address).
+ /// @dev In non-upgradeable version this was immutable.
+ IFleetIdentity private _fleetContract;
+
+ /// @notice The ServiceProvider contract (proxy address).
+ /// @dev In non-upgradeable version this was immutable.
+ IServiceProvider private _providerContract;
+
+ /// @notice SwarmID -> Swarm metadata
+ mapping(uint256 => Swarm) public swarms;
+
+ /// @notice SwarmID -> XOR filter data (stored as bytes)
+ mapping(uint256 => bytes) internal filterData;
+
+ /// @notice UUID -> List of SwarmIDs
+ mapping(bytes16 => uint256[]) public uuidSwarms;
+
+ /// @notice SwarmID -> index in uuidSwarms[fleetUuid] (for O(1) removal)
+ mapping(uint256 => uint256) public swarmIndexInUuid;
+
+ // ──────────────────────────────────────────────
+ // Storage Gap (for future upgrades)
+ // ──────────────────────────────────────────────
+
+ /// @dev Reserved storage slots for future upgrades.
+ // solhint-disable-next-line var-name-mixedcase
+ uint256[44] private __gap;
+
+ // ──────────────────────────────────────────────
+ // Events
+ // ──────────────────────────────────────────────
+
+ /// @dev SwarmRegistered with filterSize param (Universal-specific).
+ event SwarmRegistered(
+ uint256 indexed swarmId, bytes16 indexed fleetUuid, uint256 indexed providerId, address owner, uint32 filterSize
+ );
+ event SwarmStatusChanged(uint256 indexed swarmId, SwarmStatus status);
+ event SwarmProviderUpdated(uint256 indexed swarmId, uint256 indexed oldProvider, uint256 indexed newProvider);
+ event SwarmDeleted(uint256 indexed swarmId, bytes16 indexed fleetUuid, address indexed owner);
+ event SwarmPurged(uint256 indexed swarmId, bytes16 indexed fleetUuid, address indexed purgedBy);
+
+ // ──────────────────────────────────────────────
+ // Constructor (disables initializers on implementation)
+ // ──────────────────────────────────────────────
+
+ /// @custom:oz-upgrades-unsafe-allow constructor
+ constructor() {
+ _disableInitializers();
+ }
+
+ // ──────────────────────────────────────────────
+ // Initializer
+ // ──────────────────────────────────────────────
+
+ /// @notice Initializes the contract. Must be called once via proxy.
+ /// @param fleetContract_ Address of the FleetIdentity proxy contract.
+ /// @param providerContract_ Address of the ServiceProvider proxy contract.
+ /// @param owner_ The address that will own this contract and can authorize upgrades.
+ function initialize(address fleetContract_, address providerContract_, address owner_) external initializer {
+ if (fleetContract_ == address(0) || providerContract_ == address(0)) {
+ revert InvalidSwarmData();
+ }
+
+ __Ownable_init(owner_);
+ __Ownable2Step_init();
+
+ _fleetContract = IFleetIdentity(fleetContract_);
+ _providerContract = IServiceProvider(providerContract_);
+ }
+
+ // ──────────────────────────────────────────────
+ // Public Getters for former immutables
+ // ──────────────────────────────────────────────
+
+ /// @notice Returns the FleetIdentity contract address.
+ function FLEET_CONTRACT() external view returns (IFleetIdentity) {
+ return _fleetContract;
+ }
+
+ /// @notice Returns the ServiceProvider contract address.
+ function PROVIDER_CONTRACT() external view returns (IServiceProvider) {
+ return _providerContract;
+ }
+
+ // ──────────────────────────────────────────────
+ // Pure Functions
+ // ──────────────────────────────────────────────
+
+ /// @notice Derives a deterministic swarm ID.
+ function computeSwarmId(bytes16 fleetUuid, bytes calldata filter, FingerprintSize fpSize, TagType tagType)
+ public
+ pure
+ returns (uint256)
+ {
+ return uint256(keccak256(abi.encode(fleetUuid, filter, fpSize, tagType)));
+ }
+
+ // ──────────────────────────────────────────────
+ // Core Functions
+ // ──────────────────────────────────────────────
+
+ /// @notice Registers a new swarm. Caller must own the fleet UUID.
+ /// @param fleetUuid The fleet UUID (must be owned by caller)
+ /// @param providerId The service provider NFT ID
+ /// @param filter The XOR filter data
+ /// @param fpSize Fingerprint size (BITS_8 or BITS_16)
+ /// @param tagType The tag type for this swarm
+ function registerSwarm(
+ bytes16 fleetUuid,
+ uint256 providerId,
+ bytes calldata filter,
+ FingerprintSize fpSize,
+ TagType tagType
+ ) external nonReentrant returns (uint256 swarmId) {
+ if (fleetUuid == bytes16(0)) {
+ revert InvalidUuid();
+ }
+ if (filter.length == 0) {
+ revert InvalidFilterSize();
+ }
+ if (filter.length > MAX_FILTER_SIZE) {
+ revert FilterTooLarge();
+ }
+
+ if (_fleetContract.uuidOwner(fleetUuid) != msg.sender) {
+ revert NotUuidOwner();
+ }
+ try _providerContract.ownerOf(providerId) returns (address) {}
+ catch {
+ revert ProviderDoesNotExist();
+ }
+
+ swarmId = computeSwarmId(fleetUuid, filter, fpSize, tagType);
+
+ if (swarms[swarmId].filterLength != 0) {
+ revert SwarmAlreadyExists();
+ }
+
+ Swarm storage s = swarms[swarmId];
+ s.fleetUuid = fleetUuid;
+ s.providerId = providerId;
+ s.filterLength = uint32(filter.length);
+ s.fpSize = fpSize;
+ s.tagType = tagType;
+ s.status = SwarmStatus.REGISTERED;
+
+ filterData[swarmId] = filter;
+
+ uuidSwarms[fleetUuid].push(swarmId);
+ swarmIndexInUuid[swarmId] = uuidSwarms[fleetUuid].length - 1;
+
+ emit SwarmRegistered(swarmId, fleetUuid, providerId, msg.sender, uint32(filter.length));
+ }
+
+ /// @notice Approves a swarm. Caller must own the provider NFT.
+ function acceptSwarm(uint256 swarmId) external {
+ Swarm storage s = swarms[swarmId];
+ if (s.filterLength == 0) revert SwarmNotFound();
+
+ (bool fleetValid, bool providerValid) = isSwarmValid(swarmId);
+ if (!fleetValid || !providerValid) revert SwarmOrphaned();
+
+ if (_providerContract.ownerOf(s.providerId) != msg.sender) {
+ revert NotProviderOwner();
+ }
+ s.status = SwarmStatus.ACCEPTED;
+ emit SwarmStatusChanged(swarmId, SwarmStatus.ACCEPTED);
+ }
+
+ /// @notice Rejects a swarm. Caller must own the provider NFT.
+ function rejectSwarm(uint256 swarmId) external {
+ Swarm storage s = swarms[swarmId];
+ if (s.filterLength == 0) revert SwarmNotFound();
+
+ (bool fleetValid, bool providerValid) = isSwarmValid(swarmId);
+ if (!fleetValid || !providerValid) revert SwarmOrphaned();
+
+ if (_providerContract.ownerOf(s.providerId) != msg.sender) {
+ revert NotProviderOwner();
+ }
+ s.status = SwarmStatus.REJECTED;
+ emit SwarmStatusChanged(swarmId, SwarmStatus.REJECTED);
+ }
+
+ /// @notice Reassigns the service provider. Resets status to REGISTERED.
+ function updateSwarmProvider(uint256 swarmId, uint256 newProviderId) external {
+ Swarm storage s = swarms[swarmId];
+ if (s.filterLength == 0) {
+ revert SwarmNotFound();
+ }
+ if (_fleetContract.uuidOwner(s.fleetUuid) != msg.sender) {
+ revert NotUuidOwner();
+ }
+ try _providerContract.ownerOf(newProviderId) returns (address) {}
+ catch {
+ revert ProviderDoesNotExist();
+ }
+
+ uint256 oldProvider = s.providerId;
+
+ s.providerId = newProviderId;
+ s.status = SwarmStatus.REGISTERED;
+
+ emit SwarmProviderUpdated(swarmId, oldProvider, newProviderId);
+ }
+
+ /// @notice Permanently deletes a swarm. Caller must own the fleet UUID.
+ function deleteSwarm(uint256 swarmId) external {
+ Swarm storage s = swarms[swarmId];
+ if (s.filterLength == 0) {
+ revert SwarmNotFound();
+ }
+ if (_fleetContract.uuidOwner(s.fleetUuid) != msg.sender) {
+ revert NotUuidOwner();
+ }
+
+ bytes16 fleetUuid = s.fleetUuid;
+
+ _removeFromUuidSwarms(fleetUuid, swarmId);
+
+ delete swarms[swarmId];
+ delete filterData[swarmId];
+
+ emit SwarmDeleted(swarmId, fleetUuid, msg.sender);
+ }
+
+ /// @notice Returns whether the swarm's fleet UUID and provider NFT are still valid.
+ function isSwarmValid(uint256 swarmId) public view returns (bool fleetValid, bool providerValid) {
+ Swarm storage s = swarms[swarmId];
+ if (s.filterLength == 0) revert SwarmNotFound();
+
+ fleetValid = _fleetContract.uuidOwner(s.fleetUuid) != address(0);
+
+ try _providerContract.ownerOf(s.providerId) returns (address) {
+ providerValid = true;
+ } catch {
+ providerValid = false;
+ }
+ }
+
+ /// @notice Permissionless-ly removes an orphaned swarm.
+ function purgeOrphanedSwarm(uint256 swarmId) external {
+ Swarm storage s = swarms[swarmId];
+ if (s.filterLength == 0) revert SwarmNotFound();
+
+ (bool fleetValid, bool providerValid) = isSwarmValid(swarmId);
+ if (fleetValid && providerValid) revert SwarmNotOrphaned();
+
+ bytes16 fleetUuid = s.fleetUuid;
+
+ _removeFromUuidSwarms(fleetUuid, swarmId);
+
+ delete swarms[swarmId];
+ delete filterData[swarmId];
+
+ emit SwarmPurged(swarmId, fleetUuid, msg.sender);
+ }
+
+ /// @notice Tests tag membership against the swarm's XOR filter.
+ function checkMembership(uint256 swarmId, bytes32 tagHash) external view returns (bool isValid) {
+ Swarm storage s = swarms[swarmId];
+ if (s.filterLength == 0) {
+ revert SwarmNotFound();
+ }
+
+ (bool fleetValid, bool providerValid) = isSwarmValid(swarmId);
+ if (!fleetValid || !providerValid) revert SwarmOrphaned();
+
+ bytes storage filter = filterData[swarmId];
+ uint256 dataLen = s.filterLength;
+
+ // For BITS_8: m = dataLen (each byte is one fingerprint)
+ // For BITS_16: m = dataLen / 2 (each 2 bytes is one fingerprint)
+ uint256 m = s.fpSize == FingerprintSize.BITS_8 ? dataLen : dataLen >> 1;
+ if (m == 0) return false;
+
+ uint32 h1 = uint32(uint256(tagHash)) % uint32(m);
+ uint32 h2 = uint32(uint256(tagHash) >> 32) % uint32(m);
+ uint32 h3 = uint32(uint256(tagHash) >> 64) % uint32(m);
+
+ // fpMask: 0xFF for BITS_8, 0xFFFF for BITS_16
+ uint256 fpMask = s.fpSize == FingerprintSize.BITS_8 ? 0xFF : 0xFFFF;
+ uint256 expectedFp = (uint256(tagHash) >> 96) & fpMask;
+
+ uint256 f1 = _readFingerprint(filter, h1, s.fpSize);
+ uint256 f2 = _readFingerprint(filter, h2, s.fpSize);
+ uint256 f3 = _readFingerprint(filter, h3, s.fpSize);
+
+ return (f1 ^ f2 ^ f3) == expectedFp;
+ }
+
+ /// @notice Returns the raw XOR filter bytes for a swarm.
+ function getFilterData(uint256 swarmId) external view returns (bytes memory filter) {
+ if (swarms[swarmId].filterLength == 0) {
+ revert SwarmNotFound();
+ }
+ return filterData[swarmId];
+ }
+
+ // ──────────────────────────────────────────────
+ // UUPS Authorization
+ // ──────────────────────────────────────────────
+
+ /// @dev Only the owner can authorize an upgrade.
+ function _authorizeUpgrade(address newImplementation) internal override onlyOwner {}
+
+ // ──────────────────────────────────────────────
+ // Internal Functions
+ // ──────────────────────────────────────────────
+
+ function _removeFromUuidSwarms(bytes16 fleetUuid, uint256 swarmId) internal {
+ uint256[] storage arr = uuidSwarms[fleetUuid];
+ uint256 index = swarmIndexInUuid[swarmId];
+ uint256 lastId = arr[arr.length - 1];
+
+ arr[index] = lastId;
+ swarmIndexInUuid[lastId] = index;
+ arr.pop();
+ delete swarmIndexInUuid[swarmId];
+ }
+
+ /// @dev Reads a fingerprint from the filter at the given index.
+ /// Optimized for 8-bit and 16-bit fingerprints (no loops, no variable shifts).
+ function _readFingerprint(bytes storage filter, uint256 index, FingerprintSize fpSize)
+ internal
+ view
+ returns (uint256)
+ {
+ if (fpSize == FingerprintSize.BITS_8) {
+ // 8-bit: direct byte access
+ return uint256(uint8(filter[index]));
+ } else {
+ // 16-bit: two consecutive bytes
+ uint256 byteIndex = index << 1; // index * 2
+ return (uint256(uint8(filter[byteIndex])) << 8) | uint256(uint8(filter[byteIndex + 1]));
+ }
+ }
+}
diff --git a/src/swarms/doc/README.md b/src/swarms/doc/README.md
new file mode 100644
index 00000000..030950a7
--- /dev/null
+++ b/src/swarms/doc/README.md
@@ -0,0 +1,20 @@
+# Swarm System — Documentation
+
+BLE tag registry enabling decentralized device discovery using cryptographic membership proofs.
+
+## Contents
+
+| Document | Description |
+| ------------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------- |
+| [spec/swarm-specification.md](spec/swarm-specification.md) | Full technical specification (data model, registration, economics, operations, lifecycle, discovery, maintenance, upgrades) |
+| [spec/swarm-specification.pdf](spec/swarm-specification.pdf) | PDF build of the specification with rendered diagrams |
+| [upgradeable-contracts.md](upgradeable-contracts.md) | Operational guide — TypeScript/ethers.js integration, Cast CLI, upgrade & rollback procedures |
+| [iso3166-2/](iso3166-2/) | Per-country administrative-area mapping tables (ISO 3166-2 → `adminIndex`) |
+
+## Building the PDF
+
+```bash
+cd spec && bash build.sh
+```
+
+Requires `@mermaid-js/mermaid-cli` and `md-to-pdf` (install via `npm i` from repo root).
diff --git a/src/swarms/doc/iso3166-2/036-Australia.md b/src/swarms/doc/iso3166-2/036-Australia.md
new file mode 100644
index 00000000..1e00f105
--- /dev/null
+++ b/src/swarms/doc/iso3166-2/036-Australia.md
@@ -0,0 +1,18 @@
+# Australia (036)
+
+ISO 3166-1 numeric: **036**
+
+## Admin Area Mappings
+
+| Admin Code | ISO 3166-2 | Name |
+|-------------|------------|------|
+| 1 | ACT | Australian Capital Territory |
+| 2 | NSW | New South Wales |
+| 3 | NT | Northern Territory |
+| 4 | QLD | Queensland |
+| 5 | SA | South Australia |
+| 6 | TAS | Tasmania |
+| 7 | VIC | Victoria |
+| 8 | WA | Western Australia |
+
+**Total subdivisions:** 8
diff --git a/src/swarms/doc/iso3166-2/076-Brazil.md b/src/swarms/doc/iso3166-2/076-Brazil.md
new file mode 100644
index 00000000..665733d7
--- /dev/null
+++ b/src/swarms/doc/iso3166-2/076-Brazil.md
@@ -0,0 +1,37 @@
+# Brazil (076)
+
+ISO 3166-1 numeric: **076**
+
+## Admin Area Mappings
+
+| Admin Code | ISO 3166-2 | Name |
+|-------------|------------|------|
+| 1 | AC | Acre |
+| 2 | AL | Alagoas |
+| 3 | AP | Amapá |
+| 4 | AM | Amazonas |
+| 5 | BA | Bahia |
+| 6 | CE | Ceará |
+| 7 | DF | Federal District |
+| 8 | ES | Espírito Santo |
+| 9 | GO | Goiás |
+| 10 | MA | Maranhão |
+| 11 | MT | Mato Grosso |
+| 12 | MS | Mato Grosso do Sul |
+| 13 | MG | Minas Gerais |
+| 14 | PA | Pará |
+| 15 | PB | Paraíba |
+| 16 | PR | Paraná |
+| 17 | PE | Pernambuco |
+| 18 | PI | Piauí |
+| 19 | RJ | Rio de Janeiro |
+| 20 | RN | Rio Grande do Norte |
+| 21 | RS | Rio Grande do Sul |
+| 22 | RO | Rondônia |
+| 23 | RR | Roraima |
+| 24 | SC | Santa Catarina |
+| 25 | SP | São Paulo |
+| 26 | SE | Sergipe |
+| 27 | TO | Tocantins |
+
+**Total subdivisions:** 27
diff --git a/src/swarms/doc/iso3166-2/124-Canada.md b/src/swarms/doc/iso3166-2/124-Canada.md
new file mode 100644
index 00000000..f55ed1e8
--- /dev/null
+++ b/src/swarms/doc/iso3166-2/124-Canada.md
@@ -0,0 +1,23 @@
+# Canada (124)
+
+ISO 3166-1 numeric: **124**
+
+## Admin Area Mappings
+
+| Admin Code | ISO 3166-2 | Name |
+|-------------|------------|------|
+| 1 | AB | Alberta |
+| 2 | BC | British Columbia |
+| 3 | MB | Manitoba |
+| 4 | NB | New Brunswick |
+| 5 | NL | Newfoundland and Labrador |
+| 6 | NT | Northwest Territories |
+| 7 | NS | Nova Scotia |
+| 8 | NU | Nunavut |
+| 9 | ON | Ontario |
+| 10 | PE | Prince Edward Island |
+| 11 | QC | Quebec |
+| 12 | SK | Saskatchewan |
+| 13 | YT | Yukon |
+
+**Total subdivisions:** 13
diff --git a/src/swarms/doc/iso3166-2/156-China.md b/src/swarms/doc/iso3166-2/156-China.md
new file mode 100644
index 00000000..c934c1cc
--- /dev/null
+++ b/src/swarms/doc/iso3166-2/156-China.md
@@ -0,0 +1,44 @@
+# China (156)
+
+ISO 3166-1 numeric: **156**
+
+## Admin Area Mappings
+
+| Admin Code | ISO 3166-2 | Name |
+|-------------|------------|------|
+| 1 | AH | Anhui |
+| 2 | BJ | Beijing |
+| 3 | CQ | Chongqing |
+| 4 | FJ | Fujian |
+| 5 | GS | Gansu |
+| 6 | GD | Guangdong |
+| 7 | GX | Guangxi |
+| 8 | GZ | Guizhou |
+| 9 | HI | Hainan |
+| 10 | HE | Hebei |
+| 11 | HL | Heilongjiang |
+| 12 | HA | Henan |
+| 13 | HB | Hubei |
+| 14 | HN | Hunan |
+| 15 | JS | Jiangsu |
+| 16 | JX | Jiangxi |
+| 17 | JL | Jilin |
+| 18 | LN | Liaoning |
+| 19 | NM | Inner Mongolia |
+| 20 | NX | Ningxia |
+| 21 | QH | Qinghai |
+| 22 | SN | Shaanxi |
+| 23 | SD | Shandong |
+| 24 | SH | Shanghai |
+| 25 | SX | Shanxi |
+| 26 | SC | Sichuan |
+| 27 | TJ | Tianjin |
+| 28 | XJ | Xinjiang |
+| 29 | XZ | Tibet |
+| 30 | YN | Yunnan |
+| 31 | ZJ | Zhejiang |
+| 32 | HK | Hong Kong |
+| 33 | MO | Macao |
+| 34 | TW | Taiwan |
+
+**Total subdivisions:** 34
diff --git a/src/swarms/doc/iso3166-2/250-France.md b/src/swarms/doc/iso3166-2/250-France.md
new file mode 100644
index 00000000..1d253878
--- /dev/null
+++ b/src/swarms/doc/iso3166-2/250-France.md
@@ -0,0 +1,28 @@
+# France (250)
+
+ISO 3166-1 numeric: **250**
+
+## Admin Area Mappings
+
+| Admin Code | ISO 3166-2 | Name |
+|-------------|------------|------|
+| 1 | ARA | Auvergne-Rhône-Alpes |
+| 2 | BFC | Bourgogne-Franche-Comté |
+| 3 | BRE | Brittany |
+| 4 | CVL | Centre-Val de Loire |
+| 5 | COR | Corsica |
+| 6 | GES | Grand Est |
+| 7 | HDF | Hauts-de-France |
+| 8 | IDF | Île-de-France |
+| 9 | NOR | Normandy |
+| 10 | NAQ | Nouvelle-Aquitaine |
+| 11 | OCC | Occitanie |
+| 12 | PDL | Pays de la Loire |
+| 13 | PAC | Provence-Alpes-Côte d'Azur |
+| 14 | GP | Guadeloupe |
+| 15 | MQ | Martinique |
+| 16 | GF | French Guiana |
+| 17 | RE | Réunion |
+| 18 | YT | Mayotte |
+
+**Total subdivisions:** 18
diff --git a/src/swarms/doc/iso3166-2/276-Germany.md b/src/swarms/doc/iso3166-2/276-Germany.md
new file mode 100644
index 00000000..dea73125
--- /dev/null
+++ b/src/swarms/doc/iso3166-2/276-Germany.md
@@ -0,0 +1,26 @@
+# Germany (276)
+
+ISO 3166-1 numeric: **276**
+
+## Admin Area Mappings
+
+| Admin Code | ISO 3166-2 | Name |
+|-------------|------------|------|
+| 1 | BW | Baden-Württemberg |
+| 2 | BY | Bavaria |
+| 3 | BE | Berlin |
+| 4 | BB | Brandenburg |
+| 5 | HB | Bremen |
+| 6 | HH | Hamburg |
+| 7 | HE | Hesse |
+| 8 | MV | Mecklenburg-Vorpommern |
+| 9 | NI | Lower Saxony |
+| 10 | NW | North Rhine-Westphalia |
+| 11 | RP | Rhineland-Palatinate |
+| 12 | SL | Saarland |
+| 13 | SN | Saxony |
+| 14 | ST | Saxony-Anhalt |
+| 15 | SH | Schleswig-Holstein |
+| 16 | TH | Thuringia |
+
+**Total subdivisions:** 16
diff --git a/src/swarms/doc/iso3166-2/356-India.md b/src/swarms/doc/iso3166-2/356-India.md
new file mode 100644
index 00000000..b6ec245a
--- /dev/null
+++ b/src/swarms/doc/iso3166-2/356-India.md
@@ -0,0 +1,46 @@
+# India (356)
+
+ISO 3166-1 numeric: **356**
+
+## Admin Area Mappings
+
+| Admin Code | ISO 3166-2 | Name |
+|-------------|------------|------|
+| 1 | AN | Andaman and Nicobar Islands |
+| 2 | AP | Andhra Pradesh |
+| 3 | AR | Arunachal Pradesh |
+| 4 | AS | Assam |
+| 5 | BR | Bihar |
+| 6 | CH | Chandigarh |
+| 7 | CT | Chhattisgarh |
+| 8 | DH | Dadra and Nagar Haveli and Daman and Diu |
+| 9 | DL | Delhi |
+| 10 | GA | Goa |
+| 11 | GJ | Gujarat |
+| 12 | HR | Haryana |
+| 13 | HP | Himachal Pradesh |
+| 14 | JK | Jammu and Kashmir |
+| 15 | JH | Jharkhand |
+| 16 | KA | Karnataka |
+| 17 | KL | Kerala |
+| 18 | LA | Ladakh |
+| 19 | LD | Lakshadweep |
+| 20 | MP | Madhya Pradesh |
+| 21 | MH | Maharashtra |
+| 22 | MN | Manipur |
+| 23 | ML | Meghalaya |
+| 24 | MZ | Mizoram |
+| 25 | NL | Nagaland |
+| 26 | OR | Odisha |
+| 27 | PY | Puducherry |
+| 28 | PB | Punjab |
+| 29 | RJ | Rajasthan |
+| 30 | SK | Sikkim |
+| 31 | TN | Tamil Nadu |
+| 32 | TG | Telangana |
+| 33 | TR | Tripura |
+| 34 | UP | Uttar Pradesh |
+| 35 | UT | Uttarakhand |
+| 36 | WB | West Bengal |
+
+**Total subdivisions:** 36
diff --git a/src/swarms/doc/iso3166-2/380-Italy.md b/src/swarms/doc/iso3166-2/380-Italy.md
new file mode 100644
index 00000000..06d5c5f5
--- /dev/null
+++ b/src/swarms/doc/iso3166-2/380-Italy.md
@@ -0,0 +1,30 @@
+# Italy (380)
+
+ISO 3166-1 numeric: **380**
+
+## Admin Area Mappings
+
+| Admin Code | ISO 3166-2 | Name |
+|-------------|------------|------|
+| 1 | 65 | Abruzzo |
+| 2 | 77 | Basilicata |
+| 3 | 78 | Calabria |
+| 4 | 72 | Campania |
+| 5 | 45 | Emilia-Romagna |
+| 6 | 36 | Friuli-Venezia Giulia |
+| 7 | 62 | Lazio |
+| 8 | 42 | Liguria |
+| 9 | 25 | Lombardy |
+| 10 | 57 | Marche |
+| 11 | 67 | Molise |
+| 12 | 21 | Piedmont |
+| 13 | 75 | Apulia |
+| 14 | 88 | Sardinia |
+| 15 | 82 | Sicily |
+| 16 | 52 | Tuscany |
+| 17 | 32 | Trentino-South Tyrol |
+| 18 | 55 | Umbria |
+| 19 | 23 | Aosta Valley |
+| 20 | 34 | Veneto |
+
+**Total subdivisions:** 20
diff --git a/src/swarms/doc/iso3166-2/392-Japan.md b/src/swarms/doc/iso3166-2/392-Japan.md
new file mode 100644
index 00000000..d7952e81
--- /dev/null
+++ b/src/swarms/doc/iso3166-2/392-Japan.md
@@ -0,0 +1,57 @@
+# Japan (392)
+
+ISO 3166-1 numeric: **392**
+
+## Admin Area Mappings
+
+| Admin Code | ISO 3166-2 | Name |
+|-------------|------------|------|
+| 1 | 01 | Hokkaido |
+| 2 | 02 | Aomori |
+| 3 | 03 | Iwate |
+| 4 | 04 | Miyagi |
+| 5 | 05 | Akita |
+| 6 | 06 | Yamagata |
+| 7 | 07 | Fukushima |
+| 8 | 08 | Ibaraki |
+| 9 | 09 | Tochigi |
+| 10 | 10 | Gunma |
+| 11 | 11 | Saitama |
+| 12 | 12 | Chiba |
+| 13 | 13 | Tokyo |
+| 14 | 14 | Kanagawa |
+| 15 | 15 | Niigata |
+| 16 | 16 | Toyama |
+| 17 | 17 | Ishikawa |
+| 18 | 18 | Fukui |
+| 19 | 19 | Yamanashi |
+| 20 | 20 | Nagano |
+| 21 | 21 | Gifu |
+| 22 | 22 | Shizuoka |
+| 23 | 23 | Aichi |
+| 24 | 24 | Mie |
+| 25 | 25 | Shiga |
+| 26 | 26 | Kyoto |
+| 27 | 27 | Osaka |
+| 28 | 28 | Hyogo |
+| 29 | 29 | Nara |
+| 30 | 30 | Wakayama |
+| 31 | 31 | Tottori |
+| 32 | 32 | Shimane |
+| 33 | 33 | Okayama |
+| 34 | 34 | Hiroshima |
+| 35 | 35 | Yamaguchi |
+| 36 | 36 | Tokushima |
+| 37 | 37 | Kagawa |
+| 38 | 38 | Ehime |
+| 39 | 39 | Kochi |
+| 40 | 40 | Fukuoka |
+| 41 | 41 | Saga |
+| 42 | 42 | Nagasaki |
+| 43 | 43 | Kumamoto |
+| 44 | 44 | Oita |
+| 45 | 45 | Miyazaki |
+| 46 | 46 | Kagoshima |
+| 47 | 47 | Okinawa |
+
+**Total subdivisions:** 47
diff --git a/src/swarms/doc/iso3166-2/410-South_Korea.md b/src/swarms/doc/iso3166-2/410-South_Korea.md
new file mode 100644
index 00000000..fc145bc4
--- /dev/null
+++ b/src/swarms/doc/iso3166-2/410-South_Korea.md
@@ -0,0 +1,27 @@
+# South Korea (410)
+
+ISO 3166-1 numeric: **410**
+
+## Admin Area Mappings
+
+| Admin Code | ISO 3166-2 | Name |
+|-------------|------------|------|
+| 1 | 11 | Seoul |
+| 2 | 26 | Busan |
+| 3 | 27 | Daegu |
+| 4 | 28 | Incheon |
+| 5 | 29 | Gwangju |
+| 6 | 30 | Daejeon |
+| 7 | 31 | Ulsan |
+| 8 | 41 | Gyeonggi |
+| 9 | 42 | Gangwon |
+| 10 | 43 | North Chungcheong |
+| 11 | 44 | South Chungcheong |
+| 12 | 45 | North Jeolla |
+| 13 | 46 | South Jeolla |
+| 14 | 47 | North Gyeongsang |
+| 15 | 48 | South Gyeongsang |
+| 16 | 49 | Jeju |
+| 17 | 50 | Sejong |
+
+**Total subdivisions:** 17
diff --git a/src/swarms/doc/iso3166-2/484-Mexico.md b/src/swarms/doc/iso3166-2/484-Mexico.md
new file mode 100644
index 00000000..61b384f3
--- /dev/null
+++ b/src/swarms/doc/iso3166-2/484-Mexico.md
@@ -0,0 +1,42 @@
+# Mexico (484)
+
+ISO 3166-1 numeric: **484**
+
+## Admin Area Mappings
+
+| Admin Code | ISO 3166-2 | Name |
+|-------------|------------|------|
+| 1 | AGU | Aguascalientes |
+| 2 | BCN | Baja California |
+| 3 | BCS | Baja California Sur |
+| 4 | CAM | Campeche |
+| 5 | CHP | Chiapas |
+| 6 | CHH | Chihuahua |
+| 7 | CMX | Mexico City |
+| 8 | COA | Coahuila |
+| 9 | COL | Colima |
+| 10 | DUR | Durango |
+| 11 | GUA | Guanajuato |
+| 12 | GRO | Guerrero |
+| 13 | HID | Hidalgo |
+| 14 | JAL | Jalisco |
+| 15 | MEX | State of Mexico |
+| 16 | MIC | Michoacán |
+| 17 | MOR | Morelos |
+| 18 | NAY | Nayarit |
+| 19 | NLE | Nuevo León |
+| 20 | OAX | Oaxaca |
+| 21 | PUE | Puebla |
+| 22 | QUE | Querétaro |
+| 23 | ROO | Quintana Roo |
+| 24 | SLP | San Luis Potosí |
+| 25 | SIN | Sinaloa |
+| 26 | SON | Sonora |
+| 27 | TAB | Tabasco |
+| 28 | TAM | Tamaulipas |
+| 29 | TLA | Tlaxcala |
+| 30 | VER | Veracruz |
+| 31 | YUC | Yucatán |
+| 32 | ZAC | Zacatecas |
+
+**Total subdivisions:** 32
diff --git a/src/swarms/doc/iso3166-2/566-Nigeria.md b/src/swarms/doc/iso3166-2/566-Nigeria.md
new file mode 100644
index 00000000..83b523c2
--- /dev/null
+++ b/src/swarms/doc/iso3166-2/566-Nigeria.md
@@ -0,0 +1,47 @@
+# Nigeria (566)
+
+ISO 3166-1 numeric: **566**
+
+## Admin Area Mappings
+
+| Admin Code | ISO 3166-2 | Name |
+|-------------|------------|------|
+| 1 | AB | Abia |
+| 2 | FC | Abuja Federal Capital Territory |
+| 3 | AD | Adamawa |
+| 4 | AK | Akwa Ibom |
+| 5 | AN | Anambra |
+| 6 | BA | Bauchi |
+| 7 | BY | Bayelsa |
+| 8 | BE | Benue |
+| 9 | BO | Borno |
+| 10 | CR | Cross River |
+| 11 | DE | Delta |
+| 12 | EB | Ebonyi |
+| 13 | ED | Edo |
+| 14 | EK | Ekiti |
+| 15 | EN | Enugu |
+| 16 | GO | Gombe |
+| 17 | IM | Imo |
+| 18 | JI | Jigawa |
+| 19 | KD | Kaduna |
+| 20 | KN | Kano |
+| 21 | KT | Katsina |
+| 22 | KE | Kebbi |
+| 23 | KO | Kogi |
+| 24 | KW | Kwara |
+| 25 | LA | Lagos |
+| 26 | NA | Nasarawa |
+| 27 | NI | Niger |
+| 28 | OG | Ogun |
+| 29 | ON | Ondo |
+| 30 | OS | Osun |
+| 31 | OY | Oyo |
+| 32 | PL | Plateau |
+| 33 | RI | Rivers |
+| 34 | SO | Sokoto |
+| 35 | TA | Taraba |
+| 36 | YO | Yobe |
+| 37 | ZA | Zamfara |
+
+**Total subdivisions:** 37
diff --git a/src/swarms/doc/iso3166-2/643-Russia.md b/src/swarms/doc/iso3166-2/643-Russia.md
new file mode 100644
index 00000000..0705c6c6
--- /dev/null
+++ b/src/swarms/doc/iso3166-2/643-Russia.md
@@ -0,0 +1,93 @@
+# Russia (643)
+
+ISO 3166-1 numeric: **643**
+
+## Admin Area Mappings
+
+| Admin Code | ISO 3166-2 | Name |
+|-------------|------------|------|
+| 1 | AD | Adygea, Republic of |
+| 2 | AL | Altai Republic |
+| 3 | ALT | Altai Krai |
+| 4 | AMU | Amur Oblast |
+| 5 | ARK | Arkhangelsk Oblast |
+| 6 | AST | Astrakhan Oblast |
+| 7 | BA | Bashkortostan, Republic of |
+| 8 | BEL | Belgorod Oblast |
+| 9 | BRY | Bryansk Oblast |
+| 10 | BU | Buryatia, Republic of |
+| 11 | CE | Chechen Republic |
+| 12 | CHE | Chelyabinsk Oblast |
+| 13 | CHU | Chukotka Autonomous Okrug |
+| 14 | CU | Chuvash Republic |
+| 15 | DA | Dagestan, Republic of |
+| 16 | IN | Ingushetia, Republic of |
+| 17 | IRK | Irkutsk Oblast |
+| 18 | IVA | Ivanovo Oblast |
+| 19 | KB | Kabardino-Balkar Republic |
+| 20 | KGD | Kaliningrad Oblast |
+| 21 | KL | Kalmykia, Republic of |
+| 22 | KLU | Kaluga Oblast |
+| 23 | KAM | Kamchatka Krai |
+| 24 | KC | Karachay-Cherkess Republic |
+| 25 | KR | Karelia, Republic of |
+| 26 | KEM | Kemerovo Oblast |
+| 27 | KHA | Khabarovsk Krai |
+| 28 | KK | Khakassia, Republic of |
+| 29 | KHM | Khanty-Mansi Autonomous Okrug |
+| 30 | KIR | Kirov Oblast |
+| 31 | KO | Komi Republic |
+| 32 | KOS | Kostroma Oblast |
+| 33 | KDA | Krasnodar Krai |
+| 34 | KYA | Krasnoyarsk Krai |
+| 35 | KGN | Kurgan Oblast |
+| 36 | KRS | Kursk Oblast |
+| 37 | LEN | Leningrad Oblast |
+| 38 | LIP | Lipetsk Oblast |
+| 39 | MAG | Magadan Oblast |
+| 40 | ME | Mari El Republic |
+| 41 | MO | Mordovia, Republic of |
+| 42 | MOS | Moscow Oblast |
+| 43 | MOW | Moscow |
+| 44 | MUR | Murmansk Oblast |
+| 45 | NEN | Nenets Autonomous Okrug |
+| 46 | NIZ | Nizhny Novgorod Oblast |
+| 47 | NGR | Novgorod Oblast |
+| 48 | NVS | Novosibirsk Oblast |
+| 49 | OMS | Omsk Oblast |
+| 50 | ORE | Orenburg Oblast |
+| 51 | ORL | Oryol Oblast |
+| 52 | PNZ | Penza Oblast |
+| 53 | PER | Perm Krai |
+| 54 | PRI | Primorsky Krai |
+| 55 | PSK | Pskov Oblast |
+| 56 | ROS | Rostov Oblast |
+| 57 | RYA | Ryazan Oblast |
+| 58 | SA | Sakha (Yakutia), Republic of |
+| 59 | SAK | Sakhalin Oblast |
+| 60 | SAM | Samara Oblast |
+| 61 | SPE | Saint Petersburg |
+| 62 | SAR | Saratov Oblast |
+| 63 | SE | North Ossetia-Alania, Republic of |
+| 64 | SMO | Smolensk Oblast |
+| 65 | STA | Stavropol Krai |
+| 66 | SVE | Sverdlovsk Oblast |
+| 67 | TAM | Tambov Oblast |
+| 68 | TA | Tatarstan, Republic of |
+| 69 | TOM | Tomsk Oblast |
+| 70 | TUL | Tula Oblast |
+| 71 | TVE | Tver Oblast |
+| 72 | TY | Tuva Republic |
+| 73 | TYU | Tyumen Oblast |
+| 74 | UD | Udmurt Republic |
+| 75 | ULY | Ulyanovsk Oblast |
+| 76 | VLA | Vladimir Oblast |
+| 77 | VGG | Volgograd Oblast |
+| 78 | VLG | Vologda Oblast |
+| 79 | VOR | Voronezh Oblast |
+| 80 | YAN | Yamalo-Nenets Autonomous Okrug |
+| 81 | YAR | Yaroslavl Oblast |
+| 82 | YEV | Jewish Autonomous Oblast |
+| 83 | ZAB | Zabaykalsky Krai |
+
+**Total subdivisions:** 83
diff --git a/src/swarms/doc/iso3166-2/710-South_Africa.md b/src/swarms/doc/iso3166-2/710-South_Africa.md
new file mode 100644
index 00000000..99a19bd7
--- /dev/null
+++ b/src/swarms/doc/iso3166-2/710-South_Africa.md
@@ -0,0 +1,19 @@
+# South Africa (710)
+
+ISO 3166-1 numeric: **710**
+
+## Admin Area Mappings
+
+| Admin Code | ISO 3166-2 | Name |
+|-------------|------------|------|
+| 1 | EC | Eastern Cape |
+| 2 | FS | Free State |
+| 3 | GT | Gauteng |
+| 4 | NL | KwaZulu-Natal |
+| 5 | LP | Limpopo |
+| 6 | MP | Mpumalanga |
+| 7 | NW | North West |
+| 8 | NC | Northern Cape |
+| 9 | WC | Western Cape |
+
+**Total subdivisions:** 9
diff --git a/src/swarms/doc/iso3166-2/724-Spain.md b/src/swarms/doc/iso3166-2/724-Spain.md
new file mode 100644
index 00000000..6c435504
--- /dev/null
+++ b/src/swarms/doc/iso3166-2/724-Spain.md
@@ -0,0 +1,29 @@
+# Spain (724)
+
+ISO 3166-1 numeric: **724**
+
+## Admin Area Mappings
+
+| Admin Code | ISO 3166-2 | Name |
+|-------------|------------|------|
+| 1 | AN | Andalusia |
+| 2 | AR | Aragon |
+| 3 | AS | Asturias, Principality of |
+| 4 | CN | Canary Islands |
+| 5 | CB | Cantabria |
+| 6 | CL | Castile and León |
+| 7 | CM | Castilla-La Mancha |
+| 8 | CT | Catalonia |
+| 9 | CE | Ceuta |
+| 10 | EX | Extremadura |
+| 11 | GA | Galicia |
+| 12 | IB | Balearic Islands |
+| 13 | RI | La Rioja |
+| 14 | MD | Community of Madrid |
+| 15 | ML | Melilla |
+| 16 | MC | Murcia, Region of |
+| 17 | NC | Navarre, Chartered Community of |
+| 18 | PV | Basque Country |
+| 19 | VC | Valencian Community |
+
+**Total subdivisions:** 19
diff --git a/src/swarms/doc/iso3166-2/756-Switzerland.md b/src/swarms/doc/iso3166-2/756-Switzerland.md
new file mode 100644
index 00000000..978590ee
--- /dev/null
+++ b/src/swarms/doc/iso3166-2/756-Switzerland.md
@@ -0,0 +1,36 @@
+# Switzerland (756)
+
+ISO 3166-1 numeric: **756**
+
+## Admin Area Mappings
+
+| Admin Code | ISO 3166-2 | Name |
+|-------------|------------|------|
+| 1 | AG | Aargau |
+| 2 | AI | Appenzell Innerrhoden |
+| 3 | AR | Appenzell Ausserrhoden |
+| 4 | BE | Bern |
+| 5 | BL | Basel-Landschaft |
+| 6 | BS | Basel-Stadt |
+| 7 | FR | Fribourg |
+| 8 | GE | Geneva |
+| 9 | GL | Glarus |
+| 10 | GR | Graubünden |
+| 11 | JU | Jura |
+| 12 | LU | Lucerne |
+| 13 | NE | Neuchâtel |
+| 14 | NW | Nidwalden |
+| 15 | OW | Obwalden |
+| 16 | SG | St. Gallen |
+| 17 | SH | Schaffhausen |
+| 18 | SO | Solothurn |
+| 19 | SZ | Schwyz |
+| 20 | TG | Thurgau |
+| 21 | TI | Ticino |
+| 22 | UR | Uri |
+| 23 | VD | Vaud |
+| 24 | VS | Valais |
+| 25 | ZG | Zug |
+| 26 | ZH | Zurich |
+
+**Total subdivisions:** 26
diff --git a/src/swarms/doc/iso3166-2/826-United_Kingdom.md b/src/swarms/doc/iso3166-2/826-United_Kingdom.md
new file mode 100644
index 00000000..96eea194
--- /dev/null
+++ b/src/swarms/doc/iso3166-2/826-United_Kingdom.md
@@ -0,0 +1,182 @@
+# United Kingdom (826)
+
+ISO 3166-1 numeric: **826**
+
+## Admin Area Mappings
+
+| Admin Code | ISO 3166-2 | Name |
+|-------------|------------|------|
+| 1 | ENG | England |
+| 2 | NIR | Northern Ireland |
+| 3 | SCT | Scotland |
+| 4 | WLS | Wales |
+| 5 | BKM | Buckinghamshire |
+| 6 | CAM | Cambridgeshire |
+| 7 | CMA | Cumbria |
+| 8 | DBY | Derbyshire |
+| 9 | DEV | Devon |
+| 10 | DOR | Dorset |
+| 11 | ESX | East Sussex |
+| 12 | ESS | Essex |
+| 13 | GLS | Gloucestershire |
+| 14 | HAM | Hampshire |
+| 15 | HRT | Hertfordshire |
+| 16 | KEN | Kent |
+| 17 | LAN | Lancashire |
+| 18 | LEC | Leicestershire |
+| 19 | LIN | Lincolnshire |
+| 20 | NFK | Norfolk |
+| 21 | NYK | North Yorkshire |
+| 22 | NTH | Northamptonshire |
+| 23 | NTT | Nottinghamshire |
+| 24 | OXF | Oxfordshire |
+| 25 | SOM | Somerset |
+| 26 | STS | Staffordshire |
+| 27 | SFK | Suffolk |
+| 28 | SRY | Surrey |
+| 29 | WAR | Warwickshire |
+| 30 | WSX | West Sussex |
+| 31 | WOR | Worcestershire |
+| 32 | LND | London, City of |
+| 33 | BDG | Barking and Dagenham |
+| 34 | BNE | Barnet |
+| 35 | BEX | Bexley |
+| 36 | BEN | Brent |
+| 37 | BRY | Bromley |
+| 38 | CMD | Camden |
+| 39 | CRY | Croydon |
+| 40 | EAL | Ealing |
+| 41 | ENF | Enfield |
+| 42 | GRE | Greenwich |
+| 43 | HCK | Hackney |
+| 44 | HMF | Hammersmith and Fulham |
+| 45 | HRY | Haringey |
+| 46 | HRW | Harrow |
+| 47 | HAV | Havering |
+| 48 | HIL | Hillingdon |
+| 49 | HNS | Hounslow |
+| 50 | ISL | Islington |
+| 51 | KEC | Kensington and Chelsea |
+| 52 | KTT | Kingston upon Thames |
+| 53 | LBH | Lambeth |
+| 54 | LEW | Lewisham |
+| 55 | MRT | Merton |
+| 56 | NWM | Newham |
+| 57 | RDB | Redbridge |
+| 58 | RIC | Richmond upon Thames |
+| 59 | SWK | Southwark |
+| 60 | STN | Sutton |
+| 61 | TWH | Tower Hamlets |
+| 62 | WFT | Waltham Forest |
+| 63 | WND | Wandsworth |
+| 64 | WSM | Westminster |
+| 65 | BNS | Barnsley |
+| 66 | BIR | Birmingham |
+| 67 | BOL | Bolton |
+| 68 | BRD | Bradford |
+| 69 | BRI | Brighton and Hove |
+| 70 | BST | Bristol, City of |
+| 71 | CAL | Calderdale |
+| 72 | COV | Coventry |
+| 73 | DER | Derby |
+| 74 | DUD | Dudley |
+| 75 | GAT | Gateshead |
+| 76 | KIR | Kirklees |
+| 77 | KWL | Knowsley |
+| 78 | LDS | Leeds |
+| 79 | LCE | Leicester |
+| 80 | LIV | Liverpool |
+| 81 | MAN | Manchester |
+| 82 | NET | Newcastle upon Tyne |
+| 83 | NTY | North Tyneside |
+| 84 | OLD | Oldham |
+| 85 | PTE | Peterborough |
+| 86 | PLY | Plymouth |
+| 87 | RCH | Rochdale |
+| 88 | ROT | Rotherham |
+| 89 | SLF | Salford |
+| 90 | SAW | Sandwell |
+| 91 | SFT | Sefton |
+| 92 | SHF | Sheffield |
+| 93 | SOL | Solihull |
+| 94 | STY | South Tyneside |
+| 95 | SHN | Southampton |
+| 96 | SGC | South Gloucestershire |
+| 97 | STH | Southend-on-Sea |
+| 98 | SKP | Stockport |
+| 99 | STE | Stoke-on-Trent |
+| 100 | SND | Sunderland |
+| 101 | TAM | Tameside |
+| 102 | TRF | Trafford |
+| 103 | WKF | Wakefield |
+| 104 | WLL | Walsall |
+| 105 | WGN | Wigan |
+| 106 | WRL | Wirral |
+| 107 | WLV | Wolverhampton |
+| 108 | ABE | Aberdeen City |
+| 109 | ABD | Aberdeenshire |
+| 110 | ANS | Angus |
+| 111 | AGB | Argyll and Bute |
+| 112 | CLK | Clackmannanshire |
+| 113 | DGY | Dumfries and Galloway |
+| 114 | DND | Dundee City |
+| 115 | EAY | East Ayrshire |
+| 116 | EDU | East Dunbartonshire |
+| 117 | ELN | East Lothian |
+| 118 | ERW | East Renfrewshire |
+| 119 | EDH | Edinburgh, City of |
+| 120 | ELS | Eilean Siar |
+| 121 | FAL | Falkirk |
+| 122 | FIF | Fife |
+| 123 | GLG | Glasgow City |
+| 124 | HLD | Highland |
+| 125 | IVC | Inverclyde |
+| 126 | MLN | Midlothian |
+| 127 | MRY | Moray |
+| 128 | NAY | North Ayrshire |
+| 129 | NLK | North Lanarkshire |
+| 130 | ORK | Orkney Islands |
+| 131 | PKN | Perth and Kinross |
+| 132 | RFW | Renfrewshire |
+| 133 | SCB | Scottish Borders |
+| 134 | ZET | Shetland Islands |
+| 135 | SAY | South Ayrshire |
+| 136 | SLK | South Lanarkshire |
+| 137 | STG | Stirling |
+| 138 | WDU | West Dunbartonshire |
+| 139 | WLN | West Lothian |
+| 140 | BGW | Blaenau Gwent |
+| 141 | BGE | Bridgend |
+| 142 | CAY | Caerphilly |
+| 143 | CRF | Cardiff |
+| 144 | CMN | Carmarthenshire |
+| 145 | CGN | Ceredigion |
+| 146 | CWY | Conwy |
+| 147 | DEN | Denbighshire |
+| 148 | FLN | Flintshire |
+| 149 | GWN | Gwynedd |
+| 150 | AGY | Isle of Anglesey |
+| 151 | MTY | Merthyr Tydfil |
+| 152 | MON | Monmouthshire |
+| 153 | NTL | Neath Port Talbot |
+| 154 | NWP | Newport |
+| 155 | PEM | Pembrokeshire |
+| 156 | POW | Powys |
+| 157 | RCT | Rhondda Cynon Taf |
+| 158 | SWA | Swansea |
+| 159 | TOF | Torfaen |
+| 160 | VGL | Vale of Glamorgan |
+| 161 | WRX | Wrexham |
+| 162 | ANT | Antrim and Newtownabbey |
+| 163 | ARD | Ards and North Down |
+| 164 | ABC | Armagh City, Banbridge and Craigavon |
+| 165 | BFS | Belfast |
+| 166 | CCG | Causeway Coast and Glens |
+| 167 | DRS | Derry City and Strabane |
+| 168 | FMO | Fermanagh and Omagh |
+| 169 | LBC | Lisburn and Castlereagh |
+| 170 | MEA | Mid and East Antrim |
+| 171 | MUL | Mid Ulster |
+| 172 | NMD | Newry, Mourne and Down |
+
+**Total subdivisions:** 172
diff --git a/src/swarms/doc/iso3166-2/840-United_States.md b/src/swarms/doc/iso3166-2/840-United_States.md
new file mode 100644
index 00000000..1259bd49
--- /dev/null
+++ b/src/swarms/doc/iso3166-2/840-United_States.md
@@ -0,0 +1,67 @@
+# United States (840)
+
+ISO 3166-1 numeric: **840**
+
+## Admin Area Mappings
+
+| Admin Code | ISO 3166-2 | Name |
+|-------------|------------|------|
+| 1 | AL | Alabama |
+| 2 | AK | Alaska |
+| 3 | AZ | Arizona |
+| 4 | AR | Arkansas |
+| 5 | CA | California |
+| 6 | CO | Colorado |
+| 7 | CT | Connecticut |
+| 8 | DE | Delaware |
+| 9 | FL | Florida |
+| 10 | GA | Georgia |
+| 11 | HI | Hawaii |
+| 12 | ID | Idaho |
+| 13 | IL | Illinois |
+| 14 | IN | Indiana |
+| 15 | IA | Iowa |
+| 16 | KS | Kansas |
+| 17 | KY | Kentucky |
+| 18 | LA | Louisiana |
+| 19 | ME | Maine |
+| 20 | MD | Maryland |
+| 21 | MA | Massachusetts |
+| 22 | MI | Michigan |
+| 23 | MN | Minnesota |
+| 24 | MS | Mississippi |
+| 25 | MO | Missouri |
+| 26 | MT | Montana |
+| 27 | NE | Nebraska |
+| 28 | NV | Nevada |
+| 29 | NH | New Hampshire |
+| 30 | NJ | New Jersey |
+| 31 | NM | New Mexico |
+| 32 | NY | New York |
+| 33 | NC | North Carolina |
+| 34 | ND | North Dakota |
+| 35 | OH | Ohio |
+| 36 | OK | Oklahoma |
+| 37 | OR | Oregon |
+| 38 | PA | Pennsylvania |
+| 39 | RI | Rhode Island |
+| 40 | SC | South Carolina |
+| 41 | SD | South Dakota |
+| 42 | TN | Tennessee |
+| 43 | TX | Texas |
+| 44 | UT | Utah |
+| 45 | VT | Vermont |
+| 46 | VA | Virginia |
+| 47 | WA | Washington |
+| 48 | WV | West Virginia |
+| 49 | WI | Wisconsin |
+| 50 | WY | Wyoming |
+| 51 | DC | District of Columbia |
+| 52 | AS | American Samoa |
+| 53 | GU | Guam |
+| 54 | MP | Northern Mariana Islands |
+| 55 | PR | Puerto Rico |
+| 56 | UM | United States Minor Outlying Islands |
+| 57 | VI | Virgin Islands, U.S. |
+
+**Total subdivisions:** 57
diff --git a/src/swarms/doc/spec/.gitignore b/src/swarms/doc/spec/.gitignore
new file mode 100644
index 00000000..70d46385
--- /dev/null
+++ b/src/swarms/doc/spec/.gitignore
@@ -0,0 +1,6 @@
+# Build artifacts
+mermaid-images/
+*.rendered.md
+*.pdf
+node_modules/
+.puppeteer-ci.json
diff --git a/src/swarms/doc/spec/build.sh b/src/swarms/doc/spec/build.sh
new file mode 100755
index 00000000..4b95a711
--- /dev/null
+++ b/src/swarms/doc/spec/build.sh
@@ -0,0 +1,143 @@
+#!/usr/bin/env bash
+# =============================================================================
+# build.sh — Build the Swarm System specification PDF
+#
+# Renders Mermaid diagrams to images, then converts the consolidated
+# markdown specification to a styled PDF.
+#
+# USAGE:
+# ./src/swarms/doc/spec/build.sh
+#
+# OUTPUT:
+# src/swarms/doc/spec/swarm-specification.pdf
+#
+# REQUIREMENTS (installed automatically on first run):
+# - Node.js >= 18
+# - @mermaid-js/mermaid-cli (mmdc) — renders Mermaid diagrams
+# - md-to-pdf — converts Markdown to PDF via Puppeteer
+# =============================================================================
+
+set -euo pipefail
+
+SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
+cd "$SCRIPT_DIR"
+
+INPUT="swarm-specification.md"
+RENDERED="swarm-specification.rendered.md"
+OUTPUT="swarm-specification.pdf"
+CSS="pdf-style.css"
+MERMAID_CFG="mermaid-config.json"
+IMG_DIR="mermaid-images"
+
+RED='\033[0;31m'
+GREEN='\033[0;32m'
+BLUE='\033[0;34m'
+NC='\033[0m'
+
+log() { echo -e "${BLUE}[build]${NC} $1"; }
+ok() { echo -e "${GREEN}[done]${NC} $1"; }
+fail() { echo -e "${RED}[error]${NC} $1"; exit 1; }
+
+# CI environments (GitHub Actions) need --no-sandbox for Puppeteer/Chrome
+PUPPETEER_ARGS=""
+if [ "${CI:-}" = "true" ]; then
+ log "CI detected — enabling --no-sandbox for Chrome"
+ PUPPETEER_CFG="$SCRIPT_DIR/.puppeteer-ci.json"
+ echo '{"args":["--no-sandbox","--disable-setuid-sandbox"]}' > "$PUPPETEER_CFG"
+ PUPPETEER_ARGS="-p $PUPPETEER_CFG"
+fi
+
+# ---------------------------------------------------------------------------
+# 1. Check / install dependencies
+# ---------------------------------------------------------------------------
+
+if ! command -v node &>/dev/null; then
+ fail "Node.js is required but not installed."
+fi
+
+# Find binaries — they may be hoisted to the project root node_modules
+find_bin() {
+ local name="$1"
+ local dir="$SCRIPT_DIR"
+ while [ "$dir" != "/" ]; do
+ if [ -x "$dir/node_modules/.bin/$name" ]; then
+ echo "$dir/node_modules/.bin/$name"
+ return 0
+ fi
+ dir="$(dirname "$dir")"
+ done
+ return 1
+}
+
+MMDC="$(find_bin mmdc || true)"
+MD2PDF="$(find_bin md-to-pdf || true)"
+
+if [ -z "$MMDC" ] || [ -z "$MD2PDF" ]; then
+ log "Installing dependencies..."
+ npm install --no-save @mermaid-js/mermaid-cli md-to-pdf 2>&1
+ MMDC="$(find_bin mmdc)" || fail "mmdc not found after install"
+ MD2PDF="$(find_bin md-to-pdf)" || fail "md-to-pdf not found after install"
+ ok "Dependencies installed"
+fi
+
+log "Using mmdc: $MMDC"
+log "Using md-to-pdf: $MD2PDF"
+
+# ---------------------------------------------------------------------------
+# 2. Render Mermaid code blocks → PNG images
+# ---------------------------------------------------------------------------
+
+log "Rendering Mermaid diagrams..."
+
+mkdir -p "$IMG_DIR"
+
+# mmdc can process a markdown file directly:
+# - finds all ```mermaid blocks
+# - renders each to an image
+# - outputs a new markdown with image references
+"$MMDC" \
+ -i "$INPUT" \
+ -o "$RENDERED" \
+ -e png \
+ -b white \
+ -c "$MERMAID_CFG" \
+ -a "$IMG_DIR" \
+ $PUPPETEER_ARGS \
+ -q 2>&1 || fail "Mermaid rendering failed"
+
+ok "Mermaid diagrams rendered to $IMG_DIR/"
+
+# ---------------------------------------------------------------------------
+# 3. Convert rendered Markdown → PDF
+# ---------------------------------------------------------------------------
+
+log "Generating PDF..."
+
+LAUNCH_OPTS=""
+if [ "${CI:-}" = "true" ]; then
+ LAUNCH_OPTS='--launch-options {"args":["--no-sandbox","--disable-setuid-sandbox"]}'
+fi
+
+"$MD2PDF" "$RENDERED" \
+ --stylesheet "$CSS" \
+ $LAUNCH_OPTS \
+ --pdf-options '{"format":"A4","margin":{"top":"25mm","bottom":"25mm","left":"20mm","right":"20mm"},"printBackground":true,"displayHeaderFooter":true,"headerTemplate":"","footerTemplate":"
Nodle Swarm System — Technical SpecificationPage of
"}' \
+ 2>&1 || fail "PDF generation failed"
+
+# md-to-pdf names output based on input filename
+if [ -f "swarm-specification.rendered.pdf" ]; then
+ mv "swarm-specification.rendered.pdf" "$OUTPUT"
+fi
+
+ok "PDF generated: $SCRIPT_DIR/$OUTPUT"
+
+# ---------------------------------------------------------------------------
+# 4. Clean up intermediate files
+# ---------------------------------------------------------------------------
+
+rm -f "$RENDERED"
+
+log "Build complete!"
+echo ""
+echo " 📄 $SCRIPT_DIR/$OUTPUT"
+echo ""
diff --git a/src/swarms/doc/spec/mermaid-config.json b/src/swarms/doc/spec/mermaid-config.json
new file mode 100644
index 00000000..f9146662
--- /dev/null
+++ b/src/swarms/doc/spec/mermaid-config.json
@@ -0,0 +1,20 @@
+{
+ "theme": "default",
+ "themeVariables": {
+ "primaryColor": "#4a9eff",
+ "primaryTextColor": "#fff",
+ "primaryBorderColor": "#3a8eef",
+ "lineColor": "#666",
+ "secondaryColor": "#f0f4f8",
+ "tertiaryColor": "#e8f4e8",
+ "fontSize": "14px"
+ },
+ "flowchart": {
+ "curve": "basis",
+ "useMaxWidth": true
+ },
+ "sequence": {
+ "useMaxWidth": true,
+ "wrap": true
+ }
+}
diff --git a/src/swarms/doc/spec/pdf-style.css b/src/swarms/doc/spec/pdf-style.css
new file mode 100644
index 00000000..6b8c3622
--- /dev/null
+++ b/src/swarms/doc/spec/pdf-style.css
@@ -0,0 +1,264 @@
+/* ==========================================================================
+ Swarm System Specification — PDF Stylesheet
+ Used by md-to-pdf (Puppeteer-based PDF generation)
+ ========================================================================== */
+
+/* ---------- Page & Base ---------- */
+
+@page {
+ size: A4;
+ margin: 25mm 20mm;
+}
+
+body {
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif;
+ font-size: 11pt;
+ line-height: 1.55;
+ color: #1a1a1a;
+ max-width: 100%;
+}
+
+/* ---------- Title Page ---------- */
+
+.title-page {
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+ align-items: center;
+ min-height: 70vh;
+ text-align: center;
+}
+
+.title-page h1 {
+ font-size: 32pt;
+ font-weight: 700;
+ margin-bottom: 8pt;
+ color: #111;
+ border-bottom: none;
+}
+
+.title-page h2 {
+ font-size: 16pt;
+ font-weight: 400;
+ color: #444;
+ border-bottom: none;
+ margin-top: 0;
+}
+
+.title-page p {
+ font-size: 11pt;
+ color: #666;
+ margin-top: 24pt;
+}
+
+/* ---------- Page Breaks ---------- */
+
+.page-break {
+ page-break-after: always;
+}
+
+/* ---------- Headings ---------- */
+
+h1 {
+ font-size: 22pt;
+ font-weight: 700;
+ color: #111;
+ border-bottom: 2px solid #4a9eff;
+ padding-bottom: 6pt;
+ margin-top: 36pt;
+ margin-bottom: 16pt;
+ page-break-after: avoid;
+}
+
+h2 {
+ font-size: 17pt;
+ font-weight: 600;
+ color: #1a1a1a;
+ border-bottom: 1px solid #ddd;
+ padding-bottom: 4pt;
+ margin-top: 28pt;
+ margin-bottom: 12pt;
+ page-break-after: avoid;
+}
+
+h3 {
+ font-size: 13pt;
+ font-weight: 600;
+ color: #222;
+ margin-top: 20pt;
+ margin-bottom: 8pt;
+ page-break-after: avoid;
+}
+
+h4 {
+ font-size: 11.5pt;
+ font-weight: 600;
+ color: #333;
+ margin-top: 14pt;
+ margin-bottom: 6pt;
+ page-break-after: avoid;
+}
+
+/* ---------- Table of Contents ---------- */
+
+h2#table-of-contents,
+h2:has(+ ol a[href^="#"]) {
+ border-bottom: 2px solid #4a9eff;
+}
+
+/* ---------- Paragraphs ---------- */
+
+p {
+ margin: 6pt 0;
+ orphans: 3;
+ widows: 3;
+}
+
+/* ---------- Tables ---------- */
+
+table {
+ width: 100%;
+ border-collapse: collapse;
+ margin: 12pt 0;
+ font-size: 10pt;
+ page-break-inside: avoid;
+}
+
+thead {
+ background-color: #f0f4f8;
+}
+
+th {
+ font-weight: 600;
+ text-align: left;
+ padding: 8px 10px;
+ border: 1px solid #d0d7de;
+ color: #1a1a1a;
+}
+
+td {
+ padding: 6px 10px;
+ border: 1px solid #d0d7de;
+ vertical-align: top;
+}
+
+tr:nth-child(even) {
+ background-color: #f8f9fa;
+}
+
+/* ---------- Code ---------- */
+
+code {
+ font-family: "SF Mono", "Fira Code", "JetBrains Mono", Menlo, Consolas, monospace;
+ font-size: 9.5pt;
+ background-color: #f0f4f8;
+ padding: 1px 4px;
+ border-radius: 3px;
+ color: #1a1a1a;
+}
+
+pre {
+ background-color: #f6f8fa;
+ border: 1px solid #d0d7de;
+ border-radius: 6px;
+ padding: 12px 16px;
+ overflow-x: auto;
+ font-size: 9pt;
+ line-height: 1.45;
+ margin: 10pt 0;
+ page-break-inside: avoid;
+}
+
+pre code {
+ background: none;
+ padding: 0;
+ border-radius: 0;
+ font-size: inherit;
+}
+
+/* ---------- Images (Mermaid diagrams) ---------- */
+
+img {
+ max-width: 100%;
+ height: auto;
+ display: block;
+ margin: 16pt auto;
+ page-break-inside: avoid;
+}
+
+/* Center mermaid diagrams */
+p > img {
+ margin: 16pt auto;
+}
+
+/* ---------- Blockquotes ---------- */
+
+blockquote {
+ border-left: 4px solid #4a9eff;
+ padding: 8px 16px;
+ margin: 12pt 0;
+ color: #444;
+ background-color: #f0f7ff;
+ border-radius: 0 4px 4px 0;
+}
+
+/* ---------- Lists ---------- */
+
+ul, ol {
+ margin: 6pt 0;
+ padding-left: 24pt;
+}
+
+li {
+ margin: 3pt 0;
+}
+
+/* ---------- Horizontal Rules ---------- */
+
+hr {
+ border: none;
+ border-top: 1px solid #d0d7de;
+ margin: 24pt 0;
+}
+
+/* ---------- Strong / Emphasis ---------- */
+
+strong {
+ font-weight: 600;
+ color: #111;
+}
+
+em {
+ font-style: italic;
+}
+
+/* ---------- Links ---------- */
+
+a {
+ color: #4a9eff;
+ text-decoration: none;
+}
+
+a:hover {
+ text-decoration: underline;
+}
+
+/* ---------- Print-specific ---------- */
+
+@media print {
+ body {
+ font-size: 10.5pt;
+ }
+
+ h1, h2, h3, h4 {
+ page-break-after: avoid;
+ }
+
+ table, pre, img, blockquote {
+ page-break-inside: avoid;
+ }
+
+ .page-break {
+ page-break-after: always;
+ }
+}
diff --git a/src/swarms/doc/spec/swarm-specification.md b/src/swarms/doc/spec/swarm-specification.md
new file mode 100644
index 00000000..556c3471
--- /dev/null
+++ b/src/swarms/doc/spec/swarm-specification.md
@@ -0,0 +1,1335 @@
+---
+title: "Nodle Swarm System — Technical Specification"
+subtitle: "Decentralized BLE Tag Registry with Cryptographic Membership Proofs"
+date: "March 2026"
+version: "1.0"
+---
+
+
+
+# Nodle Swarm System
+
+## Technical Specification
+
+**Decentralized BLE Tag Registry with Cryptographic Membership Proofs**
+
+Version 1.0 — March 2026
+
+
+
+
+
+## Table of Contents
+
+1. [Introduction & Architecture](#1-introduction--architecture)
+2. [Data Model & Interfaces](#2-data-model--interfaces)
+3. [BLE Tag Types & UUID Encoding](#3-ble-tag-types--uuid-encoding)
+4. [Fleet Registration](#4-fleet-registration)
+5. [Tier Economics & Bond System](#5-tier-economics--bond-system)
+6. [Swarm Operations](#6-swarm-operations)
+7. [Lifecycle & State Machines](#7-lifecycle--state-machines)
+8. [Client Discovery](#8-client-discovery)
+9. [Fleet Maintenance](#9-fleet-maintenance)
+10. [Upgradeable Contract Architecture](#10-upgradeable-contract-architecture)
+11. [Appendix A: ISO 3166 Geographic Reference](#appendix-a-iso-3166-geographic-reference)
+
+
+
+## 1. Introduction & Architecture
+
+### 1.1 System Overview
+
+The Swarm System is a **non-enumerating** on-chain registry for **BLE (Bluetooth Low Energy)** tag swarms. It enables fleet owners to manage large groups of tags (approximately 10,000–20,000 per swarm) and link them to backend service providers using cryptographic membership proofs. Individual tags are never enumerated on-chain; membership is verified via XOR filter.
+
+The system resolves the full path from a raw BLE signal to a verified service URL entirely on-chain, without a centralized indexer, while preserving the privacy of individual tags within each swarm.
+
+### 1.2 Architecture
+
+```mermaid
+graph TB
+ subgraph NFTs["Identity Layer (ERC-721)"]
+ FI["FleetIdentity
SFID
tokenId = (regionKey << 128) | uuid"]
+ SP["ServiceProvider
SSV
tokenId = keccak256(url)"]
+ end
+
+ subgraph Registries["Registry Layer"]
+ REG["SwarmRegistry
L1: SSTORE2 filter storage
Universal: native bytes storage"]
+ end
+
+ subgraph Actors
+ FO(("Fleet
Owner"))
+ PRV(("Service
Provider"))
+ ANY(("Client /
Purger"))
+ end
+
+ FO -- "registerFleet* / claimUuid" --> FI
+ FO -- "registerSwarm / update / delete" --> REG
+ PRV -- "registerProvider(url)" --> SP
+ PRV -- "acceptSwarm / rejectSwarm" --> REG
+ ANY -- "checkMembership / purge" --> REG
+ ANY -- "buildHighestBondedUuidBundle" --> FI
+
+ REG -. "uuidOwner(fleetUuid)" .-> FI
+ REG -. "ownerOf(providerId)" .-> SP
+
+ style FI fill:#4a9eff,color:#fff
+ style SP fill:#4a9eff,color:#fff
+ style REG fill:#ff9f43,color:#fff
+ style FO fill:#2ecc71,color:#fff
+ style PRV fill:#2ecc71,color:#fff
+ style ANY fill:#95a5a6,color:#fff
+```
+
+### 1.3 Core Components
+
+| Contract | Role | Identity | Token |
+| :------------------------- | :--------------------------------------- | :---------------------------------------------- | :---- |
+| **FleetIdentity** | Fleet registry (ERC-721 Enumerable) | `(regionKey << 128) \| uint128(uuid)` | SFID |
+| **ServiceProvider** | Backend URL registry (ERC-721) | `keccak256(url)` | SSV |
+| **SwarmRegistryL1** | Tag group registry (Ethereum L1) | `keccak256(fleetUuid, filter, fpSize, tagType)` | — |
+| **SwarmRegistryUniversal** | Tag group registry (ZkSync Era, all EVM) | `keccak256(fleetUuid, filter, fpSize, tagType)` | — |
+
+All contracts are **DAO-owned** (UUPS upgradeable) during initial operation, allowing parameter tuning and bug fixes. Once mature and stable, an upgrade can renounce ownership to make them fully **permissionless**. Access control is via NFT ownership; FleetIdentity requires an ERC-20 bond (e.g., NODL) as an anti-spam mechanism.
+
+### 1.4 Registry Variants
+
+Two SwarmRegistry variants exist for different deployment targets:
+
+| Variant | Chain | Filter Storage | Deletion Behavior |
+| :------------------------- | :------------------ | :-------------------------------------------- | :-------------------------------- |
+| **SwarmRegistryL1** | Ethereum L1 | SSTORE2 (contract bytecode via `EXTCODECOPY`) | Struct cleared; bytecode persists |
+| **SwarmRegistryUniversal** | ZkSync Era, all EVM | `mapping(uint256 => bytes)` | Full deletion, gas refund |
+
+SwarmRegistryL1 uses `SSTORE2` for gas-efficient reads on L1 but relies on `EXTCODECOPY`, which is unsupported on ZkSync Era. SwarmRegistryUniversal uses native `bytes` storage with full deletion support and exposes `getFilterData(swarmId)` for off-chain filter retrieval. Both registries use **O(1) swap-and-pop** for removing swarms from the `uuidSwarms` array, tracked via the `swarmIndexInUuid` mapping.
+
+### 1.5 Privacy Model
+
+The system provides non-enumerating tag verification — individual tags are not listed on-chain; membership is proven via XOR filter.
+
+| Data | Visibility | Notes |
+| :---------- | :--------------- | :------------------------------------------- |
+| UUID | Public | Required for iOS background beacon detection |
+| Major/Minor | Filter-protected | Hashed, not enumerated |
+| MAC address | Android-only | iOS does not expose BLE MAC addresses |
+
+**Limitation:** The UUID must be public for iOS `CLBeaconRegion` background monitoring. The system protects the specific Major/Minor combinations (and other tag-specific fields) within each swarm through hashing and XOR filtering, which introduces an intentional, tunable false-positive rate that further obscures individual tag membership.
+
+
+
+## 2. Data Model & Interfaces
+
+### 2.1 Public Interfaces
+
+| Interface | Description |
+| :-------------------------------- | :----------------------------------------------------------------------------- |
+| `interfaces/IFleetIdentity.sol` | FleetIdentity public API (ERC721Enumerable) |
+| `interfaces/IServiceProvider.sol` | ServiceProvider public API (ERC721) |
+| `interfaces/ISwarmRegistry.sol` | Common registry interface (L1 & Universal) |
+| `interfaces/SwarmTypes.sol` | Shared enums: `RegistrationLevel`, `SwarmStatus`, `TagType`, `FingerprintSize` |
+
+### 2.2 Contract Classes
+
+```mermaid
+classDiagram
+ class FleetIdentity {
+ +IERC20 BOND_TOKEN
+ +uint256 BASE_BOND
+ +uint256 TIER_CAPACITY = 10
+ +uint256 MAX_TIERS = 24
+ +uint256 COUNTRY_BOND_MULTIPLIER = 16
+ +uint256 MAX_BONDED_UUID_BUNDLE_SIZE = 20
+ +mapping uuidOwner : bytes16 → address
+ +mapping uuidOperator : bytes16 → address
+ +mapping uuidLevel : bytes16 → RegistrationLevel
+ +mapping uuidTokenCount : bytes16 → uint256
+ +mapping uuidTotalTierBonds : bytes16 → uint256
+ +mapping regionTierCount : uint32 → uint256
+ +mapping fleetTier : uint256 → uint256
+ --
+ +claimUuid(uuid, operator) → tokenId
+ +registerFleetLocal(uuid, cc, admin, tier) → tokenId
+ +registerFleetCountry(uuid, cc, tier) → tokenId
+ +promote(tokenId)
+ +reassignTier(tokenId, tier)
+ +burn(tokenId)
+ +setOperator(uuid, operator)
+ +operatorOf(uuid) → address
+ +buildHighestBondedUuidBundle(cc, admin) → (uuids, count)
+ +buildCountryOnlyBundle(cc) → (uuids, count)
+ +localInclusionHint(cc, admin) → (tier, bond)
+ +countryInclusionHint(cc) → (tier, bond)
+ +computeTokenId(uuid, regionKey) → tokenId
+ +makeAdminRegion(cc, admin) → regionKey
+ +tokenUuid(tokenId) → bytes16
+ +tokenRegion(tokenId) → uint32
+ +tierBond(tier, isCountry) → uint256
+ }
+
+ class ServiceProvider {
+ +mapping providerUrls : uint256 → string
+ +registerProvider(url) → tokenId
+ +updateUrl(tokenId, newUrl)
+ +burn(tokenId)
+ }
+
+ class SwarmRegistry {
+ +mapping swarms : uint256 → Swarm
+ +mapping uuidSwarms : bytes16 → uint256[]
+ +mapping swarmIndexInUuid : uint256 → uint256
+ +mapping filterData : uint256 → bytes
+ +registerSwarm(uuid, providerId, filter, fpSize, tagType) → swarmId
+ +updateSwarmProvider(swarmId, newProviderId)
+ +deleteSwarm(swarmId)
+ +acceptSwarm(swarmId)
+ +rejectSwarm(swarmId)
+ +purgeOrphanedSwarm(swarmId)
+ +isSwarmValid(swarmId) → (fleetValid, providerValid)
+ +checkMembership(swarmId, tagHash) → bool
+ }
+```
+
+### 2.3 Swarm Struct
+
+```solidity
+struct Swarm {
+ bytes16 fleetUuid; // UUID that owns this swarm
+ uint256 providerId; // ServiceProvider token ID
+ uint32 filterLength; // XOR filter byte length
+ uint8 fingerprintSize; // Fingerprint bits (1–16)
+ SwarmStatus status; // Registration state
+ TagType tagType; // Tag identity scheme
+}
+```
+
+### 2.4 Enumerations
+
+#### SwarmStatus
+
+| Value | Description |
+| :----------- | :------------------------- |
+| `REGISTERED` | Awaiting provider approval |
+| `ACCEPTED` | Provider approved; active |
+| `REJECTED` | Provider rejected |
+
+#### RegistrationLevel
+
+| Value | Region Key | Description |
+| :------------ | :--------- | :----------------- |
+| `None` (0) | — | Not registered |
+| `Owned` (1) | 0 | Claimed, no region |
+| `Local` (2) | ≥ 1024 | Admin area |
+| `Country` (3) | 1–999 | Country-wide |
+
+### 2.5 Region Key Encoding
+
+Geographic regions are encoded into a single `uint32` value:
+
+```
+Country: regionKey = countryCode (1–999)
+Admin Area: regionKey = (countryCode << 10) | adminCode (≥ 1024)
+```
+
+Country codes follow ISO 3166-1 numeric. Admin codes are 1-indexed integers mapping ISO 3166-2 subdivisions (see [Appendix A](#appendix-a-iso-3166-geographic-reference)).
+
+**Examples:**
+
+| Location | Country Code | Admin Code | Region Key |
+| :------------ | -----------: | ---------: | ---------: |
+| United States | 840 | — | 840 |
+| US-California | 840 | 5 | 860,165 |
+| Canada | 124 | — | 124 |
+| CA-Alberta | 124 | 1 | 127,001 |
+
+### 2.6 Token ID Encoding
+
+Fleet tokens encode both the region and UUID into a single `uint256`:
+
+```
+tokenId = (regionKey << 128) | uint256(uint128(uuid))
+```
+
+- **Bits 0–127:** UUID (Proximity UUID as `bytes16`)
+- **Bits 128–159:** Region key
+
+This allows the same UUID to be registered in multiple regions, each producing a distinct token. Helper functions:
+
+```solidity
+bytes16 uuid = fleetIdentity.tokenUuid(tokenId);
+uint32 region = fleetIdentity.tokenRegion(tokenId);
+uint256 tokenId = fleetIdentity.computeTokenId(uuid, regionKey);
+uint32 adminRegion = fleetIdentity.makeAdminRegion(countryCode, adminCode);
+```
+
+
+
+## 3. BLE Tag Types & UUID Encoding
+
+### 3.1 Overview
+
+The `bytes16` UUID field stores the **fleet-level identifier** derived from BLE advertisement data. It serves two critical roles:
+
+1. **Background scan registration:** Edge scanners (e.g., iOS, Android) must pre-register UUIDs with the OS to receive BLE advertisements while backgrounded. The UUID must be reconstructable from observed BLE data so scanners can build the correct OS-level filter.
+2. **Swarm lookup scoping:** Each UUID maps to one or more swarms on-chain. Swarm resolution iterates all swarms under a UUID, so UUID specificity directly affects lookup performance.
+
+The UUID deliberately excludes tag-specific fields (e.g., iBeacon Major/Minor, individual sensor IDs). Those fields appear only in the **tag hash** used for XOR filter membership verification.
+
+### 3.2 UUID Design Trade-offs
+
+Fleet owners should consider these trade-offs when encoding their UUID:
+
+| Concern | More specific UUID (uses more bytes) | Less specific UUID (uses fewer bytes) |
+| :--------------- | :------------------------------------------ | :------------------------------------------------------------ |
+| **Lookup speed** | Fewer swarms per UUID → faster resolution | Many swarms per UUID → linear search overhead |
+| **Uniqueness** | Low collision risk when claiming on-chain | Higher collision risk — another owner may claim the same UUID |
+| **Privacy** | More fleet metadata publicly visible | Less exposed, more private |
+| **Scan filter** | Tighter OS-level filter → fewer false wakes | Broader filter → more false wakes |
+
+**Recommendation:** Use as much of the 16-byte UUID capacity as is acceptable for public exposure.
+
+### 3.3 TagType Enumeration
+
+The `TagType` enum (defined in `interfaces/SwarmTypes.sol`) determines how tag identities are constructed:
+
+| TagType | Tag Hash Format | UUID Encoding | Use Case |
+| :--------------------- | :----------------------------------- | :--------------------------------- | :-------------------- |
+| `IBEACON_PAYLOAD_ONLY` | UUID ∥ Major ∥ Minor (20B) | Proximity UUID (16B) | iBeacon / AltBeacon |
+| `IBEACON_INCLUDES_MAC` | UUID ∥ Major ∥ Minor ∥ MAC (26B) | Proximity UUID (16B) | Anti-spoofing iBeacon |
+| `VENDOR_ID` | CompanyID ∥ FullVendorData | Len ∥ CompanyID ∥ FleetID (16B) | Manufacturer-specific |
+| `EDDYSTONE_UID` | Namespace ∥ Instance (16B) | Namespace ∥ Instance (16B) | Eddystone-UID |
+| `SERVICE_DATA` | ExpandedServiceUUID128 ∥ ServiceData | Bluetooth Base UUID expanded (16B) | GATT Service Data |
+
+### 3.4 iBeacon / AltBeacon
+
+For iBeacon advertisements, the UUID stores the standard **16-byte Proximity UUID** defined by Apple's iBeacon specification. Major (2B) and Minor (2B) are excluded from the UUID and used only in tag hash construction.
+
+AltBeacon uses a structurally identical 20-byte Beacon ID (16B ID + 2B major + 2B minor) and is categorized under `IBEACON_PAYLOAD_ONLY` or `IBEACON_INCLUDES_MAC`.
+
+```
+UUID = Proximity UUID (16B)
+Tag Hash = keccak256(UUID ∥ Major ∥ Minor) // IBEACON_PAYLOAD_ONLY
+Tag Hash = keccak256(UUID ∥ Major ∥ Minor ∥ NormMAC) // IBEACON_INCLUDES_MAC
+```
+
+#### MAC Address Normalization (IBEACON_INCLUDES_MAC)
+
+| Address Type Bits | MAC Type | Action |
+| :---------------- | :--------------- | :------------------------------- |
+| `00` | Public / Static | Use real MAC |
+| `01`, `11` | Random / Private | Replace with `FF:FF:FF:FF:FF:FF` |
+
+This normalization supports rotating privacy MACs while still enabling validation that a tag is a privacy-address device.
+
+### 3.5 Eddystone-UID
+
+Eddystone-UID frames broadcast a **10-byte Namespace ID** and a **6-byte Instance ID**, totaling exactly 16 bytes. Both fields are static and map directly into the UUID:
+
+```
+UUID = Namespace (10B) ∥ Instance (6B)
+Tag Hash = keccak256(Namespace ∥ Instance)
+```
+
+### 3.6 Eddystone-EID (Not Supported)
+
+Eddystone-EID broadcasts a **rotating 8-byte ephemeral identifier** derived from a static 16-byte Identity Key and a time counter. Because the EID changes periodically and the Identity Key is never transmitted over the air, edge scanners cannot filter EID beacons by fleet. This makes EID incompatible with edge-filtered swarm membership and it is therefore not assigned a `TagType`.
+
+### 3.7 VENDOR_ID (Manufacturer Specific Data, AD Type 0xFF)
+
+BLE Manufacturer Specific Data contains a **2-byte Company ID** (assigned by Bluetooth SIG) followed by vendor-defined payload. Since Company ID alone typically identifies the manufacturer — not the fleet owner — additional fleet-identifying bytes from the vendor data should be included when possible.
+
+**UUID encoding** — length-prefixed for unambiguous decoding by scanners:
+
+```
+UUID (16 bytes):
+┌───────┬───────────┬───────────────────────────────────┐
+│ Len │ CompanyID │ FleetIdentifier + zero-padding │
+│ (1B) │ (2B, BE) │ (13B) │
+└───────┴───────────┴───────────────────────────────────┘
+
+Len = total meaningful bytes after the Len byte
+ = 2 (CompanyID) + N (FleetIdentifier bytes)
+ Range: 2 (company-only) to 15 (2 + 13B fleet ID)
+```
+
+**Scanner decode logic:**
+
+```
+CompanyID = UUID[1:3]
+FleetIdLen = UUID[0] - 2
+FleetId = UUID[3 : 3 + FleetIdLen]
+→ Register BLE filter: AD Type 0xFF, CompanyID, data prefix = FleetId
+```
+
+**Tag hash** uses the full vendor data (not truncated):
+
+```
+Tag Hash = keccak256(CompanyID ∥ FullVendorData)
+```
+
+**Examples:**
+
+| Company | Company ID | Fleet Identifier | Len | UUID (hex) |
+| :------------ | :--------- | :---------------------- | --: | :------------------------------------ |
+| Estimote | `015D` | OrgID `AABBCCDD` (4B) | 6 | `06 015D AABBCCDD 000000000000000000` |
+| Tile | `0113` | (company only) | 2 | `02 0113 00000000000000000000000000` |
+| Custom vendor | `1234` | `0102030405060708` (8B) | 10 | `0A 1234 0102030405060708 0000000000` |
+
+### 3.8 SERVICE_DATA (GATT Service Data, AD Types 0x16 / 0x20 / 0x21)
+
+BLE Service Data advertisements carry a Service UUID plus associated data. The AD type determines the UUID size:
+
+| AD Type | Service UUID Size | Example |
+| :------ | :---------------- | :------------------ |
+| `0x16` | 16-bit | Heart Rate (0x180D) |
+| `0x20` | 32-bit | Custom (0x12345678) |
+| `0x21` | 128-bit | Vendor-specific |
+
+**UUID encoding** — canonical expansion using the Bluetooth Base UUID:
+
+```
+Bluetooth Base UUID: 00000000-0000-1000-8000-00805F9B34FB
+
+16-bit → 0000XXXX-0000-1000-8000-00805F9B34FB
+32-bit → XXXXXXXX-0000-1000-8000-00805F9B34FB
+128-bit → stored as-is
+```
+
+The expansion is lossless and reversible: the scanner determines the original UUID size from the AD type in the BLE advertisement.
+
+```
+UUID = Expand(ServiceUUID) → bytes16
+Tag Hash = keccak256(ExpandedServiceUUID128 ∥ ServiceData)
+```
+
+### 3.9 Tag Hash Construction Summary
+
+```mermaid
+flowchart TD
+ A[Read swarm.tagType] --> B{TagType?}
+
+ B -->|IBEACON_PAYLOAD_ONLY| C["UUID ∥ Major ∥ Minor (20B)"]
+ B -->|IBEACON_INCLUDES_MAC| D{MAC type?}
+ B -->|VENDOR_ID| E["CompanyID ∥ FullVendorData"]
+ B -->|EDDYSTONE_UID| F["Namespace ∥ Instance (16B)"]
+ B -->|SERVICE_DATA| K["ExpandedServiceUUID128 ∥ ServiceData"]
+
+ D -->|Public| G["UUID ∥ Major ∥ Minor ∥ realMAC (26B)"]
+ D -->|Random| H["UUID ∥ Major ∥ Minor ∥ FF:FF:FF:FF:FF:FF"]
+
+ C --> I["tagHash = keccak256(tagId)"]
+ G --> I
+ H --> I
+ E --> I
+ F --> I
+ K --> I
+
+ I --> J["checkMembership(swarmId, tagHash)"]
+
+ style I fill:#4a9eff,color:#fff
+ style J fill:#2ecc71,color:#fff
+```
+
+
+
+## 4. Fleet Registration
+
+### 4.1 Registration Paths
+
+```mermaid
+stateDiagram-v2
+ [*] --> None : (default)
+
+ None --> Owned : claimUuid()
+ None --> Local : registerFleetLocal()
+ None --> Country : registerFleetCountry()
+
+ Owned --> Local : registerFleetLocal()
+ Owned --> Country : registerFleetCountry()
+ Owned --> [*] : burn() [owner]
+
+ Local --> Owned : burn() [operator, last token]
+ Local --> Local : burn() [operator, not last]
+
+ Country --> Owned : burn() [operator, last token]
+ Country --> Country : burn() [operator, not last]
+```
+
+### 4.2 Direct Registration
+
+#### Local (Admin Area)
+
+```solidity
+// 1. Approve bond
+NODL.approve(fleetIdentityAddress, requiredBond);
+
+// 2. Get recommended tier (free off-chain call)
+(uint256 tier, uint256 bond) = fleetIdentity.localInclusionHint(840, 5);
+
+// 3. Register
+uint256 tokenId = fleetIdentity.registerFleetLocal(uuid, 840, 5, tier);
+```
+
+#### Country
+
+```solidity
+// 1. Approve bond
+NODL.approve(fleetIdentityAddress, requiredBond);
+
+// 2. Get recommended tier
+(uint256 tier, uint256 bond) = fleetIdentity.countryInclusionHint(840);
+
+// 3. Register
+uint256 tokenId = fleetIdentity.registerFleetCountry(uuid, 840, tier);
+```
+
+When registering without a prior `claimUuid()`, the caller pays `BASE_BOND + tierBond` and becomes both owner and operator of the UUID.
+
+### 4.3 Claim-First Flow (with Operator Delegation)
+
+Reserve a UUID with a cold wallet, then delegate registration to a hot-wallet operator:
+
+```solidity
+// 1. Owner claims UUID, designates operator (costs BASE_BOND)
+NODL.approve(fleetIdentityAddress, BASE_BOND);
+uint256 ownedTokenId = fleetIdentity.claimUuid(uuid, operatorAddress);
+
+// 2. Operator registers later (pays full tier bond)
+NODL.approve(fleetIdentityAddress, tierBond); // as operator
+uint256 tokenId = fleetIdentity.registerFleetLocal(uuid, 840, 5, tier);
+// Burns owned token, mints regional token to owner
+```
+
+```mermaid
+sequenceDiagram
+ actor Owner
+ actor Operator
+ participant FI as FleetIdentity
+ participant TOKEN as BOND_TOKEN
+
+ Owner->>TOKEN: approve(FleetIdentity, BASE_BOND)
+ Owner->>+FI: claimUuid(uuid, operatorAddress)
+ FI->>TOKEN: transferFrom(owner, this, BASE_BOND)
+ FI-->>-Owner: tokenId = uint128(uuid)
+
+ Note over Operator: Later (only operator can register)...
+
+ Operator->>TOKEN: approve(FleetIdentity, tierBond)
+ Operator->>+FI: registerFleetLocal(uuid, cc, admin, tier)
+ Note over FI: Burns owned token, mints to owner
+ FI->>TOKEN: transferFrom(operator, this, tierBond)
+ FI-->>-Operator: tokenId = ((cc<<10|admin)<<128) | uuid
+```
+
+### 4.4 Operator Model
+
+**Key principles:**
+
+- **Only the operator can register** for owned UUIDs (the owner cannot register directly after claiming).
+- **Fresh registration:** The caller becomes both owner and operator, paying `BASE_BOND + tierBond`.
+- **Owned → Registered:** The operator pays the full `tierBond` (BASE_BOND already paid via `claimUuid`).
+- **Multi-region:** The operator pays `tierBond` for each additional region.
+
+**Setting or changing the operator:**
+
+```solidity
+// Owner sets operator (transfers ALL tier bonds atomically)
+fleetIdentity.setOperator(uuid, newOperator);
+// - Pulls total tier bonds from new operator
+// - Refunds total tier bonds to old operator
+// - Uses O(1) storage lookup (uuidTotalTierBonds)
+
+// Owner clears operator (reverts to owner-managed)
+fleetIdentity.setOperator(uuid, address(0));
+// - Refunds all tier bonds to old operator
+// - Pulls all tier bonds from owner
+
+// Check current operator (returns owner if none set)
+address manager = fleetIdentity.operatorOf(uuid);
+```
+
+**Permission summary:**
+
+| Action | Who Can Call |
+| :------------------------------- | :------------------------------------- |
+| Register for owned UUID | Operator only |
+| Promote / demote / reassign tier | Operator (or owner if no operator set) |
+| Burn registered token | Operator only |
+| Burn owned-only token | Owner only |
+| Set / change operator | Owner only |
+| Transfer owned-only token | Owner (ERC-721 transfer) |
+
+### 4.5 Multi-Region Registration
+
+The same UUID can hold multiple tokens at the **same level** (all Local or all Country):
+
+```solidity
+fleetIdentity.registerFleetLocal(uuid, 840, 5, 0); // US-California ✓
+fleetIdentity.registerFleetLocal(uuid, 276, 1, 0); // DE-Baden-Württemberg ✓
+fleetIdentity.registerFleetCountry(uuid, 392, 0); // Japan ✗ UuidLevelMismatch()
+```
+
+Each region pays its own tier bond independently.
+
+### 4.6 Burning
+
+| State | Who Burns | Last Token? | Result |
+| :--------- | :-------- | :---------- | :------------------------------------------------- |
+| Owned | Owner | — | Refunds BASE_BOND, clears UUID ownership |
+| Registered | Operator | No | Refunds tier bond, stays registered |
+| Registered | Operator | Yes | Refunds tier bond, mints owned-only token to owner |
+
+After the operator burns the last registered token, the owner receives an owned-only token and must burn it separately to fully release the UUID.
+
+### 4.7 Owned Token Transfer
+
+Owned-only tokens transfer UUID ownership via standard ERC-721 transfer:
+
+```solidity
+fleetIdentity.transferFrom(alice, bob, tokenId);
+// uuidOwner[uuid] = bob
+```
+
+Registered tokens can also transfer but do not change `uuidOwner`.
+
+### 4.8 Inclusion Hints
+
+View functions that recommend the cheapest tier guaranteeing bundle inclusion:
+
+```solidity
+// Local: simulates bundle for a specific admin area
+(uint256 tier, uint256 bond) = fleetIdentity.localInclusionHint(cc, admin);
+
+// Country: scans ALL active admin areas (unbounded, free off-chain)
+(uint256 tier, uint256 bond) = fleetIdentity.countryInclusionHint(cc);
+```
+
+
+
+## 5. Tier Economics & Bond System
+
+### 5.1 System Parameters
+
+| Parameter | Value |
+| :-------------------------- | :--------------------------- |
+| **Tier Capacity** | 10 members per tier |
+| **Max Tiers** | 24 per region |
+| **Max Bundle Size** | 20 UUIDs returned to clients |
+| **Country Bond Multiplier** | 16× local bond |
+
+### 5.2 Bond Formula
+
+| Level | Formula | Who Pays |
+| :------ | :------------------------ | :------------------------------ |
+| Owned | `BASE_BOND` | Owner |
+| Local | `BASE_BOND × 2^tier` | Operator (owner paid BASE_BOND) |
+| Country | `BASE_BOND × 16 × 2^tier` | Operator (owner paid BASE_BOND) |
+
+**Example (BASE_BOND = 100 NODL):**
+
+| Tier | Local Bond | Country Bond |
+| :--- | ---------: | -----------: |
+| 0 | 100 | 1,600 |
+| 1 | 200 | 3,200 |
+| 2 | 400 | 6,400 |
+| 3 | 800 | 12,800 |
+
+### 5.3 Economic Design
+
+Country fleets pay 16× more than local fleets but appear in **all** admin-area bundles within their country. This gives local fleets a significant cost advantage: a local fleet can reach tier 4 for the same cost a country fleet pays for tier 0.
+
+### 5.4 Runtime Bond Parameter Configuration
+
+The contract owner can adjust bond parameters at runtime for future registrations:
+
+```solidity
+fleetIdentity.setBaseBond(newBaseBond);
+fleetIdentity.setCountryBondMultiplier(newMultiplier);
+fleetIdentity.setBondParameters(newBaseBond, newMultiplier);
+```
+
+**Tier-0 bond tracking:** To ensure fair refunds when parameters change, each token stores its "tier-0 equivalent bond" at registration time:
+
+- **Local tokens:** `tokenTier0Bond[tokenId] = baseBond`
+- **Country tokens:** `tokenTier0Bond[tokenId] = baseBond * countryBondMultiplier`
+- **Bond at tier K:** `tokenTier0Bond[tokenId] << K` (bitshift = multiply by 2^K)
+
+This stores a single `uint256` per token and enables O(1) promote/demote/burn operations with accurate refunds regardless of parameter changes.
+
+For UUID ownership bonds, `uuidOwnershipBondPaid[uuid]` tracks the amount paid at claim or first registration.
+
+### 5.5 Tier Management
+
+#### Promote
+
+```solidity
+// Quick promote to next tier
+fleetIdentity.promote(tokenId);
+
+// Reassign to any tier (promotes or demotes)
+fleetIdentity.reassignTier(tokenId, targetTier);
+// Promotion: pulls additional bond from operator
+// Demotion: refunds bond difference to operator
+```
+
+```mermaid
+sequenceDiagram
+ actor OP as Operator
+ participant FI as FleetIdentity
+ participant TOKEN as BOND_TOKEN
+
+ alt Promote
+ OP->>TOKEN: approve(additionalBond)
+ OP->>+FI: reassignTier(tokenId, higherTier)
+ FI->>TOKEN: transferFrom(operator, this, diff)
+ FI-->>-OP: FleetPromoted
+ else Demote
+ OP->>+FI: reassignTier(tokenId, lowerTier)
+ FI->>TOKEN: transfer(operator, refund)
+ FI-->>-OP: FleetDemoted
+ end
+```
+
+Only the operator (or owner if no operator is set) can promote, demote, or reassign tiers.
+
+
+
+## 6. Swarm Operations
+
+### 6.1 Registration Flow
+
+A fleet owner groups tags into a swarm (approximately 10,000–20,000 tags) and registers the group on-chain with a pre-computed XOR filter.
+
+```mermaid
+sequenceDiagram
+ actor FO as Fleet Owner
+ actor PRV as Provider Owner
+ participant SR as SwarmRegistry
+ participant FI as FleetIdentity
+ participant SP as ServiceProvider
+
+ Note over FO: Build XOR filter off-chain
+
+ FO->>+SR: registerSwarm(fleetUuid, providerId, filter, fpSize, tagType)
+ SR->>FI: uuidOwner(fleetUuid)
+ SR->>SP: ownerOf(providerId)
+ Note over SR: swarmId = keccak256(fleetUuid, filter, fpSize, tagType)
+ SR-->>-FO: swarmId (status: REGISTERED)
+
+ PRV->>+SR: acceptSwarm(swarmId)
+ SR->>SP: ownerOf(providerId)
+ SR-->>-PRV: status: ACCEPTED
+```
+
+**Registration parameters:**
+
+| Parameter | Type | Description |
+| :----------- | :-------- | :--------------------------- |
+| `fleetUuid` | `bytes16` | UUID that owns this swarm |
+| `providerId` | `uint256` | ServiceProvider token ID |
+| `filter` | `bytes` | XOR filter data |
+| `fpSize` | `uint8` | Fingerprint size (1–16 bits) |
+| `tagType` | `TagType` | Tag identity scheme |
+
+### 6.2 Swarm ID Derivation
+
+Swarm IDs are **deterministic** and collision-free:
+
+```solidity
+swarmId = uint256(keccak256(abi.encode(fleetUuid, filterData, fingerprintSize, tagType)))
+```
+
+Swarm identity is based on fleet, filter, fingerprint size, and tag type. The `providerId` is mutable and is not part of the identity. Duplicate registration of the same tuple reverts with `SwarmAlreadyExists()`. The `computeSwarmId` function is `public pure` and can be called off-chain at zero cost.
+
+### 6.3 XOR Filter Construction
+
+#### Off-Chain Steps
+
+1. **Build TagIDs** for all tags per the TagType schema (Section 3).
+2. **Hash each TagID:** `tagHash = keccak256(tagId)`.
+3. **Construct XOR filter** using the Peeling Algorithm.
+4. **Submit filter** via `registerSwarm()`.
+
+#### Filter Membership Verification (On-Chain)
+
+The contract uses **3-hash XOR logic**:
+
+```
+Input: h = keccak256(tagId)
+M = filterLength × 8 / fingerprintSize (number of fingerprint slots)
+
+h1 = uint32(h) % M
+h2 = uint32(h >> 32) % M
+h3 = uint32(h >> 64) % M
+fp = (h >> 96) & ((1 << fingerprintSize) - 1)
+
+Member if: Filter[h1] ⊕ Filter[h2] ⊕ Filter[h3] == fp
+```
+
+The configurable `fingerprintSize` (1–16 bits) controls the false-positive rate: larger fingerprints yield fewer false positives but require proportionally more storage per tag.
+
+### 6.4 Provider Approval
+
+```mermaid
+stateDiagram-v2
+ [*] --> REGISTERED : registerSwarm()
+
+ REGISTERED --> ACCEPTED : acceptSwarm()
+ REGISTERED --> REJECTED : rejectSwarm()
+
+ ACCEPTED --> REGISTERED : updateSwarm*()
+ REJECTED --> REGISTERED : updateSwarm*()
+
+ REGISTERED --> [*] : deleteSwarm() / purge
+ ACCEPTED --> [*] : deleteSwarm() / purge
+ REJECTED --> [*] : deleteSwarm() / purge
+```
+
+| Action | Caller | Effect |
+| :--------------------- | :------------- | :---------------- |
+| `acceptSwarm(swarmId)` | Provider owner | status → ACCEPTED |
+| `rejectSwarm(swarmId)` | Provider owner | status → REJECTED |
+
+Only `ACCEPTED` swarms pass `checkMembership()`.
+
+### 6.5 Swarm Updates
+
+The fleet owner can change the service provider, which resets status to `REGISTERED` and requires fresh provider approval:
+
+```solidity
+swarmRegistry.updateSwarmProvider(swarmId, newProviderId);
+```
+
+The XOR filter is immutable and part of swarm identity. To change the filter, delete the swarm and register a new one.
+
+### 6.6 Deletion
+
+```solidity
+swarmRegistry.deleteSwarm(swarmId);
+```
+
+Removes the swarm from `uuidSwarms[]` (O(1) swap-and-pop), deletes `swarms[swarmId]`, and for the Universal variant also deletes `filterData[swarmId]`.
+
+
+
+## 7. Lifecycle & State Machines
+
+### 7.1 UUID Registration States
+
+```mermaid
+stateDiagram-v2
+ [*] --> None
+
+ None --> Owned : claimUuid()
+ None --> Local : registerFleetLocal()
+ None --> Country : registerFleetCountry()
+
+ Owned --> Local : registerFleetLocal() [operator]
+ Owned --> Country : registerFleetCountry() [operator]
+ Owned --> [*] : burn() [owner]
+
+ Local --> Owned : burn() [operator, last token]
+ Local --> Local : burn() [operator, not last]
+ Local --> [*] : burn() [owner, after owned-only]
+
+ Country --> Owned : burn() [operator, last token]
+ Country --> Country : burn() [operator, not last]
+ Country --> [*] : burn() [owner, after owned-only]
+
+ note right of Owned : regionKey = 0
+ note right of Local : regionKey ≥ 1024
+ note right of Country : regionKey 1-999
+```
+
+### 7.2 State Transition Table
+
+| From | To | Function | Who Calls | Bond Effect |
+| :------------ | :------ | :----------------------- | :-------- | :------------------------------------------------------------- |
+| None | Owned | `claimUuid()` | Anyone | Pull BASE_BOND from caller (becomes owner) |
+| None | Local | `registerFleetLocal()` | Anyone | Pull BASE_BOND + tierBond from caller (becomes owner+operator) |
+| None | Country | `registerFleetCountry()` | Anyone | Pull BASE_BOND + tierBond from caller (becomes owner+operator) |
+| Owned | Local | `registerFleetLocal()` | Operator | Pull tierBond from operator |
+| Owned | Country | `registerFleetCountry()` | Operator | Pull tierBond from operator |
+| Local/Country | Owned | `burn()` | Operator | Refund tierBond to operator (last token mints owned-only) |
+| Owned | None | `burn()` | Owner | Refund BASE_BOND to owner |
+| Local/Country | — | `burn()` | Operator | Refund tierBond to operator (not last token, stays registered) |
+
+### 7.3 Swarm Status Effects
+
+| Status | `checkMembership` | Provider Action Required |
+| :--------- | :---------------- | :------------------------------------- |
+| REGISTERED | Reverts | Accept or reject |
+| ACCEPTED | Works | None |
+| REJECTED | Reverts | None (fleet owner can update to retry) |
+
+### 7.4 Fleet Token Bond Flow
+
+```mermaid
+sequenceDiagram
+ participant TOKEN as BOND_TOKEN
+ participant FI as FleetIdentity
+ participant Owner
+ participant Operator
+
+ Note over FI: Fresh Registration (caller = owner+operator)
+ FI->>TOKEN: transferFrom(caller, this, BASE_BOND + tierBond)
+
+ Note over FI: Owned → Registered (operator only)
+ FI->>TOKEN: transferFrom(operator, this, tierBond)
+
+ Note over FI: Multi-region (operator only)
+ FI->>TOKEN: transferFrom(operator, this, tierBond)
+
+ Note over FI: Promotion (operator pays)
+ FI->>TOKEN: transferFrom(operator, this, additionalBond)
+
+ Note over FI: Demotion (operator receives)
+ FI->>TOKEN: transfer(operator, refund)
+
+ Note over FI: Change Operator (O(1) via uuidTotalTierBonds)
+ FI->>TOKEN: transferFrom(newOperator, this, totalTierBonds)
+ FI->>TOKEN: transfer(oldOperator, totalTierBonds)
+
+ Note over FI: Burn registered token (operator)
+ FI->>TOKEN: transfer(operator, tierBond)
+
+ Note over FI: Burn last registered token (operator)
+ Note over FI: Mints owned-only token to owner
+ FI->>TOKEN: transfer(operator, tierBond)
+
+ Note over FI: Burn owned-only token (owner)
+ FI->>TOKEN: transfer(owner, BASE_BOND)
+```
+
+### 7.5 Orphan Lifecycle
+
+When a fleet or provider NFT is burned, swarms referencing it become **orphaned**.
+
+```mermaid
+flowchart TD
+ ACTIVE[Swarm Active] --> BURN{NFT burned?}
+ BURN -->|No| ACTIVE
+ BURN -->|Yes| ORPHAN[Swarm Orphaned]
+ ORPHAN --> CHECK[isSwarmValid returns false]
+ CHECK --> PURGE[Anyone: purgeOrphanedSwarm]
+ PURGE --> DELETED[Swarm Deleted + Gas Refund]
+```
+
+**Detection:**
+
+```solidity
+(bool fleetValid, bool providerValid) = swarmRegistry.isSwarmValid(swarmId);
+```
+
+**Cleanup:** Anyone can purge orphaned swarms. The caller receives the storage gas refund as an incentive:
+
+```solidity
+swarmRegistry.purgeOrphanedSwarm(swarmId);
+```
+
+**Orphan guards:** These operations revert with `SwarmOrphaned()` if either NFT is invalid:
+
+- `acceptSwarm(swarmId)`
+- `rejectSwarm(swarmId)`
+- `checkMembership(swarmId, tagHash)`
+
+### 7.6 Region Index Maintenance
+
+```mermaid
+flowchart LR
+ REG[registerFleet*] --> FIRST{First in region?}
+ FIRST -->|Yes| ADD[Add to activeCountries / activeAdminAreas]
+ FIRST -->|No| SKIP[Already indexed]
+
+ BURN[burn / demotion] --> EMPTY{Region empty?}
+ EMPTY -->|Yes| REMOVE[Remove from index]
+ EMPTY -->|No| KEEP[Keep]
+```
+
+Region indexes are automatically maintained — no manual intervention is needed.
+
+
+
+## 8. Client Discovery
+
+### 8.1 Geographic Bundle Discovery (Recommended)
+
+Use location-based priority bundles for efficient discovery. The bundle returns up to 20 UUIDs, priority-ordered:
+
+1. **Tier:** Higher tier first
+2. **Level:** Local before country (within same tier)
+3. **Time:** Earlier registration (within same tier and level)
+
+```mermaid
+sequenceDiagram
+ actor Client as EdgeBeaconScanner
+ participant FI as FleetIdentity
+ participant SR as SwarmRegistry
+ participant SP as ServiceProvider
+
+ Note over Client: Location: US-California (840, 5)
Detected: UUID, Major, Minor, MAC
+
+ Client->>+FI: buildHighestBondedUuidBundle(840, 5)
+ FI-->>-Client: (uuids[], count) — up to 20 UUIDs
+
+ Note over Client: Check if detectedUUID in bundle
+
+ Client->>+SR: uuidSwarms(uuid, 0)
+ SR-->>-Client: swarmId
+ Note over Client: Iterate until revert
+
+ Note over Client: Build tagHash per TagType
+ Client->>+SR: checkMembership(swarmId, tagHash)
+ SR-->>-Client: true
+
+ Client->>+SR: swarms(swarmId)
+ SR-->>-Client: providerId, status: ACCEPTED, ...
+
+ Client->>+SP: providerUrls(providerId)
+ SP-->>-Client: "https://api.example.com"
+
+ Note over Client: Connect to service
+```
+
+### 8.2 Direct UUID Lookup
+
+When the UUID is known but location is not:
+
+```solidity
+// Try regions
+uint32 localRegion = (840 << 10) | 5;
+uint256 tokenId = fleetIdentity.computeTokenId(uuid, localRegion);
+try fleetIdentity.ownerOf(tokenId) { /* found */ }
+catch { /* try country: computeTokenId(uuid, 840) */ }
+
+// Enumerate swarms
+for (uint i = 0; ; i++) {
+ try swarmRegistry.uuidSwarms(uuid, i) returns (uint256 swarmId) {
+ // process swarmId
+ } catch { break; }
+}
+```
+
+### 8.3 Region Enumeration (for Indexers)
+
+```solidity
+// Active countries
+uint16[] memory countries = fleetIdentity.getActiveCountries();
+
+// Active admin areas
+uint32[] memory adminAreas = fleetIdentity.getActiveAdminAreas();
+
+// Tier data
+uint256 tierCount = fleetIdentity.regionTierCount(regionKey);
+uint256[] memory tokenIds = fleetIdentity.getTierMembers(regionKey, tier);
+bytes16[] memory uuids = fleetIdentity.getTierUuids(regionKey, tier);
+```
+
+### 8.4 Complete Discovery Example
+
+```solidity
+function discoverService(
+ bytes16 uuid,
+ uint16 major,
+ uint16 minor,
+ bytes6 mac,
+ uint16 countryCode,
+ uint8 adminCode
+) external view returns (string memory serviceUrl, bool found) {
+ // 1. Get priority bundle for scanner's location
+ (bytes16[] memory uuids, uint256 count) =
+ fleetIdentity.buildHighestBondedUuidBundle(countryCode, adminCode);
+
+ for (uint i = 0; i < count; i++) {
+ if (uuids[i] != uuid) continue;
+
+ // 2. Find swarms for this UUID
+ for (uint j = 0; ; j++) {
+ uint256 swarmId;
+ try swarmRegistry.uuidSwarms(uuid, j) returns (uint256 id) {
+ swarmId = id;
+ } catch { break; }
+
+ // 3. Get swarm details
+ (,uint256 providerId,,uint8 fpSize,
+ TagType tagType, SwarmStatus status) = swarmRegistry.swarms(swarmId);
+
+ if (status != SwarmStatus.ACCEPTED) continue;
+
+ // 4. Build tagId per TagType
+ bytes memory tagId;
+ if (tagType == TagType.IBEACON_PAYLOAD_ONLY) {
+ tagId = abi.encodePacked(uuid, major, minor);
+ } else if (tagType == TagType.IBEACON_INCLUDES_MAC) {
+ tagId = abi.encodePacked(uuid, major, minor, mac);
+ }
+
+ // 5. Check membership and resolve service URL
+ if (swarmRegistry.checkMembership(swarmId, keccak256(tagId))) {
+ return (serviceProvider.providerUrls(providerId), true);
+ }
+ }
+ }
+
+ return ("", false);
+}
+```
+
+
+
+## 9. Fleet Maintenance
+
+### 9.1 Overview
+
+After registration, fleet operators must monitor bundle inclusion as market conditions change — new fleets registering at higher tiers, existing fleets promoting, and bundle slots limited to 20 per location.
+
+### 9.2 Maintenance Cycle
+
+```mermaid
+flowchart TD
+ START([Registered]) --> CHECK{In bundle?}
+
+ CHECK -->|Yes| OPTIMIZE{Lower tier
possible?}
+ CHECK -->|No| PROMOTE[Promote]
+
+ OPTIMIZE -->|Yes| DEMOTE[Demote → refund]
+ OPTIMIZE -->|No| WAIT
+
+ DEMOTE --> WAIT
+ PROMOTE --> WAIT
+
+ WAIT[Wait 24h] --> CHECK
+
+ style DEMOTE fill:#2ecc71,color:#fff
+ style PROMOTE fill:#ff9f43,color:#fff
+```
+
+### 9.3 Checking Inclusion
+
+**Local fleets** check their specific admin area:
+
+```solidity
+(bytes16[] memory uuids, uint256 count) =
+ fleetIdentity.buildHighestBondedUuidBundle(countryCode, adminCode);
+// Check if myUuid appears in uuids[0..count-1]
+```
+
+**Country fleets** must check every active admin area in their country, since they must appear in all of them:
+
+```solidity
+uint32[] memory adminAreas = fleetIdentity.getActiveAdminAreas();
+// Filter to own country: adminAreas where (rk >> 10) == myCountryCode
+// Check each area's bundle for inclusion
+```
+
+**When no admin areas are active** (no edge scanners deployed yet):
+
+```solidity
+(bytes16[] memory uuids, uint256 count) =
+ fleetIdentity.buildCountryOnlyBundle(countryCode);
+```
+
+### 9.4 Getting the Required Tier
+
+```solidity
+// Local
+(uint256 tier, uint256 bond) = fleetIdentity.localInclusionHint(cc, admin);
+
+// Country (scans ALL active admin areas, free off-chain)
+(uint256 tier, uint256 bond) = fleetIdentity.countryInclusionHint(cc);
+```
+
+### 9.5 Demotion (Saving Bond)
+
+When the suggested tier is lower than the current tier, the operator can demote to reclaim bond:
+
+```solidity
+fleetIdentity.reassignTier(tokenId, lowerTier);
+// Refund deposited automatically to operator
+```
+
+### 9.6 Propagation Timing
+
+| Phase | Duration |
+| :----------------------- | :--------------- |
+| Transaction confirmation | ~1–2 s (ZkSync) |
+| Event indexing | ~1–10 s |
+| Edge network sync | Minutes to hours |
+
+**Recommendation:** 24-hour check interval for maintenance operations.
+
+### 9.7 Maintenance Summary
+
+| Task | Method |
+| :-------------------------- | :---------------------------------------- |
+| Check inclusion (local) | `buildHighestBondedUuidBundle(cc, admin)` |
+| Check inclusion (country) | Loop all admin areas |
+| Get required tier (local) | `localInclusionHint(cc, admin)` |
+| Get required tier (country) | `countryInclusionHint(cc)` |
+| Calculate bond | `tierBond(tier, isCountry)` |
+| Move tier | `reassignTier(tokenId, tier)` |
+| Quick promote | `promote(tokenId)` |
+
+
+
+## 10. Upgradeable Contract Architecture
+
+### 10.1 Overview
+
+All swarm contracts are deployed as UUPS-upgradeable proxies:
+
+| Contract | Description |
+| :---------------------------------- | :--------------------------------------------- |
+| `ServiceProviderUpgradeable` | ERC-721 for service endpoint URLs |
+| `FleetIdentityUpgradeable` | ERC-721 Enumerable with tier-based bond system |
+| `SwarmRegistryUniversalUpgradeable` | ZkSync-compatible swarm registry |
+| `SwarmRegistryL1Upgradeable` | L1-only registry with SSTORE2 |
+
+### 10.2 Proxy Pattern
+
+Each contract is deployed as a pair:
+
+- **Proxy (ERC1967Proxy):** Immutable, stores all state, forwards calls via `delegatecall`.
+- **Implementation:** Contains logic only, can be replaced by the owner.
+
+```
+┌──────────────────────────┐
+│ ERC1967Proxy │
+│ ┌──────────────────┐ │
+│ │ Implementation │────┼──▶ FleetIdentityUpgradeable
+│ │ Slot │ │ (logic contract)
+│ └──────────────────┘ │
+│ ┌──────────────────┐ │
+│ │ Storage │ │ ← bondToken, baseBond,
+│ │ (lives here) │ │ fleets, bonds, etc.
+│ └──────────────────┘ │
+└──────────────────────────┘
+```
+
+All interactions must target the **proxy address**, never the implementation.
+
+### 10.3 Storage Layout (ERC-7201)
+
+All upgradeable contracts use namespaced storage with gaps for future expansion:
+
+| Contract | Gap Size |
+| :--------------------- | :------- |
+| FleetIdentity | 40 slots |
+| ServiceProvider | 49 slots |
+| SwarmRegistryUniversal | 44 slots |
+| SwarmRegistryL1 | 45 slots |
+
+The `__gap` costs **zero gas** at runtime — uninitialized storage slots are never written to chain. The gap is a compile-time placeholder that reserves slot numbers for safe future upgrades.
+
+### 10.4 Safe Upgrade Rules
+
+When upgrading, only the logic contract address changes. All storage remains in the proxy at the same slots.
+
+| Rule | Allowed |
+| :----------------------------------------------------- | :------ |
+| Append new variables at the end (consume from `__gap`) | ✅ |
+| Add new functions | ✅ |
+| Modify function logic | ✅ |
+| Delete existing variables | ❌ |
+| Reorder existing variables | ❌ |
+| Change variable types | ❌ |
+| Insert variables between existing ones | ❌ |
+
+### 10.5 Reinitializer Pattern
+
+When a V2+ upgrade adds new storage that needs initialization:
+
+```solidity
+function initializeV2(uint256 newParam) external reinitializer(2) {
+ _newParamIntroducedInV2 = newParam;
+}
+```
+
+The `reinitializer(N)` modifier ensures the function runs exactly once and `N` must exceed the previous version number.
+
+### 10.6 Deployment Order
+
+Deployment order is dictated by contract dependencies:
+
+1. **ServiceProviderUpgradeable** — No dependencies
+2. **FleetIdentityUpgradeable** — Requires bond token address
+3. **SwarmRegistry (L1 or Universal)** — Requires both ServiceProvider and FleetIdentity
+
+Each step deploys an implementation contract followed by an ERC1967Proxy pointing to it.
+
+### 10.7 Security Considerations
+
+- **Constructor disable:** All upgradeable contracts call `_disableInitializers()` in the constructor, preventing direct initialization of the implementation contract.
+- **Upgrade authorization:** `_authorizeUpgrade()` requires `onlyOwner`.
+- **Ownership transfer:** All contracts use `Ownable2Step` for safe two-step ownership transfers.
+- **Storage collision prevention:** ERC-7201 namespaced storage plus storage gaps prevent slot collisions across upgrades.
+
+### 10.8 ZkSync Compatibility
+
+| Feature | SwarmRegistryUniversal | SwarmRegistryL1 |
+| :------------------ | :--------------------- | :---------------------- |
+| ZkSync Era | ✅ Compatible | ❌ Not compatible |
+| Storage | Native `bytes` | SSTORE2 (`EXTCODECOPY`) |
+| Gas efficiency (L1) | Medium | High |
+
+`SwarmRegistryL1Upgradeable` relies on `EXTCODECOPY` (unsupported on ZkSync). Always deploy `SwarmRegistryUniversalUpgradeable` on ZkSync Era.
+
+
+
+## Appendix A: ISO 3166 Geographic Reference
+
+### Country Codes (ISO 3166-1 Numeric)
+
+FleetIdentity uses ISO 3166-1 numeric codes (1–999) for country identification. Selected codes:
+
+| Code | Country |
+| :--- | :------------- |
+| 036 | Australia |
+| 076 | Brazil |
+| 124 | Canada |
+| 156 | China |
+| 250 | France |
+| 276 | Germany |
+| 356 | India |
+| 380 | Italy |
+| 392 | Japan |
+| 410 | South Korea |
+| 484 | Mexico |
+| 566 | Nigeria |
+| 643 | Russia |
+| 710 | South Africa |
+| 724 | Spain |
+| 756 | Switzerland |
+| 826 | United Kingdom |
+| 840 | United States |
+
+### Admin Area Codes
+
+Admin codes map ISO 3166-2 subdivisions to 1-indexed integers (range: 1–255). Code 0 is invalid and reverts with `InvalidAdminCode()`.
+
+Per-country mapping files are maintained separately (e.g., `840-United_States.md` provides all US state mappings). Selected US examples:
+
+| Admin Code | ISO 3166-2 | State |
+| ---------: | :--------- | :--------- |
+| 1 | AL | Alabama |
+| 5 | CA | California |
+| 32 | NY | New York |
+| 43 | TX | Texas |
+
+### Contract Functions
+
+```solidity
+// Build region key
+uint32 region = fleetIdentity.makeAdminRegion(countryCode, adminCode);
+
+// Active regions
+uint16[] memory countries = fleetIdentity.getActiveCountries();
+uint32[] memory adminAreas = fleetIdentity.getActiveAdminAreas();
+
+// Extract from token
+uint32 region = fleetIdentity.tokenRegion(tokenId);
+// If region < 1024: country-level
+// If region >= 1024: adminCode = region & 0x3FF, countryCode = region >> 10
+```
+
+### Data Source
+
+All mappings are based on the ISO 3166-2 standard maintained by ISO and national statistical agencies.
diff --git a/src/swarms/doc/upgradeable-contracts.md b/src/swarms/doc/upgradeable-contracts.md
new file mode 100644
index 00000000..72d558ff
--- /dev/null
+++ b/src/swarms/doc/upgradeable-contracts.md
@@ -0,0 +1,806 @@
+# Upgradeable Swarm Contracts
+
+This document covers the UUPS-upgradeable versions of the swarm registry contracts.
+
+## Table of Contents
+
+1. [Overview](#overview)
+2. [Architecture](#architecture)
+3. [Storage Migration](#storage-migration)
+4. [Deployment](#deployment)
+5. [Interacting with Proxies](#interacting-with-proxies)
+6. [Upgrade Process](#upgrade-process)
+7. [Emergency Procedures](#emergency-procedures)
+8. [Security Considerations](#security-considerations)
+
+---
+
+## Overview
+
+The following contracts have been converted to UUPS-upgradeable versions:
+
+| Contract | File | Description |
+| ----------------------------------- | -------------------------------------------------- | -------------------------------------------- |
+| `ServiceProviderUpgradeable` | `src/swarms/ServiceProviderUpgradeable.sol` | ERC721 for service endpoint URLs |
+| `FleetIdentityUpgradeable` | `src/swarms/FleetIdentityUpgradeable.sol` | ERC721Enumerable with tier-based bond system |
+| `SwarmRegistryUniversalUpgradeable` | `src/swarms/SwarmRegistryUniversalUpgradeable.sol` | ZkSync-compatible swarm registry |
+| `SwarmRegistryL1Upgradeable` | `src/swarms/SwarmRegistryL1Upgradeable.sol` | L1-only registry with SSTORE2 |
+
+---
+
+## Architecture
+
+### Proxy Pattern
+
+Each contract is deployed as a pair:
+
+- **Proxy (ERC1967Proxy)**: Immutable, stores state, forwards calls
+- **Implementation**: Contains logic, can be replaced
+
+```
+┌──────────────────────────┐
+│ ERC1967Proxy │
+│ ┌──────────────────┐ │
+│ │ Implementation │────┼──> FleetIdentityUpgradeable
+│ │ Slot │ │ (logic contract)
+│ └──────────────────┘ │
+│ ┌──────────────────┐ │
+│ │ Storage │ │ ← bondToken, baseBond,
+│ │ (lives here) │ │ fleets, bonds, etc.
+│ └──────────────────┘ │
+└──────────────────────────┘
+```
+
+### Storage Layout (ERC-7201)
+
+All upgradeable contracts use namespaced storage with gaps for future expansion:
+
+```solidity
+// Storage variables (inherited from OpenZeppelin)
+// + Custom storage at contract-specific slots
+// + Storage gap at the end
+
+uint256[40] private __gap; // FleetIdentity: 40 slots
+uint256[49] private __gap; // ServiceProvider: 49 slots
+uint256[44] private __gap; // SwarmRegistryUniversal: 44 slots
+uint256[45] private __gap; // SwarmRegistryL1: 45 slots
+```
+
+**Storage Gap Cost**: The `__gap` costs **zero gas** at runtime. Uninitialized storage slots are not written to chain, and the gap is never read or written. It's purely a compile-time placeholder that reserves slot numbers for safe future upgrades.
+
+---
+
+## Storage Migration
+
+### How Storage Persists Through Upgrades
+
+When you upgrade a UUPS proxy, **only the logic contract address changes**. All storage remains in the proxy at exactly the same slots.
+
+**Example: Adding a reputation score to ServiceProvider**
+
+**Version 1 storage:**
+
+```
+Slot 0-10: ERC721 state (name, symbol, balances...)
+Slot 11: Ownable state (owner)
+Slot 12-14: UUPSUpgradeable (empty, stateless)
+Slot 15: providerUrls mapping
+Slot 16-64: __gap (49 empty slots)
+```
+
+**Version 2 storage (adding 1 new mapping):**
+
+```
+Slot 0-10: ERC721 state ← UNCHANGED
+Slot 11: Ownable state ← UNCHANGED
+Slot 12-14: UUPSUpgradeable ← UNCHANGED
+Slot 15: providerUrls mapping ← UNCHANGED
+Slot 16: providerScores mapping ← NEW (from gap)
+Slot 17-64: __gap (48 empty slots) ← REDUCED BY 1
+```
+
+**Key rule**: Existing slot offsets must NEVER change.
+
+### Safe Upgrade Rules
+
+1. ✅ **Append new variables** at the end (consume from `__gap`)
+2. ✅ **Add new functions**
+3. ✅ **Modify function logic**
+4. ❌ **Never delete existing variables**
+5. ❌ **Never reorder existing variables**
+6. ❌ **Never change variable types** (e.g., `uint256` → `uint128`)
+7. ❌ **Never insert variables between existing ones**
+
+### Reinitializer Pattern for V2+
+
+When adding new storage that needs initialization:
+
+```solidity
+// In V2 implementation
+function initializeV2(uint256 newParam) external reinitializer(2) {
+ _newParamIntroducedInV2 = newParam;
+}
+```
+
+The `reinitializer(N)` modifier:
+
+- Ensures this can only run once
+- Must be called with N > previous version
+- Prevents re-initialization attacks
+
+### Upgrade with Reinitializer
+
+```bash
+# Generate reinitializer calldata
+cast calldata "initializeV2(uint256)" 12345
+# Output: 0x...
+
+# Execute upgrade with initialization
+REINIT_DATA=0x... CONTRACT_TYPE=ServiceProvider PROXY_ADDRESS=0x... \
+ forge script script/UpgradeSwarm.s.sol --rpc-url $RPC_URL --broadcast
+```
+
+---
+
+## Deployment
+
+### Fresh Deployment
+
+Use the deployment script:
+
+```bash
+# Set environment variables
+export DEPLOYER_PRIVATE_KEY=0x...
+export OWNER=0x... # Contract owner address
+export BOND_TOKEN=0x... # ERC20 token for bonds
+export BASE_BOND=1000000000000000000 # 1 token (18 decimals)
+export DEPLOY_L1_REGISTRY=false # true for L1, false for ZkSync
+
+# Deploy
+forge script script/DeploySwarmUpgradeable.s.sol \
+ --rpc-url $RPC_URL \
+ --broadcast
+```
+
+The script deploys in order:
+
+1. ServiceProviderUpgradeable + proxy
+2. FleetIdentityUpgradeable + proxy
+3. SwarmRegistry (L1 or Universal) + proxy
+
+### Output
+
+Save these addresses:
+
+```
+ServiceProvider Proxy: 0x... ← Use this address for interactions
+FleetIdentity Proxy: 0x... ← Use this address for interactions
+SwarmRegistry Proxy: 0x... ← Use this address for interactions
+```
+
+---
+
+## Interacting with Proxies
+
+### Important: Always Use the Proxy Address
+
+When interacting with upgradeable contracts, **always use the proxy address**, never the implementation address. The proxy contains all the state (storage) and forwards calls to the current implementation.
+
+### TypeScript/Backend Integration
+
+#### Setup with ethers.js v6
+
+```typescript
+import { ethers } from "ethers";
+import { Contract, Provider, Wallet } from "ethers";
+
+// Import ABIs from your compilation artifacts
+import ServiceProviderABI from "./artifacts/ServiceProviderUpgradeable.json";
+import FleetIdentityABI from "./artifacts/FleetIdentityUpgradeable.json";
+import SwarmRegistryABI from "./artifacts/SwarmRegistryUniversalUpgradeable.json";
+
+// Proxy addresses (saved from deployment)
+const PROXY_ADDRESSES = {
+ serviceProvider: "0x...", // From deployment output
+ fleetIdentity: "0x...",
+ swarmRegistry: "0x...",
+};
+
+// Setup provider
+const provider = new ethers.JsonRpcProvider(process.env.RPC_URL);
+const wallet = new ethers.Wallet(process.env.PRIVATE_KEY, provider);
+
+// Connect to contracts via proxies
+const serviceProvider = new ethers.Contract(
+ PROXY_ADDRESSES.serviceProvider,
+ ServiceProviderABI.abi,
+ wallet,
+);
+
+const fleetIdentity = new ethers.Contract(
+ PROXY_ADDRESSES.fleetIdentity,
+ FleetIdentityABI.abi,
+ wallet,
+);
+
+const swarmRegistry = new ethers.Contract(
+ PROXY_ADDRESSES.swarmRegistry,
+ SwarmRegistryABI.abi,
+ wallet,
+);
+```
+
+#### Read-Only Operations
+
+```typescript
+// Query provider endpoint
+async function getProviderUrl(tokenId: bigint): Promise {
+ return await serviceProvider.providerUrls(tokenId);
+}
+
+// Check fleet bond
+async function getFleetBond(tokenId: bigint): Promise {
+ return await fleetIdentity.bonds(tokenId);
+}
+
+// Get swarm details
+async function getSwarmInfo(swarmId: bigint) {
+ const swarm = await swarmRegistry.swarms(swarmId);
+ return {
+ fleetUuid: swarm.fleetUuid,
+ providerId: swarm.providerId,
+ status: swarm.status, // 0: REGISTERED, 1: ACCEPTED, 2: REJECTED
+ tagType: swarm.tagType,
+ };
+}
+
+// Check tag membership
+async function checkMembership(
+ swarmId: bigint,
+ tagHash: string,
+): Promise {
+ return await swarmRegistry.checkMembership(swarmId, tagHash);
+}
+
+// Get highest bonded UUIDs for a region
+async function discoverFleets(
+ countryCode: number,
+ adminCode: number,
+): Promise<{ uuids: string[]; count: bigint }> {
+ const [uuids, count] = await fleetIdentity.buildHighestBondedUuidBundle(
+ countryCode,
+ adminCode,
+ );
+ return { uuids, count };
+}
+```
+
+#### Write Operations
+
+```typescript
+// Register a service provider
+async function registerProvider(url: string): Promise {
+ const tx = await serviceProvider.registerProvider(url);
+ const receipt = await tx.wait();
+
+ // Extract tokenId from event
+ const event = receipt.logs
+ .map((log) => serviceProvider.interface.parseLog(log))
+ .find((e) => e?.name === "ProviderRegistered");
+
+ return event.args.tokenId;
+}
+
+// Claim a UUID
+async function claimUuid(
+ uuid: string, // bytes16 as hex string
+ operator: string,
+): Promise {
+ // Approve bond token first
+ const bondToken = new ethers.Contract(
+ await fleetIdentity.BOND_TOKEN(),
+ ["function approve(address,uint256)"],
+ wallet,
+ );
+
+ const baseBond = await fleetIdentity.BASE_BOND();
+ await (await bondToken.approve(fleetIdentity.target, baseBond)).wait();
+
+ // Claim UUID
+ const tx = await fleetIdentity.claimUuid(uuid, operator);
+ const receipt = await tx.wait();
+
+ const event = receipt.logs
+ .map((log) => fleetIdentity.interface.parseLog(log))
+ .find((e) => e?.name === "UuidClaimed");
+
+ return event.args.tokenId;
+}
+
+// Register a swarm
+async function registerSwarm(
+ fleetUuid: string,
+ providerId: bigint,
+ filterData: Uint8Array,
+ fingerprintSize: number,
+ tagType: number, // 0: IBEACON_PAYLOAD_ONLY, 1: IBEACON_INCLUDES_MAC, 2: VENDOR_ID, 3: EDDYSTONE_UID, 4: SERVICE_DATA
+): Promise {
+ const tx = await swarmRegistry.registerSwarm(
+ fleetUuid,
+ providerId,
+ filterData,
+ fingerprintSize,
+ tagType,
+ );
+ const receipt = await tx.wait();
+
+ const event = receipt.logs
+ .map((log) => swarmRegistry.interface.parseLog(log))
+ .find((e) => e?.name === "SwarmRegistered");
+
+ return event.args.swarmId;
+}
+
+// Accept swarm (provider)
+async function acceptSwarm(swarmId: bigint): Promise {
+ const tx = await swarmRegistry.acceptSwarm(swarmId);
+ await tx.wait();
+}
+```
+
+#### Error Handling
+
+```typescript
+import { ErrorFragment } from "ethers";
+
+try {
+ await fleetIdentity.claimUuid(uuid, operator);
+} catch (error: any) {
+ // Parse custom errors
+ if (error.data) {
+ const iface = fleetIdentity.interface;
+ const decodedError = iface.parseError(error.data);
+
+ if (decodedError?.name === "UuidAlreadyOwned") {
+ console.error("UUID is already claimed");
+ } else if (decodedError?.name === "InvalidUUID") {
+ console.error("Invalid UUID format");
+ }
+ }
+
+ // Handle revert reasons
+ if (error.reason) {
+ console.error("Revert reason:", error.reason);
+ }
+
+ throw error;
+}
+```
+
+#### Environment Configuration
+
+```typescript
+// .env
+RPC_URL=https://mainnet.era.zksync.io
+PRIVATE_KEY=0x...
+SERVICE_PROVIDER_PROXY=0x...
+FLEET_IDENTITY_PROXY=0x...
+SWARM_REGISTRY_PROXY=0x...
+
+// config.ts
+export const CONTRACTS = {
+ serviceProvider: process.env.SERVICE_PROVIDER_PROXY!,
+ fleetIdentity: process.env.FLEET_IDENTITY_PROXY!,
+ swarmRegistry: process.env.SWARM_REGISTRY_PROXY!
+};
+```
+
+---
+
+### Developer/Maintainer Tools
+
+#### Using Cast (Foundry)
+
+**Read Operations:**
+
+```bash
+# Set proxy addresses as environment variables
+export SERVICE_PROVIDER=0x...
+export FLEET_IDENTITY=0x...
+export SWARM_REGISTRY=0x...
+export RPC_URL=https://mainnet.era.zksync.io
+
+# Query provider URL
+cast call $SERVICE_PROVIDER "providerUrls(uint256)(string)" 12345 --rpc-url $RPC_URL
+
+# Get fleet bond
+cast call $FLEET_IDENTITY "bonds(uint256)(uint256)" 67890 --rpc-url $RPC_URL
+
+# Get contract owner
+cast call $FLEET_IDENTITY "owner()(address)" --rpc-url $RPC_URL
+
+# Get base bond amount
+cast call $FLEET_IDENTITY "BASE_BOND()(uint256)" --rpc-url $RPC_URL
+
+# Check swarm status
+cast call $SWARM_REGISTRY "swarms(uint256)" 101 --rpc-url $RPC_URL
+
+# Check membership
+cast call $SWARM_REGISTRY \
+ "checkMembership(uint256,bytes32)(bool)" \
+ 101 \
+ 0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef \
+ --rpc-url $RPC_URL
+
+# Decode hex output to decimal
+cast --to-dec $(cast call $FLEET_IDENTITY "BASE_BOND()(uint256)" --rpc-url $RPC_URL)
+```
+
+**Write Operations:**
+
+```bash
+# Register a provider
+cast send $SERVICE_PROVIDER \
+ "registerProvider(string)(uint256)" \
+ "https://api.example.com" \
+ --rpc-url $RPC_URL \
+ --private-key $PRIVATE_KEY
+
+# Approve bond token (first get bond token address)
+BOND_TOKEN=$(cast call $FLEET_IDENTITY "BOND_TOKEN()(address)" --rpc-url $RPC_URL)
+BASE_BOND=$(cast call $FLEET_IDENTITY "BASE_BOND()(uint256)" --rpc-url $RPC_URL)
+
+cast send $BOND_TOKEN \
+ "approve(address,uint256)" \
+ $FLEET_IDENTITY \
+ $BASE_BOND \
+ --rpc-url $RPC_URL \
+ --private-key $PRIVATE_KEY
+
+# Claim UUID
+cast send $FLEET_IDENTITY \
+ "claimUuid(bytes16,address)(uint256)" \
+ 0x12345678901234567890123456789012 \
+ 0x0000000000000000000000000000000000000000 \
+ --rpc-url $RPC_URL \
+ --private-key $PRIVATE_KEY
+
+# Accept swarm (as provider)
+cast send $SWARM_REGISTRY \
+ "acceptSwarm(uint256)" \
+ 101 \
+ --rpc-url $RPC_URL \
+ --private-key $PRIVATE_KEY
+```
+
+**Get Transaction Receipt:**
+
+```bash
+# Send transaction and save hash
+TX_HASH=$(cast send $SERVICE_PROVIDER \
+ "registerProvider(string)" \
+ "https://api.example.com" \
+ --rpc-url $RPC_URL \
+ --private-key $PRIVATE_KEY \
+ --json | jq -r .transactionHash)
+
+# Get receipt
+cast receipt $TX_HASH --rpc-url $RPC_URL
+
+# Parse logs
+cast receipt $TX_HASH --rpc-url $RPC_URL --json | jq .logs
+```
+
+#### Using Block Explorers (Etherscan/Blockscout)
+
+**Verifying Proxy Contracts:**
+
+1. **Navigate to proxy address** on block explorer
+2. **Read Contract tab**:
+
+ - Shows current implementation address
+ - All read functions are available
+ - Use "Read as Proxy" mode to see implementation ABI
+
+3. **Write Contract tab**:
+ - Connect wallet (MetaMask, WalletConnect)
+ - "Write as Proxy" mode essential for upgradeable contracts
+ - All write functions visible with implementation ABI
+
+**Common Operations via GUI:**
+
+```
+1. Register Provider:
+ Contract: ServiceProvider Proxy
+ Function: registerProvider
+ Parameters:
+ - url: "https://api.example.com"
+
+2. Claim UUID:
+ Contract: FleetIdentity Proxy
+ Function: claimUuid
+ Parameters:
+ - uuid: 0x12345678901234567890123456789012
+ - operator: 0x0000000000000000000000000000000000000000
+
+ IMPORTANT: Approve bond token first!
+ Contract: Bond Token (get address from BOND_TOKEN() view)
+ Function: approve
+ Parameters:
+ - spender: [FleetIdentity Proxy Address]
+ - amount: [Get from BASE_BOND() view]
+
+3. Register Swarm:
+ Contract: SwarmRegistry Proxy
+ Function: registerSwarm
+ Parameters:
+ - fleetUuid: 0x12345678901234567890123456789012
+ - providerId: 12345
+ - filter: 0x0102030405...
+ - fingerprintSize: 16
+ - tagType: 0
+
+4. Accept Swarm:
+ Contract: SwarmRegistry Proxy
+ Function: acceptSwarm
+ Parameters:
+ - swarmId: 101
+```
+
+**Checking Implementation:**
+
+```bash
+# Via cast
+cast implementation $PROXY_ADDRESS --rpc-url $RPC_URL
+
+# Or read the slot directly (EIP-1967)
+cast storage $PROXY_ADDRESS \
+ 0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc \
+ --rpc-url $RPC_URL
+```
+
+**Reading Events:**
+
+```bash
+# Get all ProviderRegistered events
+cast logs \
+ --address $SERVICE_PROVIDER \
+ "ProviderRegistered(address,string,uint256)" \
+ --from-block 1000000 \
+ --to-block latest \
+ --rpc-url $RPC_URL
+
+# Get specific swarm events
+cast logs \
+ --address $SWARM_REGISTRY \
+ "SwarmRegistered(address,uint256,bytes16,uint256)" \
+ --from-block 1000000 \
+ --rpc-url $RPC_URL
+```
+
+#### Upgrading via Cast (Owner Only)
+
+```bash
+# Deploy new implementation
+NEW_IMPL=$(forge create src/swarms/ServiceProviderUpgradeable.sol:ServiceProviderUpgradeable \
+ --rpc-url $RPC_URL \
+ --private-key $OWNER_KEY \
+ --json | jq -r .deployedTo)
+
+# Upgrade proxy
+cast send $SERVICE_PROVIDER \
+ "upgradeToAndCall(address,bytes)" \
+ $NEW_IMPL \
+ 0x \ # ← No init code, just upgrade \
+ --rpc-url $RPC_URL \
+ --private-key $OWNER_KEY
+
+# Verify implementation changed
+cast implementation $SERVICE_PROVIDER --rpc-url $RPC_URL
+```
+
+---
+
+## Upgrade Process
+
+### Pre-Upgrade Checklist
+
+1. **Verify storage compatibility**:
+
+ ```bash
+ forge inspect ServiceProviderUpgradeable storageLayout > v1-layout.json
+ forge inspect ServiceProviderV2 storageLayout > v2-layout.json
+ # Manually compare: ensure all V1 variables are in same slots in V2
+ ```
+
+2. **Run all tests**:
+
+ ```bash
+ forge test
+ ```
+
+3. **Test on fork**:
+ ```bash
+ forge script script/UpgradeSwarm.s.sol \
+ --fork-url $RPC_URL \
+ --sender $OWNER
+ ```
+
+### Execute Upgrade
+
+```bash
+# Without reinitializer
+CONTRACT_TYPE=ServiceProvider PROXY_ADDRESS=0x... \
+ forge script script/UpgradeSwarm.s.sol --rpc-url $RPC_URL --broadcast
+
+# With reinitializer
+REINIT_DATA=$(cast calldata "initializeV2()") \
+CONTRACT_TYPE=ServiceProvider PROXY_ADDRESS=0x... \
+ forge script script/UpgradeSwarm.s.sol --rpc-url $RPC_URL --broadcast
+```
+
+### Post-Upgrade Verification
+
+```bash
+# Verify implementation changed
+cast implementation $PROXY_ADDRESS --rpc-url $RPC_URL
+
+# Verify owner unchanged
+cast call $PROXY_ADDRESS "owner()" --rpc-url $RPC_URL
+
+# If V2 has version() function
+cast call $PROXY_ADDRESS "version()" --rpc-url $RPC_URL
+
+# Test a core function
+cast call $PROXY_ADDRESS "totalSupply()" --rpc-url $RPC_URL
+```
+
+---
+
+### Rollback
+
+If a bug is found after upgrade:
+
+1. **Deploy the previous (or fixed) implementation**:
+
+ ```bash
+ forge create ServiceProviderUpgradeable \
+ --rpc-url $RPC_URL \
+ --private-key $DEPLOYER_PRIVATE_KEY
+ ```
+
+2. **Upgrade proxy to point back**:
+
+ ```bash
+ # Get proxy admin (owner)
+ cast call $PROXY_ADDRESS "owner()" --rpc-url $RPC_URL
+
+ # Upgrade to previous/fixed implementation
+ cast send $PROXY_ADDRESS \
+ "upgradeToAndCall(address,bytes)" \
+ $PREVIOUS_IMPL_ADDRESS \
+ 0x \
+ --rpc-url $RPC_URL \
+ --private-key $OWNER_PRIVATE_KEY
+ ```
+
+### Emergency Access Recovery
+
+If owner key is compromised or lost:
+
+**Prevention** (recommended during deployment):
+
+```bash
+# Use a multisig or Ownable2Step for ownership
+# Ownable2Step is already included in all upgradeable contracts
+```
+
+**Recovery**:
+
+1. If using `Ownable2Step`, the pending owner can accept ownership
+2. If owner is a multisig, execute recovery through governance
+3. If neither: contract is effectively immutable (by design)
+
+---
+
+## Security Considerations
+
+### Constructor Disable
+
+All upgradeable contracts disable their constructors:
+
+```solidity
+constructor() {
+ _disableInitializers();
+}
+```
+
+This prevents anyone from initializing the implementation contract directly (only proxies can be initialized).
+
+### Authorization
+
+- Upgrades require `onlyOwner` access via `_authorizeUpgrade()`
+- Use `Ownable2Step` for safe ownership transfers
+- Consider timelock governance for production upgrades
+
+### Storage Collision Prevention
+
+- OpenZeppelin's `Initializable` uses ERC-7201 namespaced storage
+- Custom upgradeable contracts follow same pattern
+- Storage gaps prevent child contract collisions
+- Use `forge inspect storageLayout` to verify before upgrades
+
+### Audit Recommendations
+
+Before production deployment:
+
+1. **Storage layout audit**: Verify all upgradeable contracts' storage compatibility
+2. **Upgrade simulation**: Test full upgrade path on testnet
+3. **Access control audit**: Verify only authorized addresses can upgrade
+4. **Initialization audit**: Ensure all initializers are protected
+5. **Reinitializer audit**: Verify V2+ initializers cannot be called multiple times
+
+### Testing Checklist
+
+- [ ] Deploy proxy + implementation V1
+- [ ] Initialize V1
+- [ ] Verify V1 cannot be reinitialized
+- [ ] Register/use core functionality
+- [ ] Deploy implementation V2
+- [ ] Upgrade proxy to V2
+- [ ] Verify V1 data persists
+- [ ] Test V2 new functionality
+- [ ] Verify only owner can upgrade
+- [ ] Test ownership transfer (2-step)
+
+---
+
+## ZkSync Compatibility
+
+### Universal vs L1 Registry
+
+| Feature | SwarmRegistryUniversalUpgradeable | SwarmRegistryL1Upgradeable |
+| :------------------ | :-------------------------------- | :--------------------------- |
+| ZkSync Era | ✅ Compatible | ❌ Not compatible |
+| Storage | Native `bytes` | SSTORE2 (external contracts) |
+| Gas efficiency (L1) | Medium | High |
+| Deployment | Standard proxy | Standard proxy |
+
+**Important**: `SwarmRegistryL1Upgradeable` uses `SSTORE2` which relies on `EXTCODECOPY` (unsupported on ZkSync). Always deploy `SwarmRegistryUniversalUpgradeable` on ZkSync Era.
+
+### Build Commands
+
+```bash
+# ZkSync-compatible contracts
+forge build --zksync
+
+# L1-only contracts (SwarmRegistryL1)
+forge build --match-path src/swarms/SwarmRegistryL1Upgradeable.sol
+
+# Test ZkSync contracts
+forge test --zksync
+
+# Test L1-only contracts
+forge test --match-path test/upgradeable/SwarmRegistryL1Upgradeable.t.sol
+```
+
+---
+
+## Version History Example
+
+Track versions in documentation:
+
+| Version | Date | Contract | Changes |
+| :------ | :--------- | :-------------- | :------------------------- |
+| 1.0.0 | 2026-03-04 | All | Initial UUPS deployment |
+| 1.1.0 | TBD | ServiceProvider | Added reputation system |
+| 1.2.0 | TBD | FleetIdentity | Added staking requirements |
+
+---
+
+## References
+
+- [OpenZeppelin UUPS Upgradeable](https://docs.openzeppelin.com/contracts/5.x/api/proxy#UUPSUpgradeable)
+- [EIP-1967: Proxy Storage Slots](https://eips.ethereum.org/EIPS/eip-1967)
+- [ERC-7201: Namespaced Storage Layout](https://eips.ethereum.org/EIPS/eip-7201)
+- [Foundry Book: Testing](https://book.getfoundry.sh/forge/tests)
diff --git a/src/swarms/interfaces/IFleetIdentity.sol b/src/swarms/interfaces/IFleetIdentity.sol
new file mode 100644
index 00000000..c0d6f007
--- /dev/null
+++ b/src/swarms/interfaces/IFleetIdentity.sol
@@ -0,0 +1,345 @@
+// SPDX-License-Identifier: BSD-3-Clause-Clear
+
+pragma solidity ^0.8.24;
+
+import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
+import {RegistrationLevel} from "./SwarmTypes.sol";
+
+/**
+ * @title IFleetIdentity
+ * @notice Interface for FleetIdentity — an ERC-721 with ERC721Enumerable representing
+ * ownership of a BLE fleet, secured by an ERC-20 bond organized into geometric tiers.
+ *
+ * @dev This interface defines the public contract surface that all FleetIdentity
+ * implementations must uphold across upgrades (UUPS pattern).
+ *
+ * **Two-level geographic registration**
+ *
+ * Fleets register at exactly one level:
+ * - Country — regionKey = countryCode (ISO 3166-1 numeric, 1-999)
+ * - Admin Area — regionKey = (countryCode << 10) | adminCode (>= 1024)
+ *
+ * Each regionKey has its **own independent tier namespace** — tier indices
+ * start at 0 for every region.
+ *
+ * **Economic Model**
+ *
+ * - Tier capacity: 10 members per tier (unified across levels)
+ * - Local bond: BASE_BOND * 2^tier
+ * - Country bond: BASE_BOND * COUNTRY_BOND_MULTIPLIER * 2^tier (16× local)
+ *
+ * **TokenID Encoding**
+ *
+ * TokenID = (regionKey << 128) | uuid
+ * - Bits 0-127: UUID (bytes16 Proximity UUID)
+ * - Bits 128-159: Region key (32-bit country or admin-area code)
+ */
+interface IFleetIdentity {
+ // ══════════════════════════════════════════════
+ // Events
+ // ══════════════════════════════════════════════
+
+ /// @notice Emitted when a fleet is registered in a region.
+ /// @param owner The address that owns the UUID.
+ /// @param uuid The fleet's proximity UUID.
+ /// @param tokenId The minted NFT token ID.
+ /// @param regionKey The region key (country or admin-area).
+ /// @param tierIndex The tier the fleet was registered at.
+ /// @param bondAmount The total bond amount paid.
+ /// @param operator The operator address for tier management.
+ event FleetRegistered(
+ address indexed owner,
+ bytes16 indexed uuid,
+ uint256 indexed tokenId,
+ uint32 regionKey,
+ uint256 tierIndex,
+ uint256 bondAmount,
+ address operator
+ );
+
+ /// @notice Emitted when an operator is changed for a UUID.
+ /// @param uuid The UUID whose operator changed.
+ /// @param oldOperator The previous operator address.
+ /// @param newOperator The new operator address.
+ /// @param tierExcessTransferred The tier bonds transferred between operators.
+ event OperatorSet(
+ bytes16 indexed uuid, address indexed oldOperator, address indexed newOperator, uint256 tierExcessTransferred
+ );
+
+ /// @notice Emitted when a fleet is promoted to a higher tier.
+ /// @param tokenId The token ID of the promoted fleet.
+ /// @param fromTier The original tier index.
+ /// @param toTier The new (higher) tier index.
+ /// @param additionalBond The additional bond paid for promotion.
+ event FleetPromoted(
+ uint256 indexed tokenId, uint256 indexed fromTier, uint256 indexed toTier, uint256 additionalBond
+ );
+
+ /// @notice Emitted when a fleet is demoted to a lower tier.
+ /// @param tokenId The token ID of the demoted fleet.
+ /// @param fromTier The original tier index.
+ /// @param toTier The new (lower) tier index.
+ /// @param bondRefund The bond amount refunded.
+ event FleetDemoted(uint256 indexed tokenId, uint256 indexed fromTier, uint256 indexed toTier, uint256 bondRefund);
+
+ /// @notice Emitted when a fleet NFT is burned.
+ /// @param owner The former owner of the token.
+ /// @param tokenId The burned token ID.
+ /// @param regionKey The region the fleet was registered in.
+ /// @param tierIndex The tier the fleet was in.
+ /// @param bondRefund The bond amount refunded.
+ event FleetBurned(
+ address indexed owner, uint256 indexed tokenId, uint32 indexed regionKey, uint256 tierIndex, uint256 bondRefund
+ );
+
+ /// @notice Emitted when a UUID is claimed in owned-only mode.
+ /// @param owner The address that claimed the UUID.
+ /// @param uuid The claimed UUID.
+ /// @param operator The operator address assigned.
+ event UuidClaimed(address indexed owner, bytes16 indexed uuid, address indexed operator);
+
+ // ══════════════════════════════════════════════
+ // Registration Functions
+ // ══════════════════════════════════════════════
+
+ /// @notice Register a fleet under a country at a specific tier.
+ /// @param uuid The proximity UUID (must be non-zero).
+ /// @param countryCode ISO 3166-1 numeric country code (1-999).
+ /// @param targetTier The tier to register at.
+ /// @return tokenId The minted NFT token ID.
+ function registerFleetCountry(bytes16 uuid, uint16 countryCode, uint256 targetTier)
+ external
+ returns (uint256 tokenId);
+
+ /// @notice Register a fleet under a country + admin area at a specific tier.
+ /// @param uuid The proximity UUID (must be non-zero).
+ /// @param countryCode ISO 3166-1 numeric country code (1-999).
+ /// @param adminCode Admin-area code within the country (1-255).
+ /// @param targetTier The tier to register at.
+ /// @return tokenId The minted NFT token ID.
+ function registerFleetLocal(bytes16 uuid, uint16 countryCode, uint16 adminCode, uint256 targetTier)
+ external
+ returns (uint256 tokenId);
+
+ /// @notice Claim ownership of a UUID without registering in any region.
+ /// @param uuid The proximity UUID (must be non-zero, unclaimed).
+ /// @param operator The operator address for future tier management.
+ /// @return tokenId The minted NFT token ID (uses UUID as low 128 bits).
+ function claimUuid(bytes16 uuid, address operator) external returns (uint256 tokenId);
+
+ // ══════════════════════════════════════════════
+ // Tier Management
+ // ══════════════════════════════════════════════
+
+ /// @notice Promotes a fleet to the next tier within its region.
+ /// @param tokenId The token ID to promote.
+ function promote(uint256 tokenId) external;
+
+ /// @notice Moves a fleet to a different tier within its region.
+ /// @param tokenId The token ID to reassign.
+ /// @param targetTier The target tier (higher = promote, lower = demote).
+ function reassignTier(uint256 tokenId, uint256 targetTier) external;
+
+ /// @notice Burns the fleet NFT and refunds the bond.
+ /// @param tokenId The token ID to burn.
+ function burn(uint256 tokenId) external;
+
+ // ══════════════════════════════════════════════
+ // Operator Management
+ // ══════════════════════════════════════════════
+
+ /// @notice Sets or changes the operator for a UUID.
+ /// @param uuid The UUID to update.
+ /// @param newOperator The new operator address.
+ function setOperator(bytes16 uuid, address newOperator) external;
+
+ // ══════════════════════════════════════════════
+ // View Functions: Bond & Tier Helpers
+ // ══════════════════════════════════════════════
+
+ /// @notice Bond required for tier K at current parameters.
+ /// @param tier The tier index.
+ /// @param isCountry True for country-level, false for local.
+ /// @return The bond amount required.
+ function tierBond(uint256 tier, bool isCountry) external view returns (uint256);
+
+ /// @notice Returns the cheapest tier for local inclusion.
+ /// @param countryCode ISO 3166-1 numeric country code.
+ /// @param adminCode Admin-area code within the country.
+ /// @return inclusionTier The tier that would be included in bundles.
+ /// @return bond The bond required for that tier.
+ function localInclusionHint(uint16 countryCode, uint16 adminCode)
+ external
+ view
+ returns (uint256 inclusionTier, uint256 bond);
+
+ /// @notice Returns the cheapest tier for country inclusion.
+ /// @param countryCode ISO 3166-1 numeric country code.
+ /// @return inclusionTier The tier that would be included in bundles.
+ /// @return bond The bond required for that tier.
+ function countryInclusionHint(uint16 countryCode) external view returns (uint256 inclusionTier, uint256 bond);
+
+ /// @notice Highest non-empty tier in a region, or 0 if none.
+ /// @param regionKey The region to query.
+ /// @return The highest active tier index.
+ function highestActiveTier(uint32 regionKey) external view returns (uint256);
+
+ /// @notice Number of members in a specific tier of a region.
+ /// @param regionKey The region to query.
+ /// @param tier The tier index.
+ /// @return The member count.
+ function tierMemberCount(uint32 regionKey, uint256 tier) external view returns (uint256);
+
+ /// @notice All token IDs in a specific tier of a region.
+ /// @param regionKey The region to query.
+ /// @param tier The tier index.
+ /// @return Array of token IDs.
+ function getTierMembers(uint32 regionKey, uint256 tier) external view returns (uint256[] memory);
+
+ /// @notice All UUIDs in a specific tier of a region.
+ /// @param regionKey The region to query.
+ /// @param tier The tier index.
+ /// @return uuids Array of UUIDs.
+ function getTierUuids(uint32 regionKey, uint256 tier) external view returns (bytes16[] memory uuids);
+
+ /// @notice Bond amount for a token.
+ /// @param tokenId The token to query.
+ /// @return The current bond amount.
+ function bonds(uint256 tokenId) external view returns (uint256);
+
+ /// @notice Returns true if the UUID is in owned-only state.
+ /// @param uuid The UUID to check.
+ /// @return True if owned but not registered in any region.
+ function isOwnedOnly(bytes16 uuid) external view returns (bool);
+
+ /// @notice Returns the effective operator for a UUID.
+ /// @param uuid The UUID to query.
+ /// @return operator The operator address (defaults to owner if not set).
+ function operatorOf(bytes16 uuid) external view returns (address operator);
+
+ /// @notice Returns the country bond multiplier.
+ /// @return The multiplier (e.g., 16 means country bonds are 16× local).
+ function countryBondMultiplier() external view returns (uint256);
+
+ // ══════════════════════════════════════════════
+ // View Functions: EdgeBeaconScanner Discovery
+ // ══════════════════════════════════════════════
+
+ /// @notice Builds a priority-ordered bundle of up to 20 UUIDs.
+ /// @param countryCode ISO 3166-1 numeric country code.
+ /// @param adminCode Admin-area code within the country.
+ /// @return uuids Array of UUIDs ordered by tier (highest first).
+ /// @return count Number of UUIDs in the bundle.
+ function buildHighestBondedUuidBundle(uint16 countryCode, uint16 adminCode)
+ external
+ view
+ returns (bytes16[] memory uuids, uint256 count);
+
+ /// @notice Builds a bundle containing ONLY country-level fleets.
+ /// @param countryCode ISO 3166-1 numeric country code.
+ /// @return uuids Array of country-level UUIDs.
+ /// @return count Number of UUIDs in the bundle.
+ function buildCountryOnlyBundle(uint16 countryCode) external view returns (bytes16[] memory uuids, uint256 count);
+
+ // ══════════════════════════════════════════════
+ // View Functions: Region Indexes
+ // ══════════════════════════════════════════════
+
+ /// @notice Returns all country codes that have at least one active fleet.
+ /// @return Array of ISO 3166-1 numeric country codes.
+ function getActiveCountries() external view returns (uint16[] memory);
+
+ /// @notice Returns all admin-area region keys across all countries.
+ /// @return Array of encoded region keys (countryCode << 10 | adminCode).
+ function getActiveAdminAreas() external view returns (uint32[] memory);
+
+ /// @notice Returns all active admin-area region keys for a specific country.
+ /// @param countryCode ISO 3166-1 numeric country code.
+ /// @return Array of encoded region keys for that country.
+ function getCountryAdminAreas(uint16 countryCode) external view returns (uint32[] memory);
+
+ // ══════════════════════════════════════════════
+ // Pure Functions: Token & Region Helpers
+ // ══════════════════════════════════════════════
+
+ /// @notice UUID for a token ID.
+ /// @param tokenId The token ID to decode.
+ /// @return The UUID (low 128 bits).
+ function tokenUuid(uint256 tokenId) external pure returns (bytes16);
+
+ /// @notice Region key encoded in a token ID.
+ /// @param tokenId The token ID to decode.
+ /// @return The region key (bits 128-159).
+ function tokenRegion(uint256 tokenId) external pure returns (uint32);
+
+ /// @notice Computes the deterministic token ID for a uuid+region pair.
+ /// @param uuid The proximity UUID.
+ /// @param regionKey The region key.
+ /// @return The computed token ID.
+ function computeTokenId(bytes16 uuid, uint32 regionKey) external pure returns (uint256);
+
+ /// @notice Encodes a country code and admin code into a region key.
+ /// @param countryCode ISO 3166-1 numeric country code (1-999).
+ /// @param adminCode Admin-area code within the country (1-255).
+ /// @return Encoded region key: (countryCode << 10) | adminCode.
+ function makeAdminRegion(uint16 countryCode, uint16 adminCode) external pure returns (uint32);
+
+ // ══════════════════════════════════════════════
+ // Public Getters
+ // ══════════════════════════════════════════════
+
+ /// @notice Returns the bond token address.
+ function BOND_TOKEN() external view returns (IERC20);
+
+ /// @notice Returns the base bond amount.
+ function BASE_BOND() external view returns (uint256);
+
+ /// @notice UUID -> address that first registered a token for this UUID.
+ function uuidOwner(bytes16 uuid) external view returns (address);
+
+ /// @notice UUID -> count of active tokens for this UUID (across all regions).
+ function uuidTokenCount(bytes16 uuid) external view returns (uint256);
+
+ /// @notice UUID -> registration level.
+ function uuidLevel(bytes16 uuid) external view returns (RegistrationLevel);
+
+ /// @notice UUID -> operator address for tier maintenance.
+ function uuidOperator(bytes16 uuid) external view returns (address);
+
+ /// @notice UUID -> total tier bonds across all registered regions.
+ function uuidTotalTierBonds(bytes16 uuid) external view returns (uint256);
+
+ /// @notice regionKey -> number of tiers opened in that region.
+ function regionTierCount(uint32 regionKey) external view returns (uint256);
+
+ /// @notice Token ID -> tier index (within its region) the fleet belongs to.
+ function fleetTier(uint256 tokenId) external view returns (uint256);
+
+ /// @notice tokenId -> tier-0 equivalent bond paid at registration.
+ function tokenTier0Bond(uint256 tokenId) external view returns (uint256);
+
+ /// @notice UUID -> ownership bond paid at claim/first-registration.
+ function uuidOwnershipBondPaid(bytes16 uuid) external view returns (uint256);
+
+ // ══════════════════════════════════════════════
+ // Constants
+ // ══════════════════════════════════════════════
+
+ /// @notice Unified tier capacity for all levels.
+ function TIER_CAPACITY() external view returns (uint256);
+
+ /// @notice Default country bond multiplier when not explicitly set (16× local).
+ function DEFAULT_COUNTRY_BOND_MULTIPLIER() external view returns (uint256);
+
+ /// @notice Default base bond for tier 0.
+ function DEFAULT_BASE_BOND() external view returns (uint256);
+
+ /// @notice Hard cap on tier count per region.
+ function MAX_TIERS() external view returns (uint256);
+
+ /// @notice Maximum UUIDs returned by buildHighestBondedUuidBundle.
+ function MAX_BONDED_UUID_BUNDLE_SIZE() external view returns (uint256);
+
+ /// @notice Region key for owned-only UUIDs (not registered in any region).
+ function OWNED_REGION_KEY() external view returns (uint32);
+}
diff --git a/src/swarms/interfaces/IServiceProvider.sol b/src/swarms/interfaces/IServiceProvider.sol
new file mode 100644
index 00000000..3e89f883
--- /dev/null
+++ b/src/swarms/interfaces/IServiceProvider.sol
@@ -0,0 +1,55 @@
+// SPDX-License-Identifier: BSD-3-Clause-Clear
+
+pragma solidity ^0.8.24;
+
+/**
+ * @title IServiceProvider
+ * @notice Interface for ServiceProvider — an ERC-721 representing ownership of a service endpoint URL.
+ * @dev This interface defines the public contract surface that all ServiceProvider
+ * implementations must uphold across upgrades (UUPS pattern).
+ *
+ * TokenID = keccak256(url), guaranteeing one owner per URL.
+ */
+interface IServiceProvider {
+ // ══════════════════════════════════════════════
+ // Events
+ // ══════════════════════════════════════════════
+
+ /// @notice Emitted when a new service provider URL is registered.
+ /// @param owner The address that registered the provider.
+ /// @param url The service endpoint URL.
+ /// @param tokenId The minted NFT token ID (derived from URL hash).
+ event ProviderRegistered(address indexed owner, string url, uint256 indexed tokenId);
+
+ /// @notice Emitted when a provider NFT is burned.
+ /// @param owner The former owner of the token.
+ /// @param tokenId The burned token ID.
+ event ProviderBurned(address indexed owner, uint256 indexed tokenId);
+
+ // ══════════════════════════════════════════════
+ // Core Functions
+ // ══════════════════════════════════════════════
+
+ /// @notice Mints a new provider NFT for the given URL.
+ /// @param url The backend service URL (must be unique, non-empty).
+ /// @return tokenId The deterministic token ID derived from `url`.
+ function registerProvider(string calldata url) external returns (uint256 tokenId);
+
+ /// @notice Burns the provider NFT. Caller must be the token owner.
+ /// @param tokenId The provider token ID to burn.
+ function burn(uint256 tokenId) external;
+
+ // ══════════════════════════════════════════════
+ // View Functions
+ // ══════════════════════════════════════════════
+
+ /// @notice Maps TokenID -> Provider URL.
+ /// @param tokenId The token to query.
+ /// @return The provider URL string.
+ function providerUrls(uint256 tokenId) external view returns (string memory);
+
+ /// @notice Returns the owner of the specified token ID (ERC-721).
+ /// @param tokenId The token ID to query.
+ /// @return The address of the token owner.
+ function ownerOf(uint256 tokenId) external view returns (address);
+}
diff --git a/src/swarms/interfaces/ISwarmRegistry.sol b/src/swarms/interfaces/ISwarmRegistry.sol
new file mode 100644
index 00000000..ba1b39cc
--- /dev/null
+++ b/src/swarms/interfaces/ISwarmRegistry.sol
@@ -0,0 +1,152 @@
+// SPDX-License-Identifier: BSD-3-Clause-Clear
+
+pragma solidity ^0.8.24;
+
+import {IFleetIdentity} from "./IFleetIdentity.sol";
+import {IServiceProvider} from "./IServiceProvider.sol";
+import {SwarmStatus, TagType, FingerprintSize} from "./SwarmTypes.sol";
+
+/**
+ * @title ISwarmRegistry
+ * @notice Interface for SwarmRegistry — a permissionless BLE swarm registry.
+ * @dev This interface defines the public contract surface that all SwarmRegistry
+ * implementations must uphold across upgrades (UUPS pattern).
+ *
+ * There are two implementations:
+ * - SwarmRegistryL1Upgradeable: Uses SSTORE2 for filter storage (L1 only, not ZkSync compatible)
+ * - SwarmRegistryUniversalUpgradeable: Uses native bytes storage (cross-chain compatible)
+ *
+ * Both implementations share the same public interface defined here.
+ * The `swarms()` mapping getter returns implementation-specific struct layouts
+ * and is NOT included in this interface.
+ */
+interface ISwarmRegistry {
+ // ══════════════════════════════════════════════
+ // Events
+ // ══════════════════════════════════════════════
+
+ /// @notice Emitted when a new swarm is registered.
+ /// @param swarmId The unique swarm identifier.
+ /// @param fleetUuid The fleet UUID this swarm belongs to.
+ /// @param providerId The service provider NFT ID.
+ /// @param owner The address that registered the swarm.
+ /// @dev Note: SwarmRegistryUniversal also emits filterSize as a 4th non-indexed param.
+ event SwarmRegistered(uint256 indexed swarmId, bytes16 indexed fleetUuid, uint256 indexed providerId, address owner);
+
+ /// @notice Emitted when a swarm's status changes.
+ /// @param swarmId The swarm identifier.
+ /// @param status The new status (REGISTERED, ACCEPTED, REJECTED).
+ event SwarmStatusChanged(uint256 indexed swarmId, SwarmStatus status);
+
+ /// @notice Emitted when a swarm's provider is updated.
+ /// @param swarmId The swarm identifier.
+ /// @param oldProvider The previous provider ID.
+ /// @param newProvider The new provider ID.
+ event SwarmProviderUpdated(uint256 indexed swarmId, uint256 indexed oldProvider, uint256 indexed newProvider);
+
+ /// @notice Emitted when a swarm is deleted by its owner.
+ /// @param swarmId The deleted swarm identifier.
+ /// @param fleetUuid The fleet UUID the swarm belonged to.
+ /// @param owner The address that deleted the swarm.
+ event SwarmDeleted(uint256 indexed swarmId, bytes16 indexed fleetUuid, address indexed owner);
+
+ /// @notice Emitted when an orphaned swarm is purged.
+ /// @param swarmId The purged swarm identifier.
+ /// @param fleetUuid The fleet UUID the swarm belonged to.
+ /// @param purgedBy The address that called purge.
+ event SwarmPurged(uint256 indexed swarmId, bytes16 indexed fleetUuid, address indexed purgedBy);
+
+ // ══════════════════════════════════════════════
+ // Pure Functions
+ // ══════════════════════════════════════════════
+
+ /// @notice Derives a deterministic swarm ID.
+ /// @param fleetUuid The fleet UUID.
+ /// @param filter The XOR filter data.
+ /// @param fpSize Fingerprint size (BITS_8 or BITS_16).
+ /// @param tagType The tag type classification.
+ /// @return The computed swarm ID.
+ function computeSwarmId(bytes16 fleetUuid, bytes calldata filter, FingerprintSize fpSize, TagType tagType)
+ external
+ pure
+ returns (uint256);
+
+ // ══════════════════════════════════════════════
+ // Core Functions
+ // ══════════════════════════════════════════════
+
+ /// @notice Registers a new swarm. Caller must own the fleet UUID.
+ /// @param fleetUuid The fleet UUID (must be owned by caller).
+ /// @param providerId The service provider NFT ID.
+ /// @param filter The XOR filter data.
+ /// @param fpSize Fingerprint size (BITS_8 or BITS_16).
+ /// @param tagType The tag type for this swarm.
+ /// @return swarmId The registered swarm's unique identifier.
+ function registerSwarm(
+ bytes16 fleetUuid,
+ uint256 providerId,
+ bytes calldata filter,
+ FingerprintSize fpSize,
+ TagType tagType
+ ) external returns (uint256 swarmId);
+
+ /// @notice Approves a swarm. Caller must own the provider NFT.
+ /// @param swarmId The swarm to accept.
+ function acceptSwarm(uint256 swarmId) external;
+
+ /// @notice Rejects a swarm. Caller must own the provider NFT.
+ /// @param swarmId The swarm to reject.
+ function rejectSwarm(uint256 swarmId) external;
+
+ /// @notice Reassigns the service provider. Resets status to REGISTERED.
+ /// @param swarmId The swarm to update.
+ /// @param newProviderId The new service provider NFT ID.
+ function updateSwarmProvider(uint256 swarmId, uint256 newProviderId) external;
+
+ /// @notice Permanently deletes a swarm. Caller must own the fleet UUID.
+ /// @param swarmId The swarm to delete.
+ function deleteSwarm(uint256 swarmId) external;
+
+ /// @notice Permissionless-ly removes an orphaned swarm.
+ /// @dev A swarm is orphaned if its fleet UUID or provider NFT no longer exists.
+ /// @param swarmId The orphaned swarm to purge.
+ function purgeOrphanedSwarm(uint256 swarmId) external;
+
+ // ══════════════════════════════════════════════
+ // View Functions
+ // ══════════════════════════════════════════════
+
+ /// @notice Returns the FleetIdentity contract address.
+ function FLEET_CONTRACT() external view returns (IFleetIdentity);
+
+ /// @notice Returns the ServiceProvider contract address.
+ function PROVIDER_CONTRACT() external view returns (IServiceProvider);
+
+ /// @notice Returns whether the swarm's fleet UUID and provider NFT are still valid.
+ /// @param swarmId The swarm to check.
+ /// @return fleetValid True if the fleet UUID owner exists.
+ /// @return providerValid True if the provider NFT exists.
+ function isSwarmValid(uint256 swarmId) external view returns (bool fleetValid, bool providerValid);
+
+ /// @notice Returns the raw XOR filter bytes for a swarm.
+ /// @param swarmId The swarm to query.
+ /// @return The filter data bytes.
+ function getFilterData(uint256 swarmId) external view returns (bytes memory);
+
+ /// @notice Tests tag membership against the swarm's XOR filter.
+ /// @param swarmId The swarm to query.
+ /// @param tagHash The keccak256 hash of the tag to test.
+ /// @return isValid True if the tag is probably a member of the filter.
+ function checkMembership(uint256 swarmId, bytes32 tagHash) external view returns (bool isValid);
+
+ /// @notice UUID -> List of SwarmIDs at a given index.
+ /// @param fleetUuid The fleet UUID.
+ /// @param index The array index.
+ /// @return The swarm ID at that index.
+ function uuidSwarms(bytes16 fleetUuid, uint256 index) external view returns (uint256);
+
+ /// @notice SwarmID -> index in uuidSwarms[fleetUuid] (for O(1) removal).
+ /// @param swarmId The swarm ID.
+ /// @return The index in the UUID's swarm array.
+ function swarmIndexInUuid(uint256 swarmId) external view returns (uint256);
+}
diff --git a/src/swarms/interfaces/SwarmTypes.sol b/src/swarms/interfaces/SwarmTypes.sol
new file mode 100644
index 00000000..3ec535cd
--- /dev/null
+++ b/src/swarms/interfaces/SwarmTypes.sol
@@ -0,0 +1,68 @@
+// SPDX-License-Identifier: BSD-3-Clause-Clear
+
+pragma solidity ^0.8.24;
+
+/**
+ * @title SwarmTypes
+ * @notice Shared type definitions for the Swarm system contracts.
+ * @dev Solidity interfaces cannot define enums, so shared enums live here.
+ * Import this file alongside interfaces when type definitions are needed.
+ */
+
+// ══════════════════════════════════════════════
+// FleetIdentity Types
+// ══════════════════════════════════════════════
+
+/// @notice Registration level for a UUID in FleetIdentity.
+enum RegistrationLevel {
+ None, // 0 - not registered (default)
+ Owned, // 1 - owned but not registered in any region
+ Local, // 2 - admin area (local) level
+ Country // 3 - country level
+}
+
+// ══════════════════════════════════════════════
+// SwarmRegistry Types
+// ══════════════════════════════════════════════
+
+/// @notice Status of a swarm registration.
+enum SwarmStatus {
+ REGISTERED, // 0 - registered but awaiting provider approval
+ ACCEPTED, // 1 - approved by the service provider
+ REJECTED // 2 - rejected by the service provider
+}
+
+/// @notice Tag type classification for BLE advertisement formats.
+/// @dev The UUID field (bytes16) encodes the fleet-level identifier derived from the BLE advertisement.
+/// It contains only the fields necessary for edge scanners to register OS-level background scan
+/// filters and to scope on-chain swarm lookups. Tag-specific fields (e.g. iBeacon Major/Minor)
+/// are excluded from the UUID and used only in tag hash construction for XOR filter membership.
+///
+/// UUID design trade-offs:
+/// - Specificity: A more populated UUID → fewer swarms per UUID → faster service resolution.
+/// - Uniqueness: Short/generic UUIDs increase collision risk when claiming fleet ownership.
+/// - Privacy: Fewer exposed bytes = more privacy, at the cost of specificity and uniqueness.
+/// - Background scanning: UUID must contain enough data for OS-level BLE scan filters.
+///
+/// Encoding per TagType:
+/// - IBEACON_*: Proximity UUID (16B). AltBeacon uses this same format.
+/// - VENDOR_ID: [Len (1B)] [CompanyID (2B, BE)] [FleetID (≤13B, zero-padded)].
+/// Len = 2 + FleetIdLen (range 2–15).
+/// - EDDYSTONE_UID: Namespace (10B) || Instance (6B).
+/// - SERVICE_DATA: 128-bit Bluetooth Base UUID expansion of the Service UUID.
+/// 16-bit: 0000XXXX-0000-1000-8000-00805F9B34FB
+/// 32-bit: XXXXXXXX-0000-1000-8000-00805F9B34FB
+/// 128-bit: stored as-is.
+enum TagType {
+ IBEACON_PAYLOAD_ONLY, // 0x00: proxUUID || major || minor (also covers AltBeacon)
+ IBEACON_INCLUDES_MAC, // 0x01: proxUUID || major || minor || MAC (Normalized)
+ VENDOR_ID, // 0x02: len-prefixed companyID || fleetIdentifier
+ EDDYSTONE_UID, // 0x03: namespace (10B) || instance (6B)
+ SERVICE_DATA // 0x04: expanded 128-bit BLE Service UUID
+}
+
+/// @notice Fingerprint size for XOR filter (8-bit or 16-bit only for gas efficiency).
+enum FingerprintSize {
+ BITS_8, // 8-bit fingerprints (1 byte each)
+ BITS_16 // 16-bit fingerprints (2 bytes each)
+}
diff --git a/test/FleetIdentity.t.sol b/test/FleetIdentity.t.sol
new file mode 100644
index 00000000..36654de1
--- /dev/null
+++ b/test/FleetIdentity.t.sol
@@ -0,0 +1,4083 @@
+// SPDX-License-Identifier: MIT
+pragma solidity ^0.8.24;
+
+import {Test} from "forge-std/Test.sol";
+import {ERC1967Proxy} from "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol";
+import {FleetIdentityUpgradeable} from "../src/swarms/FleetIdentityUpgradeable.sol";
+import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol";
+
+/// @dev Minimal ERC-20 mock with public mint for testing.
+contract MockERC20 is ERC20 {
+ constructor() ERC20("Mock Bond Token", "MBOND") {}
+
+ function mint(address to, uint256 amount) external {
+ _mint(to, amount);
+ }
+}
+
+/// @dev ERC-20 that returns false on transfer instead of reverting.
+contract BadERC20 is ERC20 {
+ bool public shouldFail;
+
+ constructor() ERC20("Bad Token", "BAD") {}
+
+ function mint(address to, uint256 amount) external {
+ _mint(to, amount);
+ }
+
+ function setFail(bool _fail) external {
+ shouldFail = _fail;
+ }
+
+ function transfer(address to, uint256 amount) public override returns (bool) {
+ if (shouldFail) return false;
+ return super.transfer(to, amount);
+ }
+
+ function transferFrom(address from, address to, uint256 amount) public override returns (bool) {
+ if (shouldFail) return false;
+ return super.transferFrom(from, to, amount);
+ }
+}
+
+contract FleetIdentityTest is Test {
+ FleetIdentityUpgradeable fleet;
+ MockERC20 bondToken;
+
+ address owner = address(0x1111);
+ address alice = address(0xA);
+ address bob = address(0xB);
+ address carol = address(0xC);
+
+ bytes16 constant UUID_1 = bytes16(keccak256("fleet-alpha"));
+ bytes16 constant UUID_2 = bytes16(keccak256("fleet-bravo"));
+ bytes16 constant UUID_3 = bytes16(keccak256("fleet-charlie"));
+
+ uint256 constant BASE_BOND = 100 ether;
+
+ uint16 constant US = 840;
+ uint16 constant DE = 276;
+ uint16 constant FR = 250;
+ uint16 constant JP = 392;
+ uint16 constant ADMIN_CA = 1;
+ uint16 constant ADMIN_NY = 2;
+
+ event FleetRegistered(
+ address indexed owner,
+ bytes16 indexed uuid,
+ uint256 indexed tokenId,
+ uint32 regionKey,
+ uint256 tierIndex,
+ uint256 bondAmount,
+ address operator
+ );
+ event OperatorSet(
+ bytes16 indexed uuid,
+ address indexed oldOperator,
+ address indexed newOperator,
+ uint256 tierExcessTransferred
+ );
+ event FleetPromoted(
+ uint256 indexed tokenId, uint256 indexed fromTier, uint256 indexed toTier, uint256 additionalBond
+ );
+ event FleetDemoted(uint256 indexed tokenId, uint256 indexed fromTier, uint256 indexed toTier, uint256 bondRefund);
+ event FleetBurned(
+ address indexed owner, uint256 indexed tokenId, uint32 indexed regionKey, uint256 tierIndex, uint256 bondRefund
+ );
+
+ function setUp() public {
+ bondToken = new MockERC20();
+
+ // Deploy implementation
+ FleetIdentityUpgradeable impl = new FleetIdentityUpgradeable();
+
+ // Deploy proxy with initialize call
+ ERC1967Proxy proxy = new ERC1967Proxy(
+ address(impl),
+ abi.encodeCall(FleetIdentityUpgradeable.initialize, (owner, address(bondToken), BASE_BOND, 0))
+ );
+
+ // Cast proxy to contract type
+ fleet = FleetIdentityUpgradeable(address(proxy));
+
+ // Mint enough for all 24 tiers (tier 23 bond = BASE_BOND * 2^23 ≈ 838M ether)
+ // Total for 8 members across 24 tiers ≈ 13.4 billion ether
+ bondToken.mint(alice, 100_000_000_000_000 ether);
+ bondToken.mint(bob, 100_000_000_000_000 ether);
+ bondToken.mint(carol, 100_000_000_000_000 ether);
+
+ vm.prank(alice);
+ bondToken.approve(address(fleet), type(uint256).max);
+ vm.prank(bob);
+ bondToken.approve(address(fleet), type(uint256).max);
+ vm.prank(carol);
+ bondToken.approve(address(fleet), type(uint256).max);
+ }
+
+ // --- Helpers ---
+
+ /// @dev Compute tokenId from (uuid, region) using new encoding
+ function _tokenId(bytes16 uuid, uint32 region) internal pure returns (uint256) {
+ return (uint256(region) << 128) | uint256(uint128(uuid));
+ }
+
+ /// @dev Given a UUID from buildBundle, find tokenId by checking local first, then country
+ function _findTokenId(bytes16 uuid, uint16 cc, uint16 admin) internal view returns (uint256) {
+ uint32 localRegion = (uint32(cc) << 10) | uint32(admin);
+ uint256 localTokenId = _tokenId(uuid, localRegion);
+ // Check if local token exists by trying to get its owner
+ try fleet.ownerOf(localTokenId) returns (address) {
+ return localTokenId;
+ } catch {
+ uint32 countryRegion = uint32(cc);
+ return _tokenId(uuid, countryRegion);
+ }
+ }
+
+ function _uuid(uint256 i) internal pure returns (bytes16) {
+ return bytes16(keccak256(abi.encodePacked("fleet-", i)));
+ }
+
+ function _regionUS() internal pure returns (uint32) {
+ return uint32(US);
+ }
+
+ function _regionDE() internal pure returns (uint32) {
+ return uint32(DE);
+ }
+
+ function _regionUSCA() internal pure returns (uint32) {
+ return (uint32(US) << 10) | uint32(ADMIN_CA);
+ }
+
+ function _regionUSNY() internal pure returns (uint32) {
+ return (uint32(US) << 10) | uint32(ADMIN_NY);
+ }
+
+ function _makeAdminRegion(uint16 cc, uint16 admin) internal pure returns (uint32) {
+ return (uint32(cc) << 10) | uint32(admin);
+ }
+
+ function _registerNCountry(address owner, uint16 cc, uint256 count, uint256 startSeed)
+ internal
+ returns (uint256[] memory ids)
+ {
+ uint256 cap = fleet.TIER_CAPACITY();
+ ids = new uint256[](count);
+ for (uint256 i = 0; i < count; i++) {
+ uint256 targetTier = i / cap;
+ vm.prank(owner);
+ ids[i] = fleet.registerFleetCountry(_uuid(startSeed + i), cc, targetTier);
+ }
+ }
+
+ function _registerNCountryAt(address owner, uint16 cc, uint256 count, uint256 startSeed, uint256 tier)
+ internal
+ returns (uint256[] memory ids)
+ {
+ ids = new uint256[](count);
+ for (uint256 i = 0; i < count; i++) {
+ vm.prank(owner);
+ ids[i] = fleet.registerFleetCountry(_uuid(startSeed + i), cc, tier);
+ }
+ }
+
+ function _registerNLocal(address owner, uint16 cc, uint16 admin, uint256 count, uint256 startSeed)
+ internal
+ returns (uint256[] memory ids)
+ {
+ uint256 cap = fleet.TIER_CAPACITY();
+ ids = new uint256[](count);
+ for (uint256 i = 0; i < count; i++) {
+ uint256 targetTier = i / cap;
+ vm.prank(owner);
+ ids[i] = fleet.registerFleetLocal(_uuid(startSeed + i), cc, admin, targetTier);
+ }
+ }
+
+ function _registerNLocalAt(address owner, uint16 cc, uint16 admin, uint256 count, uint256 startSeed, uint256 tier)
+ internal
+ returns (uint256[] memory ids)
+ {
+ ids = new uint256[](count);
+ for (uint256 i = 0; i < count; i++) {
+ vm.prank(owner);
+ ids[i] = fleet.registerFleetLocal(_uuid(startSeed + i), cc, admin, tier);
+ }
+ }
+
+ // --- Constructor ---
+
+ function test_constructor_setsImmutables() public view {
+ assertEq(address(fleet.BOND_TOKEN()), address(bondToken));
+ assertEq(fleet.BASE_BOND(), BASE_BOND);
+ assertEq(fleet.name(), "Swarm Fleet Identity");
+ assertEq(fleet.symbol(), "SFID");
+ }
+
+ function test_constructor_constants() public view {
+ assertEq(fleet.TIER_CAPACITY(), 10);
+ assertEq(fleet.MAX_TIERS(), 24);
+ assertEq(fleet.MAX_BONDED_UUID_BUNDLE_SIZE(), 20);
+ assertEq(fleet.countryBondMultiplier(), 16);
+ }
+
+ // --- tierBond ---
+
+ function test_tierBond_local_tier0() public view {
+ // Local regions get 1× multiplier
+ assertEq(fleet.tierBond(0, false), BASE_BOND);
+ }
+
+ function test_tierBond_country_tier0() public view {
+ // Country regions get 16x multiplier
+ assertEq(fleet.tierBond(0, true), BASE_BOND * fleet.countryBondMultiplier());
+ }
+
+ function test_tierBond_local_tier1() public view {
+ assertEq(fleet.tierBond(1, false), BASE_BOND * 2);
+ }
+
+ function test_tierBond_country_tier1() public view {
+ assertEq(fleet.tierBond(1, true), BASE_BOND * fleet.countryBondMultiplier() * 2);
+ }
+
+ function test_tierBond_geometricProgression() public view {
+ for (uint256 i = 1; i <= 5; i++) {
+ assertEq(fleet.tierBond(i, false), fleet.tierBond(i - 1, false) * 2);
+ assertEq(fleet.tierBond(i, true), fleet.tierBond(i - 1, true) * 2);
+ }
+ }
+
+ // --- registerFleetCountry ---
+
+ function test_registerFleetCountry_auto_setsRegionAndTier() public {
+ vm.prank(alice);
+ uint256 tokenId = fleet.registerFleetCountry(UUID_1, US, 0);
+
+ assertEq(fleet.tokenRegion(tokenId), _regionUS());
+ assertEq(fleet.fleetTier(tokenId), 0);
+ assertEq(fleet.bonds(tokenId), BASE_BOND * fleet.countryBondMultiplier()); // Country gets 16x multiplier
+ assertEq(fleet.regionTierCount(_regionUS()), 1);
+ }
+
+ function test_RevertIf_registerFleetCountry_invalidCode_zero() public {
+ vm.prank(alice);
+ vm.expectRevert(FleetIdentityUpgradeable.InvalidCountryCode.selector);
+ fleet.registerFleetCountry(UUID_1, 0, 0);
+ }
+
+ function test_RevertIf_registerFleetCountry_invalidCode_over999() public {
+ vm.prank(alice);
+ vm.expectRevert(FleetIdentityUpgradeable.InvalidCountryCode.selector);
+ fleet.registerFleetCountry(UUID_1, 1000, 0);
+ }
+
+ // --- registerFleetLocal ---
+
+ function test_registerFleetLocal_setsRegionAndTier() public {
+ vm.prank(alice);
+ uint256 tokenId = fleet.registerFleetLocal(UUID_1, US, ADMIN_CA, 0);
+
+ assertEq(fleet.tokenRegion(tokenId), _regionUSCA());
+ assertEq(fleet.fleetTier(tokenId), 0);
+ assertEq(fleet.bonds(tokenId), BASE_BOND);
+ }
+
+ function test_RevertIf_registerFleetLocal_invalidCountry() public {
+ vm.prank(alice);
+ vm.expectRevert(FleetIdentityUpgradeable.InvalidCountryCode.selector);
+ fleet.registerFleetLocal(UUID_1, 0, ADMIN_CA, 0);
+ }
+
+ function test_RevertIf_registerFleetLocal_invalidAdmin_zero() public {
+ vm.prank(alice);
+ vm.expectRevert(FleetIdentityUpgradeable.InvalidAdminCode.selector);
+ fleet.registerFleetLocal(UUID_1, US, 0, 0);
+ }
+
+ function test_RevertIf_registerFleetLocal_invalidAdmin_over4095() public {
+ vm.prank(alice);
+ vm.expectRevert(FleetIdentityUpgradeable.InvalidAdminCode.selector);
+ fleet.registerFleetLocal(UUID_1, US, 4096, 0);
+ }
+
+ // --- Per-region independent tier indexing (KEY REQUIREMENT) ---
+
+ function test_perRegionTiers_firstFleetInEachLevelPaysBondWithMultiplier() public {
+ // Country level pays 16x multiplier
+ vm.prank(alice);
+ uint256 c1 = fleet.registerFleetCountry(UUID_1, US, 0);
+ // Local level pays 1× multiplier
+ vm.prank(alice);
+ uint256 l1 = fleet.registerFleetLocal(UUID_2, US, ADMIN_CA, 0);
+
+ assertEq(fleet.fleetTier(c1), 0);
+ assertEq(fleet.fleetTier(l1), 0);
+
+ assertEq(fleet.bonds(c1), BASE_BOND * fleet.countryBondMultiplier()); // Country gets 16× multiplier
+ assertEq(fleet.bonds(l1), BASE_BOND); // Local gets 1× multiplier
+ }
+
+ function test_perRegionTiers_fillOneRegionDoesNotAffectOthers() public {
+ // Fill US country tier 0 with 4 fleets
+ _registerNCountryAt(alice, US, 4, 0, 0);
+ assertEq(fleet.regionTierCount(_regionUS()), 1);
+ assertEq(fleet.tierMemberCount(_regionUS(), 0), 4);
+
+ // Next US country fleet goes to tier 1
+ vm.prank(bob);
+ uint256 us21 = fleet.registerFleetCountry(_uuid(100), US, 1);
+ assertEq(fleet.fleetTier(us21), 1);
+ assertEq(fleet.bonds(us21), BASE_BOND * fleet.countryBondMultiplier() * 2); // Country tier 1: 16× * 2^1
+
+ // DE country is independent - can still join tier 0
+ vm.prank(bob);
+ uint256 de1 = fleet.registerFleetCountry(_uuid(200), DE, 0);
+ assertEq(fleet.fleetTier(de1), 0);
+ assertEq(fleet.bonds(de1), BASE_BOND * fleet.countryBondMultiplier());
+ assertEq(fleet.regionTierCount(_regionDE()), 1);
+
+ // US local is independent - can still join tier 0
+ vm.prank(bob);
+ uint256 usca1 = fleet.registerFleetLocal(_uuid(300), US, ADMIN_CA, 0);
+ assertEq(fleet.fleetTier(usca1), 0);
+ assertEq(fleet.bonds(usca1), BASE_BOND);
+ }
+
+ function test_perRegionTiers_twoCountriesIndependent() public {
+ // Register 4 US country fleets at tier 0
+ _registerNCountryAt(alice, US, 4, 0, 0);
+ assertEq(fleet.tierMemberCount(_regionUS(), 0), 4);
+
+ // Next US country fleet explicitly goes to tier 1
+ vm.prank(bob);
+ uint256 us21 = fleet.registerFleetCountry(_uuid(500), US, 1);
+ assertEq(fleet.fleetTier(us21), 1);
+ assertEq(fleet.bonds(us21), BASE_BOND * fleet.countryBondMultiplier() * 2); // Country tier 1: 16× * 2^1
+
+ // DE country is independent - can still join tier 0
+ vm.prank(bob);
+ uint256 de1 = fleet.registerFleetCountry(_uuid(600), DE, 0);
+ assertEq(fleet.fleetTier(de1), 0);
+ assertEq(fleet.bonds(de1), BASE_BOND * fleet.countryBondMultiplier()); // Country tier 0: 16× * 2^0
+ }
+
+ function test_perRegionTiers_twoAdminAreasIndependent() public {
+ // Register 4 local fleets at tier 0 in US/CA
+ _registerNLocalAt(alice, US, ADMIN_CA, 4, 0, 0);
+ assertEq(fleet.tierMemberCount(_regionUSCA(), 0), 4);
+
+ // NY is independent - can still join tier 0
+ vm.prank(bob);
+ uint256 ny1 = fleet.registerFleetLocal(_uuid(500), US, ADMIN_NY, 0);
+ assertEq(fleet.fleetTier(ny1), 0);
+ assertEq(fleet.bonds(ny1), BASE_BOND);
+ }
+
+ // --- Local inclusion hint tier logic ---
+
+ function test_localInclusionHint_emptyRegionReturnsTier0() public {
+ // No fleets anywhere — localInclusionHint returns tier 0.
+ (uint256 inclusionTier,) = fleet.localInclusionHint(US, ADMIN_CA);
+ assertEq(inclusionTier, 0);
+
+ // Register at tier 0 (inclusionTier is 0, so no promotion needed)
+ vm.prank(alice);
+ uint256 tokenId = fleet.registerFleetLocal(UUID_1, US, ADMIN_CA, 0);
+ assertEq(fleet.fleetTier(tokenId), 0);
+ assertEq(fleet.regionTierCount(_regionUSCA()), 1);
+ }
+
+ function test_localInclusionHint_returnsCheapestInclusionTier() public {
+ // Fill admin-area tier 0 so tier 0 is full.
+ _registerNLocalAt(alice, US, ADMIN_CA, fleet.TIER_CAPACITY(), 0, 0);
+
+ // localInclusionHint should return tier 1 (cheapest tier with capacity).
+ (uint256 inclusionTier,) = fleet.localInclusionHint(US, ADMIN_CA);
+ assertEq(inclusionTier, 1);
+
+ // Register directly at inclusionTier as tier 0 is full
+ vm.prank(bob);
+ uint256 tokenId = fleet.registerFleetLocal(_uuid(100), US, ADMIN_CA, inclusionTier);
+ assertEq(fleet.fleetTier(tokenId), 1);
+ assertEq(fleet.regionTierCount(_regionUSCA()), 2);
+ }
+
+ // --- promote ---
+
+ function test_promote_next_movesToNextTierInRegion() public {
+ vm.prank(alice);
+ uint256 tokenId = fleet.registerFleetCountry(UUID_1, US, 0);
+
+ vm.prank(alice);
+ fleet.promote(tokenId);
+
+ assertEq(fleet.fleetTier(tokenId), 1);
+ assertEq(fleet.bonds(tokenId), fleet.tierBond(1, true));
+ }
+
+ function test_promote_next_pullsBondDifference() public {
+ vm.prank(alice);
+ uint256 tokenId = fleet.registerFleetLocal(UUID_1, US, ADMIN_CA, 0);
+
+ uint256 balBefore = bondToken.balanceOf(alice);
+ uint256 diff = fleet.tierBond(1, false) - fleet.tierBond(0, false);
+
+ vm.prank(alice);
+ fleet.promote(tokenId);
+
+ assertEq(bondToken.balanceOf(alice), balBefore - diff);
+ }
+
+ function test_reassignTier_promotesWhenTargetHigher() public {
+ vm.prank(alice);
+ uint256 tokenId = fleet.registerFleetLocal(UUID_1, US, ADMIN_CA, 0);
+
+ vm.prank(alice);
+ fleet.reassignTier(tokenId, 3);
+
+ assertEq(fleet.fleetTier(tokenId), 3);
+ assertEq(fleet.bonds(tokenId), fleet.tierBond(3, false));
+ assertEq(fleet.regionTierCount(_regionUSCA()), 4);
+ }
+
+ function test_promote_emitsEvent() public {
+ vm.prank(alice);
+ uint256 tokenId = fleet.registerFleetLocal(UUID_1, US, ADMIN_CA, 0);
+ uint256 diff = fleet.tierBond(1, false) - fleet.tierBond(0, false);
+
+ vm.expectEmit(true, true, true, true);
+ emit FleetPromoted(tokenId, 0, 1, diff);
+
+ vm.prank(alice);
+ fleet.promote(tokenId);
+ }
+
+ function test_RevertIf_promote_notOperator() public {
+ vm.prank(alice);
+ uint256 tokenId = fleet.registerFleetLocal(UUID_1, US, ADMIN_CA, 0);
+
+ vm.prank(bob);
+ vm.expectRevert(FleetIdentityUpgradeable.NotOperator.selector);
+ fleet.promote(tokenId);
+ }
+
+ function test_RevertIf_reassignTier_targetSameAsCurrent() public {
+ vm.prank(alice);
+ uint256 tokenId = fleet.registerFleetLocal(UUID_1, US, ADMIN_CA, 0);
+ vm.prank(alice);
+ fleet.reassignTier(tokenId, 2);
+
+ vm.prank(alice);
+ vm.expectRevert(FleetIdentityUpgradeable.TargetTierSameAsCurrent.selector);
+ fleet.reassignTier(tokenId, 2);
+ }
+
+ function test_RevertIf_promote_targetTierFull() public {
+ vm.prank(alice);
+ uint256 tokenId = fleet.registerFleetLocal(UUID_1, US, ADMIN_CA, 0);
+
+ // Fill tier 1 with TIER_CAPACITY members
+ for (uint256 i = 0; i < fleet.TIER_CAPACITY(); i++) {
+ vm.prank(bob);
+ fleet.registerFleetLocal(_uuid(50 + i), US, ADMIN_CA, 1);
+ }
+
+ vm.prank(alice);
+ vm.expectRevert(FleetIdentityUpgradeable.TierFull.selector);
+ fleet.promote(tokenId);
+ }
+
+ function test_RevertIf_reassignTier_exceedsMaxTiers() public {
+ vm.prank(alice);
+ uint256 tokenId = fleet.registerFleetLocal(UUID_1, US, ADMIN_CA, 0);
+
+ vm.prank(alice);
+ vm.expectRevert(FleetIdentityUpgradeable.MaxTiersReached.selector);
+ fleet.reassignTier(tokenId, 50);
+ }
+
+ // --- reassignTier (demote direction) ---
+
+ function test_reassignTier_demotesWhenTargetLower() public {
+ vm.prank(alice);
+ uint256 tokenId = fleet.registerFleetCountry(UUID_1, DE, 0);
+ vm.prank(alice);
+ fleet.reassignTier(tokenId, 3);
+
+ vm.prank(alice);
+ fleet.reassignTier(tokenId, 1);
+
+ assertEq(fleet.fleetTier(tokenId), 1);
+ assertEq(fleet.bonds(tokenId), fleet.tierBond(1, true));
+ }
+
+ function test_reassignTier_demoteRefundsBondDifference() public {
+ vm.prank(alice);
+ uint256 tokenId = fleet.registerFleetLocal(UUID_1, US, ADMIN_CA, 0);
+ vm.prank(alice);
+ fleet.reassignTier(tokenId, 3);
+
+ uint256 balBefore = bondToken.balanceOf(alice);
+ uint256 refund = fleet.tierBond(3, false) - fleet.tierBond(1, false);
+
+ vm.prank(alice);
+ fleet.reassignTier(tokenId, 1);
+
+ assertEq(bondToken.balanceOf(alice), balBefore + refund);
+ }
+
+ function test_reassignTier_demoteEmitsEvent() public {
+ vm.prank(alice);
+ uint256 tokenId = fleet.registerFleetLocal(UUID_1, US, ADMIN_CA, 0);
+ vm.prank(alice);
+ fleet.reassignTier(tokenId, 3);
+ uint256 refund = fleet.tierBond(3, false) - fleet.tierBond(1, false);
+
+ vm.expectEmit(true, true, true, true);
+ emit FleetDemoted(tokenId, 3, 1, refund);
+
+ vm.prank(alice);
+ fleet.reassignTier(tokenId, 1);
+ }
+
+ function test_reassignTier_demoteTrimsTierCountWhenTopEmpties() public {
+ vm.prank(alice);
+ uint256 tokenId = fleet.registerFleetLocal(UUID_1, US, ADMIN_CA, 0);
+ vm.prank(alice);
+ fleet.reassignTier(tokenId, 3);
+ assertEq(fleet.regionTierCount(_regionUSCA()), 4);
+
+ vm.prank(alice);
+ fleet.reassignTier(tokenId, 0);
+ assertEq(fleet.regionTierCount(_regionUSCA()), 1);
+ }
+
+ function test_RevertIf_reassignTier_demoteNotOperator() public {
+ vm.prank(alice);
+ uint256 tokenId = fleet.registerFleetLocal(UUID_1, US, ADMIN_CA, 0);
+ vm.prank(alice);
+ fleet.reassignTier(tokenId, 2);
+
+ vm.prank(bob);
+ vm.expectRevert(FleetIdentityUpgradeable.NotOperator.selector);
+ fleet.reassignTier(tokenId, 0);
+ }
+
+ function test_RevertIf_reassignTier_demoteTargetTierFull() public {
+ _registerNLocalAt(alice, US, ADMIN_CA, fleet.TIER_CAPACITY(), 0, 0);
+
+ // Register at tier 1 since tier 0 is full, then promote further
+ vm.prank(bob);
+ uint256 tokenId = fleet.registerFleetLocal(_uuid(100), US, ADMIN_CA, 1);
+ vm.prank(bob);
+ fleet.reassignTier(tokenId, 2);
+
+ vm.prank(bob);
+ vm.expectRevert(FleetIdentityUpgradeable.TierFull.selector);
+ fleet.reassignTier(tokenId, 0);
+ }
+
+ function test_RevertIf_reassignTier_promoteNotOperator() public {
+ vm.prank(alice);
+ uint256 tokenId = fleet.registerFleetLocal(UUID_1, US, ADMIN_CA, 0);
+
+ vm.prank(bob);
+ vm.expectRevert(FleetIdentityUpgradeable.NotOperator.selector);
+ fleet.reassignTier(tokenId, 3);
+ }
+
+ // --- burn ---
+
+ function test_burn_refundsTierBond() public {
+ vm.prank(alice);
+ uint256 tokenId = fleet.registerFleetLocal(UUID_1, US, ADMIN_CA, 0);
+ uint256 balBefore = bondToken.balanceOf(alice);
+
+ vm.prank(alice);
+ fleet.burn(tokenId);
+
+ // Last registered token burn -> transitions to owned-only
+ // Operator (alice) gets tierBond refund, owned-only token minted (holds BASE_BOND)
+ assertEq(bondToken.balanceOf(alice), balBefore + fleet.tierBond(0, false));
+ assertEq(bondToken.balanceOf(address(fleet)), BASE_BOND); // owned-only token holds BASE_BOND
+ assertEq(fleet.bonds(tokenId), 0);
+
+ // Verify owned-only token was minted
+ uint256 ownedTokenId = uint256(uint128(UUID_1));
+ assertEq(fleet.ownerOf(ownedTokenId), alice);
+ assertTrue(fleet.isOwnedOnly(UUID_1));
+ }
+
+ function test_burn_emitsEvent() public {
+ vm.prank(alice);
+ uint256 tokenId = fleet.registerFleetLocal(UUID_1, US, ADMIN_CA, 0);
+
+ vm.expectEmit(true, true, true, true);
+ // Event emits tier bond refund only (owned-only token keeps BASE_BOND)
+ emit FleetBurned(alice, tokenId, _regionUSCA(), 0, fleet.tierBond(0, false));
+
+ vm.prank(alice);
+ fleet.burn(tokenId);
+ }
+
+ function test_burn_trimsTierCount() public {
+ vm.prank(alice);
+ uint256 tokenId = fleet.registerFleetCountry(UUID_1, US, 0);
+ vm.prank(alice);
+ fleet.reassignTier(tokenId, 3);
+ assertEq(fleet.regionTierCount(_regionUS()), 4);
+
+ vm.prank(alice);
+ fleet.burn(tokenId);
+ assertEq(fleet.regionTierCount(_regionUS()), 0);
+
+ // Verify transitioned to owned-only
+ assertTrue(fleet.isOwnedOnly(UUID_1));
+ }
+
+ function test_burn_allowsReregistration_sameRegion() public {
+ vm.prank(alice);
+ uint256 tokenId = fleet.registerFleetLocal(UUID_1, US, ADMIN_CA, 0);
+
+ vm.prank(alice);
+ fleet.burn(tokenId);
+
+ // Now in owned-only state - burn that too to fully release
+ uint256 ownedTokenId = uint256(uint128(UUID_1));
+ vm.prank(alice);
+ fleet.burn(ownedTokenId);
+
+ // Same UUID can be re-registered in same region, same tokenId
+ vm.prank(bob);
+ uint256 newId = fleet.registerFleetLocal(UUID_1, US, ADMIN_CA, 0);
+ assertEq(newId, tokenId);
+ assertEq(fleet.tokenRegion(newId), _regionUSCA());
+ }
+
+ function test_multiRegion_sameUuidCanRegisterInDifferentRegions() public {
+ // Same UUID can be registered in multiple regions simultaneously (by SAME owner, SAME level)
+ vm.prank(alice);
+ uint256 localId1 = fleet.registerFleetLocal(UUID_1, US, ADMIN_CA, 0);
+
+ vm.prank(alice);
+ uint256 localId2 = fleet.registerFleetLocal(UUID_1, DE, ADMIN_CA, 0);
+
+ // Different tokenIds for different regions
+ assertTrue(localId1 != localId2, "Different regions should have different tokenIds");
+
+ // Both have same UUID but different regions
+ assertEq(fleet.tokenUuid(localId1), UUID_1);
+ assertEq(fleet.tokenUuid(localId2), UUID_1);
+ assertEq(fleet.tokenRegion(localId1), _regionUSCA());
+ assertEq(fleet.tokenRegion(localId2), _makeAdminRegion(DE, ADMIN_CA));
+
+ // Both owned by alice
+ assertEq(fleet.ownerOf(localId1), alice);
+ assertEq(fleet.ownerOf(localId2), alice);
+ }
+
+ function test_RevertIf_burn_registeredToken_notOperator() public {
+ vm.prank(alice);
+ uint256 tokenId = fleet.registerFleetLocal(UUID_1, US, ADMIN_CA, 0);
+
+ // Bob is not operator - should revert
+ vm.prank(bob);
+ vm.expectRevert(FleetIdentityUpgradeable.NotOperator.selector);
+ fleet.burn(tokenId);
+ }
+
+ // --- localInclusionHint ---
+
+ function test_localInclusionHint_emptyRegion() public view {
+ (uint256 tier, uint256 bond) = fleet.localInclusionHint(US, ADMIN_CA);
+ assertEq(tier, 0);
+ assertEq(bond, BASE_BOND);
+ }
+
+ function test_localInclusionHint_afterFillingAdminTier0() public {
+ _registerNLocalAt(alice, US, ADMIN_CA, fleet.TIER_CAPACITY(), 0, 0);
+
+ // Admin tier 0 full → cheapest inclusion is tier 1.
+ (uint256 tier, uint256 bond) = fleet.localInclusionHint(US, ADMIN_CA);
+ assertEq(tier, 1);
+ assertEq(bond, BASE_BOND * 2);
+ }
+
+ // --- highestActiveTier ---
+
+ function test_highestActiveTier_noFleets() public view {
+ assertEq(fleet.highestActiveTier(_regionUS()), 0);
+ assertEq(fleet.highestActiveTier(_regionUSCA()), 0);
+ }
+
+ function test_highestActiveTier_afterRegistrations() public {
+ vm.prank(alice);
+ uint256 tokenId = fleet.registerFleetCountry(UUID_1, US, 0);
+ vm.prank(alice);
+ fleet.reassignTier(tokenId, 3);
+ assertEq(fleet.highestActiveTier(_regionUS()), 3);
+
+ // Different region still at 0
+ assertEq(fleet.highestActiveTier(_regionDE()), 0);
+ }
+
+ // --- EdgeBeaconScanner helpers ---
+
+ function test_tierMemberCount_perRegion() public {
+ _registerNLocalAt(alice, US, ADMIN_CA, 3, 0, 0);
+ _registerNCountryAt(bob, US, 4, 100, 0);
+
+ assertEq(fleet.tierMemberCount(_regionUSCA(), 0), 3);
+ assertEq(fleet.tierMemberCount(_regionUS(), 0), 4);
+ }
+
+ function test_getTierMembers_perRegion() public {
+ vm.prank(alice);
+ uint256 usId = fleet.registerFleetCountry(UUID_1, US, 0);
+
+ vm.prank(bob);
+ uint256 uscaId = fleet.registerFleetLocal(UUID_2, US, ADMIN_CA, 0);
+
+ uint256[] memory usMembers = fleet.getTierMembers(_regionUS(), 0);
+ assertEq(usMembers.length, 1);
+ assertEq(usMembers[0], usId);
+
+ uint256[] memory uscaMembers = fleet.getTierMembers(_regionUSCA(), 0);
+ assertEq(uscaMembers.length, 1);
+ assertEq(uscaMembers[0], uscaId);
+ }
+
+ function test_getTierUuids_perRegion() public {
+ vm.prank(alice);
+ fleet.registerFleetCountry(UUID_1, US, 0);
+
+ vm.prank(bob);
+ fleet.registerFleetLocal(UUID_2, US, ADMIN_CA, 0);
+
+ bytes16[] memory usUUIDs = fleet.getTierUuids(_regionUS(), 0);
+ assertEq(usUUIDs.length, 1);
+ assertEq(usUUIDs[0], UUID_1);
+
+ bytes16[] memory uscaUUIDs = fleet.getTierUuids(_regionUSCA(), 0);
+ assertEq(uscaUUIDs.length, 1);
+ assertEq(uscaUUIDs[0], UUID_2);
+ }
+
+ // --- Region indexes ---
+
+ function test_activeCountries_addedOnRegistration() public {
+ vm.prank(alice);
+ fleet.registerFleetCountry(UUID_1, US, 0);
+ vm.prank(bob);
+ fleet.registerFleetCountry(UUID_2, DE, 0);
+
+ uint16[] memory countries = fleet.getActiveCountries();
+ assertEq(countries.length, 2);
+ }
+
+ function test_activeCountries_removedWhenAllBurned() public {
+ vm.prank(alice);
+ uint256 id1 = fleet.registerFleetCountry(UUID_1, US, 0);
+
+ uint16[] memory before_ = fleet.getActiveCountries();
+ assertEq(before_.length, 1);
+
+ vm.prank(alice);
+ fleet.burn(id1);
+
+ uint16[] memory after_ = fleet.getActiveCountries();
+ assertEq(after_.length, 0);
+ }
+
+ function test_activeCountries_notDuplicated() public {
+ vm.prank(alice);
+ fleet.registerFleetCountry(UUID_1, US, 0);
+ vm.prank(bob);
+ fleet.registerFleetCountry(UUID_2, US, 0);
+
+ uint16[] memory countries = fleet.getActiveCountries();
+ assertEq(countries.length, 1);
+ assertEq(countries[0], US);
+ }
+
+ function test_activeAdminAreas_trackedCorrectly() public {
+ vm.prank(alice);
+ fleet.registerFleetLocal(UUID_1, US, ADMIN_CA, 0);
+ vm.prank(bob);
+ fleet.registerFleetLocal(UUID_2, US, ADMIN_NY, 0);
+
+ uint32[] memory areas = fleet.getActiveAdminAreas();
+ assertEq(areas.length, 2);
+ }
+
+ function test_activeAdminAreas_removedWhenAllBurned() public {
+ vm.prank(alice);
+ uint256 id1 = fleet.registerFleetLocal(UUID_1, US, ADMIN_CA, 0);
+
+ assertEq(fleet.getActiveAdminAreas().length, 1);
+
+ vm.prank(alice);
+ fleet.burn(id1);
+
+ assertEq(fleet.getActiveAdminAreas().length, 0);
+ }
+
+ function test_activeAdminAreas_multipleCountries() public {
+ // Register admin areas in multiple countries
+ vm.prank(alice);
+ fleet.registerFleetLocal(UUID_1, US, ADMIN_CA, 0);
+ vm.prank(bob);
+ fleet.registerFleetLocal(UUID_2, DE, ADMIN_CA, 0);
+
+ uint32[] memory areas = fleet.getActiveAdminAreas();
+ assertEq(areas.length, 2);
+ }
+
+ function test_adminAreaSwapAndPop_whenNotLastArea() public {
+ // Register two admin areas in the same country
+ vm.prank(alice);
+ uint256 id1 = fleet.registerFleetLocal(UUID_1, US, ADMIN_CA, 0);
+ vm.prank(bob);
+ fleet.registerFleetLocal(UUID_2, US, ADMIN_NY, 0);
+
+ // Burn the first one (not the last in the array) to trigger swap-and-pop
+ vm.prank(alice);
+ fleet.burn(id1);
+
+ // Should still have one admin area
+ uint32[] memory areas = fleet.getActiveAdminAreas();
+ assertEq(areas.length, 1);
+ // The remaining area should be ADMIN_NY
+ assertEq(areas[0], fleet.makeAdminRegion(US, ADMIN_NY));
+ }
+
+ function test_countrySwapAndPop_whenNotLastCountry() public {
+ // Register admin areas in multiple countries
+ vm.prank(alice);
+ uint256 id1 = fleet.registerFleetLocal(UUID_1, US, ADMIN_CA, 0);
+ vm.prank(bob);
+ fleet.registerFleetLocal(UUID_2, DE, ADMIN_CA, 0);
+ vm.prank(carol);
+ fleet.registerFleetLocal(UUID_3, FR, ADMIN_CA, 0);
+
+ // Burn the first country's fleet (not the last country in the array) to trigger swap-and-pop
+ vm.prank(alice);
+ fleet.burn(id1);
+
+ // Should still have two countries
+ uint16[] memory countries = fleet.getActiveCountries();
+ assertEq(countries.length, 2);
+ }
+
+ // --- Region key helpers ---
+
+ function test_makeAdminRegion() public view {
+ assertEq(fleet.makeAdminRegion(US, ADMIN_CA), (uint32(US) << 10) | uint32(ADMIN_CA));
+ }
+
+ function test_regionKeyNoOverlap_countryVsAdmin() public pure {
+ uint32 maxCountry = 999;
+ uint32 minAdmin = (uint32(1) << 10) | uint32(1);
+ assertTrue(minAdmin > maxCountry);
+ }
+
+ // --- tokenUuid / bonds ---
+
+ function test_tokenUuid_roundTrip() public {
+ vm.prank(alice);
+ uint256 tokenId = fleet.registerFleetLocal(UUID_1, US, ADMIN_CA, 0);
+ assertEq(fleet.tokenUuid(tokenId), UUID_1);
+ }
+
+ function test_bonds_returnsTierBond() public {
+ vm.prank(alice);
+ uint256 tokenId = fleet.registerFleetLocal(UUID_1, US, ADMIN_CA, 0);
+ assertEq(fleet.bonds(tokenId), BASE_BOND);
+ }
+
+ function test_bonds_zeroForNonexistentToken() public view {
+ assertEq(fleet.bonds(99999), 0);
+ }
+
+ // --- ERC721Enumerable ---
+
+ function test_enumerable_totalSupply() public {
+ assertEq(fleet.totalSupply(), 0);
+
+ vm.prank(alice);
+ fleet.registerFleetCountry(UUID_1, US, 0);
+ assertEq(fleet.totalSupply(), 1);
+
+ vm.prank(bob);
+ fleet.registerFleetCountry(UUID_2, DE, 0);
+ assertEq(fleet.totalSupply(), 2);
+
+ vm.prank(carol);
+ fleet.registerFleetLocal(UUID_3, US, ADMIN_CA, 0);
+ assertEq(fleet.totalSupply(), 3);
+ }
+
+ function test_enumerable_supportsInterface() public view {
+ assertTrue(fleet.supportsInterface(0x780e9d63));
+ assertTrue(fleet.supportsInterface(0x80ac58cd));
+ assertTrue(fleet.supportsInterface(0x01ffc9a7));
+ }
+
+ // --- Bond accounting ---
+
+ function test_bondAccounting_acrossRegions() public {
+ vm.prank(alice);
+ uint256 c1 = fleet.registerFleetCountry(UUID_1, US, 0);
+ vm.prank(bob);
+ uint256 c2 = fleet.registerFleetCountry(UUID_2, DE, 0);
+ vm.prank(carol);
+ uint256 l1 = fleet.registerFleetLocal(UUID_3, US, ADMIN_CA, 0);
+
+ // Each token costs BASE_BOND + tierBond
+ // c1 and c2 are country (BASE_BOND + 16*BASE_BOND each), l1 is local (BASE_BOND + BASE_BOND)
+ uint256 countryTotal = 2 * (BASE_BOND + fleet.tierBond(0, true));
+ uint256 localTotal = BASE_BOND + fleet.tierBond(0, false);
+ assertEq(bondToken.balanceOf(address(fleet)), countryTotal + localTotal);
+
+ // Burn c2: transitions to owned-only (BASE_BOND stays in contract)
+ vm.prank(bob);
+ fleet.burn(c2);
+ uint256 ownedTokenBob = uint256(uint128(UUID_2));
+ // After burning c2, remaining: c1 + l1 + owned-only token for UUID_2
+ assertEq(bondToken.balanceOf(address(fleet)), (BASE_BOND + fleet.tierBond(0, true)) + (BASE_BOND + fleet.tierBond(0, false)) + BASE_BOND);
+
+ // Burn the owned-only token for UUID_2
+ vm.prank(bob);
+ fleet.burn(ownedTokenBob);
+ // Now: c1 + l1
+ assertEq(bondToken.balanceOf(address(fleet)), (BASE_BOND + fleet.tierBond(0, true)) + (BASE_BOND + fleet.tierBond(0, false)));
+
+ // Burn remaining tokens (and their resulting owned-only tokens)
+ vm.prank(alice);
+ fleet.burn(c1);
+ vm.prank(alice);
+ fleet.burn(uint256(uint128(UUID_1)));
+ vm.prank(carol);
+ fleet.burn(l1);
+ vm.prank(carol);
+ fleet.burn(uint256(uint128(UUID_3)));
+ assertEq(bondToken.balanceOf(address(fleet)), 0);
+ }
+
+ function test_bondAccounting_reassignTierRoundTrip() public {
+ vm.prank(alice);
+ uint256 tokenId = fleet.registerFleetLocal(UUID_1, US, ADMIN_CA, 0);
+ uint256 balStart = bondToken.balanceOf(alice);
+
+ vm.prank(alice);
+ fleet.reassignTier(tokenId, 3);
+
+ vm.prank(alice);
+ fleet.reassignTier(tokenId, 0);
+
+ assertEq(bondToken.balanceOf(alice), balStart);
+ assertEq(fleet.bonds(tokenId), BASE_BOND);
+ }
+
+ // --- ERC-20 edge case ---
+
+ function test_RevertIf_bondToken_transferFromReturnsFalse() public {
+ BadERC20 badToken = new BadERC20();
+
+ // Deploy implementation
+ FleetIdentityUpgradeable impl = new FleetIdentityUpgradeable();
+ // Deploy proxy with initialize call
+ ERC1967Proxy proxy = new ERC1967Proxy(
+ address(impl),
+ abi.encodeCall(FleetIdentityUpgradeable.initialize, (owner, address(badToken), BASE_BOND, 0))
+ );
+ FleetIdentityUpgradeable f = FleetIdentityUpgradeable(address(proxy));
+
+ badToken.mint(alice, 1_000 ether);
+ vm.prank(alice);
+ badToken.approve(address(f), type(uint256).max);
+
+ badToken.setFail(true);
+
+ vm.prank(alice);
+ vm.expectRevert();
+ f.registerFleetLocal(UUID_1, US, ADMIN_CA, 0);
+ }
+
+ // --- Transfer preserves region and tier ---
+
+ function test_transfer_regionAndTierStayWithToken() public {
+ vm.prank(alice);
+ uint256 tokenId = fleet.registerFleetCountry(UUID_1, US, 0);
+ vm.prank(alice);
+ fleet.reassignTier(tokenId, 2);
+
+ vm.prank(alice);
+ fleet.transferFrom(alice, bob, tokenId);
+
+ assertEq(fleet.tokenRegion(tokenId), _regionUS());
+ assertEq(fleet.fleetTier(tokenId), 2);
+ assertEq(fleet.bonds(tokenId), fleet.tierBond(2, true));
+
+ // After transfer, bob holds the token but alice is still uuidOwner/operator.
+ // On burn, operator (alice) gets full tierBond, owned-only token minted to owner (alice).
+ uint256 aliceBefore = bondToken.balanceOf(alice);
+ vm.prank(alice); // operator burns
+ fleet.burn(tokenId);
+ // Alice gets tier bond refund
+ assertEq(bondToken.balanceOf(alice), aliceBefore + fleet.tierBond(2, true));
+ // Owned-only token minted to alice
+ assertTrue(fleet.isOwnedOnly(UUID_1));
+ }
+
+ // --- Tier lifecycle ---
+
+ function test_tierLifecycle_fillBurnBackfillPerRegion() public {
+ // Register 4 US country fleets at tier 0 (fills capacity)
+ uint256[] memory usIds = _registerNCountryAt(alice, US, 4, 0, 0);
+ assertEq(fleet.tierMemberCount(_regionUS(), 0), 4);
+
+ // Next country fleet goes to tier 1
+ vm.prank(bob);
+ uint256 us5 = fleet.registerFleetCountry(_uuid(100), US, 1);
+ assertEq(fleet.fleetTier(us5), 1);
+
+ // Burn from tier 0 — now tier 0 has 3, tier 1 has 1.
+ vm.prank(alice);
+ fleet.burn(usIds[3]);
+
+ // Explicitly register into tier 1.
+ vm.prank(carol);
+ uint256 backfill = fleet.registerFleetCountry(_uuid(200), US, 1);
+ assertEq(fleet.fleetTier(backfill), 1);
+ assertEq(fleet.tierMemberCount(_regionUS(), 1), 2);
+ }
+
+ // --- Edge cases ---
+
+ function test_initialize_zeroBaseBond_usesDefault() public {
+ // Deploy implementation
+ FleetIdentityUpgradeable impl = new FleetIdentityUpgradeable();
+ // Deploy proxy with zero base bond - should use DEFAULT_BASE_BOND
+ ERC1967Proxy proxy = new ERC1967Proxy(
+ address(impl),
+ abi.encodeCall(FleetIdentityUpgradeable.initialize, (owner, address(bondToken), 0, 0))
+ );
+ FleetIdentityUpgradeable f = FleetIdentityUpgradeable(address(proxy));
+ assertEq(f.BASE_BOND(), f.DEFAULT_BASE_BOND());
+ }
+
+ function test_initialize_zeroMultiplier_usesDefault() public {
+ // Deploy implementation
+ FleetIdentityUpgradeable impl = new FleetIdentityUpgradeable();
+ // Deploy proxy with zero multiplier - should use DEFAULT_COUNTRY_BOND_MULTIPLIER
+ ERC1967Proxy proxy = new ERC1967Proxy(
+ address(impl),
+ abi.encodeCall(FleetIdentityUpgradeable.initialize, (owner, address(bondToken), BASE_BOND, 0))
+ );
+ FleetIdentityUpgradeable f = FleetIdentityUpgradeable(address(proxy));
+ assertEq(f.countryBondMultiplier(), f.DEFAULT_COUNTRY_BOND_MULTIPLIER());
+ }
+
+ function test_initialize_revertsOnZeroBondToken() public {
+ // Deploy implementation
+ FleetIdentityUpgradeable impl = new FleetIdentityUpgradeable();
+ // Attempt to deploy proxy with zero bond token - should revert
+ vm.expectRevert(FleetIdentityUpgradeable.InvalidBondToken.selector);
+ new ERC1967Proxy(
+ address(impl),
+ abi.encodeCall(FleetIdentityUpgradeable.initialize, (owner, address(0), BASE_BOND, 0))
+ );
+ }
+
+ // --- Fuzz Tests ---
+
+ function testFuzz_registerFleetCountry_validCountryCodes(uint16 cc) public {
+ cc = uint16(bound(cc, 1, 999));
+
+ vm.prank(alice);
+ uint256 tokenId = fleet.registerFleetCountry(UUID_1, cc, 0);
+
+ assertEq(fleet.tokenRegion(tokenId), uint32(cc));
+ assertEq(fleet.fleetTier(tokenId), 0);
+ assertEq(fleet.bonds(tokenId), BASE_BOND * fleet.countryBondMultiplier()); // Country gets 16x multiplier
+ }
+
+ function testFuzz_registerFleetLocal_validCodes(uint16 cc, uint16 admin) public {
+ cc = uint16(bound(cc, 1, 999));
+ admin = uint16(bound(admin, 1, 255));
+
+ vm.prank(alice);
+ uint256 tokenId = fleet.registerFleetLocal(UUID_1, cc, admin, 0);
+
+ uint32 expectedRegion = (uint32(cc) << 10) | uint32(admin);
+ assertEq(fleet.tokenRegion(tokenId), expectedRegion);
+ assertEq(fleet.fleetTier(tokenId), 0);
+ assertEq(fleet.bonds(tokenId), BASE_BOND);
+ }
+
+ function testFuzz_promote_onlyOperator(address caller) public {
+ vm.assume(caller != alice);
+ vm.assume(caller != address(0));
+
+ vm.prank(alice);
+ uint256 tokenId = fleet.registerFleetLocal(UUID_1, US, ADMIN_CA, 0);
+
+ vm.prank(caller);
+ vm.expectRevert(FleetIdentityUpgradeable.NotOperator.selector);
+ fleet.promote(tokenId);
+ }
+
+ function testFuzz_burn_onlyOperator(address caller) public {
+ vm.assume(caller != alice);
+ vm.assume(caller != address(0));
+
+ vm.prank(alice);
+ uint256 tokenId = fleet.registerFleetLocal(UUID_1, US, ADMIN_CA, 0);
+
+ // Only operator (alice) can burn registered tokens, not random callers
+ vm.prank(caller);
+ vm.expectRevert(FleetIdentityUpgradeable.NotOperator.selector);
+ fleet.burn(tokenId);
+ }
+
+ // ══════════════════════════════════════════════
+ // UUID Ownership Enforcement Tests
+ // ══════════════════════════════════════════════
+
+ function test_uuidOwner_setOnFirstRegistration() public {
+ assertEq(fleet.uuidOwner(UUID_1), address(0), "No owner before registration");
+ assertEq(fleet.uuidTokenCount(UUID_1), 0, "No tokens before registration");
+
+ vm.prank(alice);
+ fleet.registerFleetLocal(UUID_1, US, ADMIN_CA, 0);
+
+ assertEq(fleet.uuidOwner(UUID_1), alice, "Alice is UUID owner after registration");
+ assertEq(fleet.uuidTokenCount(UUID_1), 1, "Token count is 1 after registration");
+ }
+
+ function test_uuidOwner_sameOwnerCanRegisterMultipleRegions() public {
+ // Alice registers UUID_1 in first region (same level across all)
+ vm.prank(alice);
+ uint256 id1 = fleet.registerFleetLocal(UUID_1, US, ADMIN_CA, 0);
+
+ // Alice can register same UUID in second region (same level)
+ vm.prank(alice);
+ uint256 id2 = fleet.registerFleetLocal(UUID_1, DE, ADMIN_CA, 0);
+
+ // And a third region (same level)
+ vm.prank(alice);
+ uint256 id3 = fleet.registerFleetLocal(UUID_1, FR, ADMIN_CA, 0);
+
+ assertEq(fleet.uuidOwner(UUID_1), alice, "Alice is still UUID owner");
+ assertEq(fleet.uuidTokenCount(UUID_1), 3, "Token count is 3");
+ assertEq(fleet.ownerOf(id1), alice);
+ assertEq(fleet.ownerOf(id2), alice);
+ assertEq(fleet.ownerOf(id3), alice);
+ }
+
+ function test_RevertIf_differentOwnerRegistersSameUuid_local() public {
+ // Alice registers UUID_1 first
+ vm.prank(alice);
+ fleet.registerFleetLocal(UUID_1, US, ADMIN_CA, 0);
+
+ // Bob tries to register same UUID in different region → revert
+ vm.prank(bob);
+ vm.expectRevert(FleetIdentityUpgradeable.NotOperator.selector);
+ fleet.registerFleetLocal(UUID_1, DE, ADMIN_CA, 0);
+ }
+
+ function test_RevertIf_differentOwnerRegistersSameUuid_country() public {
+ // Alice registers UUID_1 first
+ vm.prank(alice);
+ fleet.registerFleetCountry(UUID_1, US, 0);
+
+ // Bob tries to register same UUID in different country → revert
+ vm.prank(bob);
+ vm.expectRevert(FleetIdentityUpgradeable.NotOperator.selector);
+ fleet.registerFleetCountry(UUID_1, DE, 0);
+ }
+
+ function test_RevertIf_differentOwnerRegistersSameUuid_crossLevel() public {
+ // Alice registers UUID_1 at country level
+ vm.prank(alice);
+ fleet.registerFleetCountry(UUID_1, US, 0);
+
+ // Bob tries to register same UUID at local level → revert
+ vm.prank(bob);
+ vm.expectRevert(FleetIdentityUpgradeable.NotOperator.selector);
+ fleet.registerFleetLocal(UUID_1, DE, ADMIN_CA, 0);
+ }
+
+ function test_uuidOwner_clearedWhenAllTokensBurned() public {
+ // Alice registers UUID_1 in one region
+ vm.prank(alice);
+ uint256 tokenId = fleet.registerFleetLocal(UUID_1, US, ADMIN_CA, 0);
+
+ assertEq(fleet.uuidOwner(UUID_1), alice);
+ assertEq(fleet.uuidTokenCount(UUID_1), 1);
+
+ // Burn the registered token -> transitions to owned-only
+ vm.prank(alice);
+ fleet.burn(tokenId);
+
+ // UUID owner should NOT be cleared yet (now in owned-only state)
+ assertEq(fleet.uuidOwner(UUID_1), alice, "UUID owner preserved in owned-only state");
+ assertTrue(fleet.isOwnedOnly(UUID_1));
+
+ // Burn the owned-only token to fully clear ownership
+ uint256 ownedTokenId = uint256(uint128(UUID_1));
+ vm.prank(alice);
+ fleet.burn(ownedTokenId);
+
+ // NOW UUID owner should be cleared
+ assertEq(fleet.uuidOwner(UUID_1), address(0), "UUID owner cleared after owned-only token burned");
+ assertEq(fleet.uuidTokenCount(UUID_1), 0, "Token count is 0 after all burned");
+ }
+
+ function test_uuidOwner_notClearedWhileTokensRemain() public {
+ // Alice registers UUID_1 in two regions (same level)
+ vm.prank(alice);
+ uint256 id1 = fleet.registerFleetLocal(UUID_1, US, ADMIN_CA, 0);
+
+ vm.prank(alice);
+ uint256 id2 = fleet.registerFleetLocal(UUID_1, DE, ADMIN_CA, 0);
+
+ assertEq(fleet.uuidTokenCount(UUID_1), 2);
+
+ // Burn first token
+ vm.prank(alice);
+ fleet.burn(id1);
+
+ // UUID owner should still be alice (one token remains)
+ assertEq(fleet.uuidOwner(UUID_1), alice, "UUID owner still alice with remaining token");
+ assertEq(fleet.uuidTokenCount(UUID_1), 1, "Token count decremented to 1");
+
+ // Burn second token -> transitions to owned-only
+ vm.prank(alice);
+ fleet.burn(id2);
+
+ // Still owned (in owned-only state)
+ assertEq(fleet.uuidOwner(UUID_1), alice, "UUID owner preserved in owned-only state");
+ assertTrue(fleet.isOwnedOnly(UUID_1));
+
+ // Burn owned-only token to fully clear
+ uint256 ownedTokenId = uint256(uint128(UUID_1));
+ vm.prank(alice);
+ fleet.burn(ownedTokenId);
+
+ // Now UUID owner should be cleared
+ assertEq(fleet.uuidOwner(UUID_1), address(0), "UUID owner cleared after owned-only burned");
+ assertEq(fleet.uuidTokenCount(UUID_1), 0);
+ }
+
+ function test_uuidOwner_differentUuidsHaveDifferentOwners() public {
+ // Alice registers UUID_1
+ vm.prank(alice);
+ fleet.registerFleetLocal(UUID_1, US, ADMIN_CA, 0);
+
+ // Bob registers UUID_2 (different UUID, no conflict)
+ vm.prank(bob);
+ fleet.registerFleetLocal(UUID_2, US, ADMIN_CA, 0);
+
+ assertEq(fleet.uuidOwner(UUID_1), alice);
+ assertEq(fleet.uuidOwner(UUID_2), bob);
+ }
+
+ function test_uuidOwner_canReRegisterAfterBurningAll() public {
+ // Alice registers and burns UUID_1
+ vm.prank(alice);
+ uint256 tokenId = fleet.registerFleetLocal(UUID_1, US, ADMIN_CA, 0);
+ vm.prank(alice);
+ fleet.burn(tokenId);
+
+ // Now in owned-only state, burn that too
+ uint256 ownedTokenId = uint256(uint128(UUID_1));
+ vm.prank(alice);
+ fleet.burn(ownedTokenId);
+
+ // Bob can now register the same UUID (uuid owner was cleared)
+ vm.prank(bob);
+ uint256 newTokenId = fleet.registerFleetLocal(UUID_1, US, ADMIN_CA, 0);
+
+ assertEq(fleet.uuidOwner(UUID_1), bob, "Bob is now UUID owner");
+ assertEq(fleet.uuidTokenCount(UUID_1), 1);
+ assertEq(fleet.ownerOf(newTokenId), bob);
+ }
+
+ function test_uuidOwner_transferDoesNotChangeUuidOwner() public {
+ // Alice registers UUID_1
+ vm.prank(alice);
+ uint256 tokenId = fleet.registerFleetLocal(UUID_1, US, ADMIN_CA, 0);
+
+ assertEq(fleet.uuidOwner(UUID_1), alice);
+
+ // Alice transfers to Bob
+ vm.prank(alice);
+ fleet.transferFrom(alice, bob, tokenId);
+
+ // Token owner changed but UUID owner did not
+ assertEq(fleet.ownerOf(tokenId), bob);
+ assertEq(fleet.uuidOwner(UUID_1), alice, "UUID owner still alice after transfer");
+ }
+
+ function test_RevertIf_transferRecipientTriesToRegisterSameUuid() public {
+ // Alice registers UUID_1
+ vm.prank(alice);
+ uint256 tokenId = fleet.registerFleetLocal(UUID_1, US, ADMIN_CA, 0);
+
+ // Alice transfers to Bob
+ vm.prank(alice);
+ fleet.transferFrom(alice, bob, tokenId);
+
+ // Bob now owns tokenId, but cannot register NEW tokens for UUID_1
+ vm.prank(bob);
+ vm.expectRevert(FleetIdentityUpgradeable.NotOperator.selector);
+ fleet.registerFleetLocal(UUID_1, DE, ADMIN_CA, 0);
+ }
+
+ function test_uuidOwner_originalOwnerCanStillRegisterAfterTransfer() public {
+ // Alice registers UUID_1 in one region
+ vm.prank(alice);
+ uint256 tokenId = fleet.registerFleetLocal(UUID_1, US, ADMIN_CA, 0);
+
+ // Alice transfers to Bob
+ vm.prank(alice);
+ fleet.transferFrom(alice, bob, tokenId);
+
+ // Alice can still register UUID_1 in new regions (she's still uuidOwner, same level)
+ vm.prank(alice);
+ uint256 newTokenId = fleet.registerFleetLocal(UUID_1, DE, ADMIN_CA, 0);
+
+ assertEq(fleet.ownerOf(newTokenId), alice);
+ assertEq(fleet.uuidTokenCount(UUID_1), 2);
+ }
+
+ function testFuzz_uuidOwner_enforcedAcrossAllRegions(uint16 cc1, uint16 cc2, uint16 admin1, uint16 admin2) public {
+ cc1 = uint16(bound(cc1, 1, 999));
+ cc2 = uint16(bound(cc2, 1, 999));
+ admin1 = uint16(bound(admin1, 1, 255));
+ admin2 = uint16(bound(admin2, 1, 255));
+
+ // Alice registers first
+ vm.prank(alice);
+ fleet.registerFleetLocal(UUID_1, cc1, admin1, 0);
+
+ // Bob cannot register same UUID anywhere
+ vm.prank(bob);
+ vm.expectRevert(FleetIdentityUpgradeable.NotOperator.selector);
+ fleet.registerFleetLocal(UUID_1, cc2, admin2, 0);
+
+ vm.prank(bob);
+ vm.expectRevert(FleetIdentityUpgradeable.NotOperator.selector);
+ fleet.registerFleetCountry(UUID_1, cc2, 0);
+ }
+
+ function testFuzz_uuidOwner_multiRegionTokenCount(uint8 regionCount) public {
+ regionCount = uint8(bound(regionCount, 1, 10));
+
+ for (uint8 i = 0; i < regionCount; i++) {
+ uint16 cc = uint16(1 + i);
+ vm.prank(alice);
+ fleet.registerFleetCountry(UUID_1, cc, 0);
+ }
+
+ assertEq(fleet.uuidTokenCount(UUID_1), regionCount);
+ assertEq(fleet.uuidOwner(UUID_1), alice);
+ }
+
+ function testFuzz_uuidOwner_partialBurnPreservesOwnership(uint8 burnCount) public {
+ uint8 totalTokens = 5;
+ burnCount = uint8(bound(burnCount, 1, totalTokens - 1));
+
+ // Register tokens
+ uint256[] memory tokenIds = new uint256[](totalTokens);
+ for (uint8 i = 0; i < totalTokens; i++) {
+ uint16 cc = uint16(1 + i);
+ vm.prank(alice);
+ tokenIds[i] = fleet.registerFleetCountry(UUID_1, cc, 0);
+ }
+
+ assertEq(fleet.uuidTokenCount(UUID_1), totalTokens);
+
+ // Burn some tokens
+ for (uint8 i = 0; i < burnCount; i++) {
+ vm.prank(alice);
+ fleet.burn(tokenIds[i]);
+ }
+
+ // Owner still alice, count decreased
+ assertEq(fleet.uuidOwner(UUID_1), alice);
+ assertEq(fleet.uuidTokenCount(UUID_1), totalTokens - burnCount);
+ }
+
+ // ══════════════════════════════════════════════
+ // UUID Level Enforcement Tests
+ // ══════════════════════════════════════════════
+
+ function test_uuidLevel_setOnFirstRegistration_local() public {
+ assertEq(uint8(fleet.uuidLevel(UUID_1)), 0, "No level before registration");
+
+ vm.prank(alice);
+ fleet.registerFleetLocal(UUID_1, US, ADMIN_CA, 0);
+
+ assertEq(uint8(fleet.uuidLevel(UUID_1)), 2, "Level is 2 (local) after local registration");
+ }
+
+ function test_uuidLevel_setOnFirstRegistration_country() public {
+ assertEq(uint8(fleet.uuidLevel(UUID_1)), 0, "No level before registration");
+
+ vm.prank(alice);
+ fleet.registerFleetCountry(UUID_1, US, 0);
+
+ assertEq(uint8(fleet.uuidLevel(UUID_1)), 3, "Level is 3 (country) after country registration");
+ }
+
+ function test_RevertIf_crossLevelRegistration_localThenCountry() public {
+ // Alice registers UUID_1 at local level
+ vm.prank(alice);
+ fleet.registerFleetLocal(UUID_1, US, ADMIN_CA, 0);
+
+ // Alice tries to register same UUID at country level → revert
+ vm.prank(alice);
+ vm.expectRevert(FleetIdentityUpgradeable.UuidLevelMismatch.selector);
+ fleet.registerFleetCountry(UUID_1, DE, 0);
+ }
+
+ function test_RevertIf_crossLevelRegistration_countryThenLocal() public {
+ // Alice registers UUID_1 at country level
+ vm.prank(alice);
+ fleet.registerFleetCountry(UUID_1, US, 0);
+
+ // Alice tries to register same UUID at local level → revert
+ vm.prank(alice);
+ vm.expectRevert(FleetIdentityUpgradeable.UuidLevelMismatch.selector);
+ fleet.registerFleetLocal(UUID_1, DE, ADMIN_CA, 0);
+ }
+
+ function test_uuidLevel_clearedOnLastTokenBurn() public {
+ vm.prank(alice);
+ uint256 tokenId = fleet.registerFleetLocal(UUID_1, US, ADMIN_CA, 0);
+
+ assertEq(uint8(fleet.uuidLevel(UUID_1)), 2);
+
+ // Burn -> transitions to owned-only (level = 1)
+ vm.prank(alice);
+ fleet.burn(tokenId);
+
+ assertEq(uint8(fleet.uuidLevel(UUID_1)), 1, "Level is Owned after burning last registered token");
+
+ // Burn owned-only token to fully clear
+ uint256 ownedTokenId = uint256(uint128(UUID_1));
+ vm.prank(alice);
+ fleet.burn(ownedTokenId);
+
+ assertEq(uint8(fleet.uuidLevel(UUID_1)), 0, "Level cleared after owned-only token burned");
+ }
+
+ function test_uuidLevel_notClearedWhileTokensRemain() public {
+ vm.prank(alice);
+ uint256 id1 = fleet.registerFleetLocal(UUID_1, US, ADMIN_CA, 0);
+
+ vm.prank(alice);
+ fleet.registerFleetLocal(UUID_1, DE, ADMIN_CA, 0);
+
+ assertEq(uint8(fleet.uuidLevel(UUID_1)), 2);
+
+ vm.prank(alice);
+ fleet.burn(id1);
+
+ assertEq(uint8(fleet.uuidLevel(UUID_1)), 2, "Level preserved while tokens remain");
+ }
+
+ function test_uuidLevel_canChangeLevelAfterBurningAll() public {
+ // Register as local
+ vm.prank(alice);
+ uint256 tokenId = fleet.registerFleetLocal(UUID_1, US, ADMIN_CA, 0);
+ assertEq(uint8(fleet.uuidLevel(UUID_1)), 2);
+
+ // Burn
+ vm.prank(alice);
+ fleet.burn(tokenId);
+
+ // Now can register as country
+ vm.prank(alice);
+ fleet.registerFleetCountry(UUID_1, US, 0);
+ assertEq(uint8(fleet.uuidLevel(UUID_1)), 3);
+ }
+
+ // ══════════════════════════════════════════════
+ // Owned-Only Mode Tests
+ // ══════════════════════════════════════════════
+
+ function test_claimUuid_basic() public {
+ uint256 aliceBalanceBefore = bondToken.balanceOf(alice);
+
+ vm.prank(alice);
+ uint256 tokenId = fleet.claimUuid(UUID_1, address(0));
+
+ // Token minted
+ assertEq(fleet.ownerOf(tokenId), alice);
+ assertEq(fleet.tokenUuid(tokenId), UUID_1);
+ assertEq(fleet.tokenRegion(tokenId), 0); // OWNED_REGION_KEY
+
+ // UUID ownership set
+ assertEq(fleet.uuidOwner(UUID_1), alice);
+ assertEq(fleet.uuidTokenCount(UUID_1), 1);
+ assertTrue(fleet.isOwnedOnly(UUID_1));
+ assertEq(uint8(fleet.uuidLevel(UUID_1)), 1); // Owned
+
+ // Bond pulled
+ assertEq(aliceBalanceBefore - bondToken.balanceOf(alice), BASE_BOND);
+
+ // bonds() returns BASE_BOND for owned-only
+ assertEq(fleet.bonds(tokenId), BASE_BOND);
+ }
+
+ function test_RevertIf_claimUuid_alreadyOwned() public {
+ vm.prank(alice);
+ fleet.claimUuid(UUID_1, address(0));
+
+ vm.prank(bob);
+ vm.expectRevert(FleetIdentityUpgradeable.UuidAlreadyOwned.selector);
+ fleet.claimUuid(UUID_1, address(0));
+ }
+
+ function test_RevertIf_claimUuid_alreadyRegistered() public {
+ vm.prank(alice);
+ fleet.registerFleetLocal(UUID_1, US, ADMIN_CA, 0);
+
+ vm.prank(bob);
+ vm.expectRevert(FleetIdentityUpgradeable.UuidAlreadyOwned.selector);
+ fleet.claimUuid(UUID_1, address(0));
+ }
+
+ function test_RevertIf_claimUuid_invalidUuid() public {
+ vm.prank(alice);
+ vm.expectRevert(FleetIdentityUpgradeable.InvalidUUID.selector);
+ fleet.claimUuid(bytes16(0), address(0));
+ }
+
+ function test_registerFromOwned_local() public {
+ // First claim
+ vm.prank(alice);
+ uint256 ownedTokenId = fleet.claimUuid(UUID_1, address(0));
+
+ uint256 aliceBalanceBefore = bondToken.balanceOf(alice);
+
+ // Register from owned state - operator (alice) pays tierBond
+ vm.prank(alice);
+ uint256 tokenId = fleet.registerFleetLocal(UUID_1, US, ADMIN_CA, 0);
+
+ // Old owned token burned
+ vm.expectRevert();
+ fleet.ownerOf(ownedTokenId);
+
+ // New token exists
+ assertEq(fleet.ownerOf(tokenId), alice);
+ assertEq(fleet.tokenRegion(tokenId), _regionUSCA());
+ assertEq(fleet.fleetTier(tokenId), 0);
+
+ // UUID state updated
+ assertEq(fleet.uuidOwner(UUID_1), alice);
+ assertEq(fleet.uuidTokenCount(UUID_1), 1); // still 1
+ assertFalse(fleet.isOwnedOnly(UUID_1));
+ assertEq(uint8(fleet.uuidLevel(UUID_1)), 2); // Local
+
+ // Operator pays tierBond (owner already paid BASE_BOND via claim)
+ assertEq(aliceBalanceBefore - bondToken.balanceOf(alice), fleet.tierBond(0, false));
+ }
+
+ function test_registerFromOwned_country() public {
+ vm.prank(alice);
+ fleet.claimUuid(UUID_1, address(0));
+
+ uint256 aliceBalanceBefore = bondToken.balanceOf(alice);
+
+ vm.prank(alice);
+ uint256 tokenId = fleet.registerFleetCountry(UUID_1, US, 0);
+
+ assertEq(fleet.ownerOf(tokenId), alice);
+ assertEq(fleet.tokenRegion(tokenId), uint32(US));
+ assertEq(uint8(fleet.uuidLevel(UUID_1)), 3); // Country
+
+ // Operator pays tierBond for country tier 0 = 16*BASE_BOND
+ assertEq(aliceBalanceBefore - bondToken.balanceOf(alice), fleet.tierBond(0, true));
+ }
+
+ function test_registerFromOwned_higherTier() public {
+ vm.prank(alice);
+ fleet.claimUuid(UUID_1, address(0));
+
+ uint256 aliceBalanceBefore = bondToken.balanceOf(alice);
+
+ // Register at tier 0 local - operator pays tierBond(0, false) = BASE_BOND
+ vm.prank(alice);
+ uint256 tokenId = fleet.registerFleetLocal(UUID_1, US, ADMIN_CA, 0);
+ assertEq(aliceBalanceBefore - bondToken.balanceOf(alice), fleet.tierBond(0, false));
+
+ // Promote to tier 2: additional bond = tierBond(2) - tierBond(0) = 4*BASE_BOND - BASE_BOND = 3*BASE_BOND
+ uint256 balBeforePromote = bondToken.balanceOf(alice);
+ vm.prank(alice);
+ fleet.reassignTier(tokenId, 2);
+ assertEq(balBeforePromote - bondToken.balanceOf(alice), 3 * BASE_BOND);
+ }
+
+ function test_burn_lastRegisteredToken_transitionsToOwned() public {
+ vm.prank(alice);
+ uint256 tokenId = fleet.registerFleetLocal(UUID_1, US, ADMIN_CA, 0);
+
+ uint256 aliceBalanceBefore = bondToken.balanceOf(alice);
+
+ vm.prank(alice);
+ fleet.burn(tokenId);
+
+ // Old token burned
+ vm.expectRevert();
+ fleet.ownerOf(tokenId);
+
+ // New owned-only token exists
+ uint256 ownedTokenId = uint256(uint128(UUID_1));
+ assertEq(fleet.ownerOf(ownedTokenId), alice);
+ assertEq(fleet.tokenRegion(ownedTokenId), 0);
+
+ // UUID state updated to Owned
+ assertTrue(fleet.isOwnedOnly(UUID_1));
+ assertEq(uint8(fleet.uuidLevel(UUID_1)), 1); // Owned
+
+ // Operator (alice) gets tierBond refunded
+ assertEq(bondToken.balanceOf(alice) - aliceBalanceBefore, fleet.tierBond(0, false));
+ }
+
+ function test_burn_lastRegisteredToken_withHighTierRefund() public {
+ // Register at tier 0, then promote to tier 2
+ vm.prank(alice);
+ uint256 tokenId = fleet.registerFleetLocal(UUID_1, US, ADMIN_CA, 0);
+ vm.prank(alice);
+ fleet.reassignTier(tokenId, 2);
+
+ uint256 aliceBalanceBefore = bondToken.balanceOf(alice);
+
+ vm.prank(alice);
+ fleet.burn(tokenId);
+
+ // Operator (alice) gets full tierBond(2, false) refunded
+ assertEq(bondToken.balanceOf(alice) - aliceBalanceBefore, fleet.tierBond(2, false));
+
+ // Transitioned to owned-only
+ assertTrue(fleet.isOwnedOnly(UUID_1));
+ }
+
+ function test_burn_lastCountryToken_transitionsToOwned() public {
+ // Register country tier 0
+ vm.prank(alice);
+ uint256 tokenId = fleet.registerFleetCountry(UUID_1, US, 0);
+
+ uint256 aliceBalanceBefore = bondToken.balanceOf(alice);
+
+ vm.prank(alice);
+ fleet.burn(tokenId);
+
+ // Operator (alice) gets full tierBond(0, true) refunded
+ assertEq(bondToken.balanceOf(alice) - aliceBalanceBefore, fleet.tierBond(0, true));
+
+ // Level changed to Owned
+ assertEq(uint8(fleet.uuidLevel(UUID_1)), 1);
+ assertTrue(fleet.isOwnedOnly(UUID_1));
+ }
+
+ function test_burn_multiRegion_doesNotTransitionUntilLastToken() public {
+ // Register in two regions
+ vm.prank(alice);
+ uint256 id1 = fleet.registerFleetLocal(UUID_1, US, ADMIN_CA, 0);
+ vm.prank(alice);
+ uint256 id2 = fleet.registerFleetLocal(UUID_1, DE, ADMIN_CA, 0);
+
+ // Burn first token - should NOT transition to owned
+ vm.prank(alice);
+ fleet.burn(id1);
+
+ // Still registered level, not owned
+ assertFalse(fleet.isOwnedOnly(UUID_1));
+ assertEq(fleet.uuidTokenCount(UUID_1), 1);
+
+ // Second token still exists
+ assertEq(fleet.ownerOf(id2), alice);
+
+ // Burn second token - NOW should transition to owned
+ vm.prank(alice);
+ fleet.burn(id2);
+
+ assertTrue(fleet.isOwnedOnly(UUID_1));
+ uint256 ownedTokenId = uint256(uint128(UUID_1));
+ assertEq(fleet.ownerOf(ownedTokenId), alice);
+ }
+
+ function test_burn_ownedOnly_clearsUuid() public {
+ vm.prank(alice);
+ uint256 tokenId = fleet.claimUuid(UUID_1, address(0));
+
+ uint256 aliceBalanceBefore = bondToken.balanceOf(alice);
+
+ vm.prank(alice);
+ fleet.burn(tokenId);
+
+ // Token burned
+ vm.expectRevert();
+ fleet.ownerOf(tokenId);
+
+ // UUID cleared
+ assertEq(fleet.uuidOwner(UUID_1), address(0));
+ assertEq(fleet.uuidTokenCount(UUID_1), 0);
+ assertEq(uint8(fleet.uuidLevel(UUID_1)), 0); // None
+
+ // Refund received
+ assertEq(bondToken.balanceOf(alice) - aliceBalanceBefore, BASE_BOND);
+ }
+
+ function test_burn_ownedOnly_afterTransfer() public {
+ vm.prank(alice);
+ uint256 tokenId = fleet.claimUuid(UUID_1, address(0));
+
+ // Transfer to bob
+ vm.prank(alice);
+ fleet.transferFrom(alice, bob, tokenId);
+
+ // uuidOwner should have updated
+ assertEq(fleet.uuidOwner(UUID_1), bob);
+
+ // Alice cannot burn (not token owner)
+ vm.prank(alice);
+ vm.expectRevert(FleetIdentityUpgradeable.NotTokenOwner.selector);
+ fleet.burn(tokenId);
+
+ // Bob can burn
+ uint256 bobBalanceBefore = bondToken.balanceOf(bob);
+ vm.prank(bob);
+ fleet.burn(tokenId);
+ assertEq(bondToken.balanceOf(bob) - bobBalanceBefore, BASE_BOND);
+ }
+
+ function test_RevertIf_burn_ownedOnly_notOwner() public {
+ vm.prank(alice);
+ uint256 tokenId = fleet.claimUuid(UUID_1, address(0));
+
+ // Bob cannot burn owned-only token (not owner)
+ vm.prank(bob);
+ vm.expectRevert(FleetIdentityUpgradeable.NotTokenOwner.selector);
+ fleet.burn(tokenId);
+ }
+
+ function test_ownedOnly_transfer_updatesUuidOwner() public {
+ vm.prank(alice);
+ uint256 tokenId = fleet.claimUuid(UUID_1, address(0));
+
+ assertEq(fleet.uuidOwner(UUID_1), alice);
+
+ vm.prank(alice);
+ fleet.transferFrom(alice, bob, tokenId);
+
+ // uuidOwner updated on transfer for owned-only tokens
+ assertEq(fleet.uuidOwner(UUID_1), bob);
+ assertEq(fleet.ownerOf(tokenId), bob);
+ }
+
+ function test_ownedOnly_notInBundle() public {
+ // Claim some UUIDs as owned-only
+ vm.prank(alice);
+ fleet.claimUuid(UUID_1, address(0));
+ vm.prank(alice);
+ fleet.claimUuid(UUID_2, address(0));
+
+ // Bundle should be empty
+ (bytes16[] memory uuids, uint256 count) = fleet.buildHighestBondedUuidBundle(US, ADMIN_CA);
+ assertEq(count, 0);
+
+ // Now register one
+ vm.prank(alice);
+ fleet.registerFleetLocal(UUID_1, US, ADMIN_CA, 0);
+
+ // Bundle should contain only the registered one
+ (uuids, count) = fleet.buildHighestBondedUuidBundle(US, ADMIN_CA);
+ assertEq(count, 1);
+ assertEq(uuids[0], UUID_1);
+ }
+
+ function test_burn_ownedOnly() public {
+ vm.prank(alice);
+ uint256 tokenId = fleet.claimUuid(UUID_1, address(0));
+
+ uint256 aliceBalanceBefore = bondToken.balanceOf(alice);
+
+ vm.prank(alice);
+ fleet.burn(tokenId);
+
+ // Token burned
+ vm.expectRevert();
+ fleet.ownerOf(tokenId);
+
+ // UUID cleared
+ assertEq(fleet.uuidOwner(UUID_1), address(0));
+
+ // Refund received
+ assertEq(bondToken.balanceOf(alice) - aliceBalanceBefore, BASE_BOND);
+ }
+
+ function test_ownedOnly_canReRegisterAfterBurn() public {
+ vm.prank(alice);
+ uint256 tokenId = fleet.claimUuid(UUID_1, address(0));
+
+ vm.prank(alice);
+ fleet.burn(tokenId);
+
+ // Bob can now claim or register
+ vm.prank(bob);
+ uint256 newTokenId = fleet.registerFleetLocal(UUID_1, US, ADMIN_CA, 0);
+
+ assertEq(fleet.ownerOf(newTokenId), bob);
+ assertEq(fleet.uuidOwner(UUID_1), bob);
+ }
+
+ function test_migration_viaBurnAndReregister() public {
+ // This test shows the migration pattern using burn
+
+ // Register local in US
+ vm.prank(alice);
+ uint256 oldTokenId = fleet.registerFleetLocal(UUID_1, US, ADMIN_CA, 0);
+
+ uint256 aliceBalanceAfterRegister = bondToken.balanceOf(alice);
+
+ // Burn registered token -> transitions to owned-only, refunds tierBond(0, false)
+ vm.prank(alice);
+ fleet.burn(oldTokenId);
+
+ // Now in owned-only state, re-register in DE as country
+ // Pays tierBond(0, true) = 16*BASE_BOND for country registration
+ vm.prank(alice);
+ uint256 newTokenId = fleet.registerFleetCountry(UUID_1, DE, 0);
+
+ assertEq(fleet.ownerOf(newTokenId), alice);
+ assertEq(fleet.tokenRegion(newTokenId), uint32(DE));
+ assertEq(uint8(fleet.uuidLevel(UUID_1)), 3); // Country
+
+ // Net bond change: tierBond(0, true) - tierBond(0, false) = 16*BASE_BOND - BASE_BOND = 15*BASE_BOND
+ assertEq(aliceBalanceAfterRegister - bondToken.balanceOf(alice), 15 * BASE_BOND);
+ }
+
+ function testFuzz_tierBond_geometric(uint256 tier) public view {
+ tier = bound(tier, 0, 10);
+ uint256 expected = BASE_BOND;
+ for (uint256 i = 0; i < tier; i++) {
+ expected *= 2;
+ }
+ // Local regions get 1× multiplier
+ assertEq(fleet.tierBond(tier, false), expected);
+ // Country regions get 16x multiplier
+ assertEq(fleet.tierBond(tier, true), expected * fleet.countryBondMultiplier());
+ }
+
+ function testFuzz_perRegionTiers_newRegionAlwaysStartsAtTier0(uint16 cc) public {
+ cc = uint16(bound(cc, 1, 999));
+ vm.assume(cc != US); // Skip US since we fill it below
+
+ // Fill one country with 8 fleets
+ _registerNCountry(alice, US, 8, 0);
+ uint256 cap = fleet.TIER_CAPACITY();
+ uint256 expectedTiers = (8 + cap - 1) / cap; // ceiling division
+ assertEq(fleet.regionTierCount(_regionUS()), expectedTiers);
+
+ // New country should start at tier 0 regardless of other regions
+ vm.prank(bob);
+ uint256 tokenId = fleet.registerFleetCountry(_uuid(999), cc, 0);
+ assertEq(fleet.fleetTier(tokenId), 0);
+ assertEq(fleet.bonds(tokenId), BASE_BOND * fleet.countryBondMultiplier()); // Country gets 16x multiplier
+ }
+
+ function testFuzz_tierAssignment_autoFillsSequentiallyPerRegion(uint8 count) public {
+ count = uint8(bound(count, 1, 40));
+ uint256 cap = fleet.TIER_CAPACITY();
+
+ for (uint256 i = 0; i < count; i++) {
+ uint256 expectedTier = i / cap;
+ vm.prank(alice);
+ uint256 tokenId = fleet.registerFleetLocal(_uuid(i + 300), US, ADMIN_CA, expectedTier);
+
+ assertEq(fleet.fleetTier(tokenId), expectedTier);
+ }
+
+ uint256 expectedTiers = (uint256(count) + cap - 1) / cap;
+ assertEq(fleet.regionTierCount(_regionUSCA()), expectedTiers);
+ }
+
+ // --- Invariants ---
+
+ function test_invariant_contractBalanceEqualsSumOfBonds() public {
+ vm.prank(alice);
+ uint256 id1 = fleet.registerFleetCountry(UUID_1, US, 0);
+ vm.prank(bob);
+ uint256 id2 = fleet.registerFleetCountry(UUID_2, DE, 0);
+ vm.prank(carol);
+ uint256 id3 = fleet.registerFleetLocal(UUID_3, US, ADMIN_CA, 0);
+
+ // Contract balance = 3 * BASE_BOND (per UUID) + sum of tierBonds
+ uint256 expected = 3 * BASE_BOND + fleet.bonds(id1) + fleet.bonds(id2) + fleet.bonds(id3);
+ assertEq(bondToken.balanceOf(address(fleet)), expected);
+
+ // Burn id1 -> transitions to owned-only (BASE_BOND stays, tierBond refunded)
+ vm.prank(alice);
+ fleet.burn(id1);
+
+ // After burn: 2 registered UUIDs + 1 owned-only UUID
+ // = 2 * BASE_BOND (registered) + 2 * tierBond (registered) + BASE_BOND (owned-only)
+ uint256 expectedAfterBurn = 3 * BASE_BOND + fleet.bonds(id2) + fleet.bonds(id3);
+ assertEq(bondToken.balanceOf(address(fleet)), expectedAfterBurn);
+ }
+
+ function test_invariant_contractBalanceAfterReassignTierBurn() public {
+ vm.prank(alice);
+ uint256 id1 = fleet.registerFleetCountry(UUID_1, US, 0);
+ vm.prank(bob);
+ uint256 id2 = fleet.registerFleetLocal(UUID_2, US, ADMIN_CA, 0);
+ vm.prank(carol);
+ uint256 id3 = fleet.registerFleetLocal(UUID_3, DE, ADMIN_NY, 0);
+
+ vm.prank(alice);
+ fleet.reassignTier(id1, 3);
+
+ vm.prank(alice);
+ fleet.reassignTier(id1, 1);
+
+ // Contract balance = 3 * BASE_BOND + sum of tierBonds
+ uint256 expected = 3 * BASE_BOND + fleet.bonds(id1) + fleet.bonds(id2) + fleet.bonds(id3);
+ assertEq(bondToken.balanceOf(address(fleet)), expected);
+
+ // Burn all registered tokens (each transitions to owned-only)
+ vm.prank(alice);
+ fleet.burn(id1);
+ vm.prank(bob);
+ fleet.burn(id2);
+ vm.prank(carol);
+ fleet.burn(id3);
+
+ // Now have 3 owned-only tokens, each with BASE_BOND
+ assertEq(bondToken.balanceOf(address(fleet)), 3 * BASE_BOND);
+
+ // Burn all owned-only tokens
+ vm.prank(alice);
+ fleet.burn(uint256(uint128(UUID_1)));
+ vm.prank(bob);
+ fleet.burn(uint256(uint128(UUID_2)));
+ vm.prank(carol);
+ fleet.burn(uint256(uint128(UUID_3)));
+
+ assertEq(bondToken.balanceOf(address(fleet)), 0);
+ }
+
+ // --- countryInclusionHint ---
+
+ function test_countryInclusionHint_emptyReturnsZero() public view {
+ (uint256 tier, uint256 bond) = fleet.countryInclusionHint(US);
+ assertEq(tier, 0);
+ assertEq(bond, BASE_BOND * fleet.countryBondMultiplier()); // Country pays 16x multiplier
+ }
+
+ function test_countryInclusionHint_onlyCountryFleets() public {
+ _registerNCountryAt(alice, US, fleet.TIER_CAPACITY(), 1000, 0); // fills tier 0
+ vm.prank(bob);
+ fleet.registerFleetCountry(_uuid(9000), US, 1); // tier 1
+
+ // Tier 0 is full → cheapest inclusion = tier 1.
+ (uint256 tier, uint256 bond) = fleet.countryInclusionHint(US);
+ assertEq(tier, 1);
+ assertEq(bond, BASE_BOND * fleet.countryBondMultiplier() * 2); // Country pays 16x multiplier, tier 1 = 2× base
+ }
+
+ function test_countryInclusionHint_adminAreaCreatesPressure() public {
+ // Country US: tier 0 with 1 member
+ vm.prank(alice);
+ fleet.registerFleetCountry(_uuid(1000), US, 0);
+
+ // US-CA: push to tier 3 (1 member at tier 3)
+ vm.prank(bob);
+ fleet.registerFleetLocal(_uuid(2000), US, ADMIN_CA, 3);
+
+ // Country fleet needs to be included in bundle(US, ADMIN_CA).
+ // Simulation: cursor 3→0. At cursor 3: admin=1 (fits). At cursor 0: admin=0, country=1+1=2 (fits).
+ // Country tier 0 with 2 members: 2 <= 20-1 = 19. Fits.
+ // So cheapest = 0 (tier 0 has room: 1/4).
+ (uint256 tier,) = fleet.countryInclusionHint(US);
+ assertEq(tier, 0);
+ }
+
+ function test_countryInclusionHint_multipleAdminAreas_takesMax() public {
+ // US-CA: fill admin tier 0 + fill country tier 0
+ _registerNLocalAt(alice, US, ADMIN_CA, fleet.TIER_CAPACITY(), 0, 0);
+ _registerNCountryAt(alice, US, fleet.TIER_CAPACITY(), 100, 0);
+ // US-NY: light (3 admin)
+ _registerNLocal(alice, US, ADMIN_NY, 3, 200);
+
+ // Country tier 0 is full (TIER_CAPACITY members).
+ // Even though the bundle has room, the tier capacity is exhausted.
+ // So cheapest inclusion tier for a country fleet = 1.
+ (uint256 tier,) = fleet.countryInclusionHint(US);
+ assertEq(tier, 1);
+ }
+
+ function test_countryInclusionHint_ignoresOtherCountries() public {
+ // DE admin area at tier 5 — should NOT affect US hint
+ vm.prank(alice);
+ fleet.registerFleetLocal(_uuid(1000), DE, 1, 5);
+
+ // US-CA at tier 1
+ vm.prank(bob);
+ fleet.registerFleetLocal(_uuid(2000), US, ADMIN_CA, 1);
+
+ (uint256 usTier,) = fleet.countryInclusionHint(US);
+ // US country fleet needs inclusion in bundle(US, ADMIN_CA).
+ // Admin has 1 at tier 1. Country at tier 0: +1=1, fits.
+ assertEq(usTier, 0);
+ }
+
+ function test_countryInclusionHint_afterBurn_updates() public {
+ vm.prank(alice);
+ uint256 id = fleet.registerFleetLocal(_uuid(1000), US, ADMIN_CA, 3);
+
+ vm.prank(alice);
+ fleet.burn(id);
+
+ (uint256 after_,) = fleet.countryInclusionHint(US);
+ assertEq(after_, 0);
+ }
+
+ function test_countryInclusionHint_registrantCanActOnHint() public {
+ // Fill up to create pressure
+ _registerNLocal(alice, US, ADMIN_CA, 8, 0);
+ _registerNCountry(alice, US, 8, 100);
+
+ (uint256 inclusionTier, uint256 hintBond) = fleet.countryInclusionHint(US);
+
+ // Bob registers at country level at the hinted tier
+ vm.prank(bob);
+ fleet.registerFleetCountry(_uuid(2000), US, inclusionTier);
+
+ uint256 tokenId = _tokenId(_uuid(2000), _regionUS());
+ assertEq(fleet.fleetTier(tokenId), inclusionTier);
+ assertEq(fleet.bonds(tokenId), hintBond);
+
+ // Bundle for US-CA includes Bob's fleet
+ (bytes16[] memory uuids, uint256 count) = fleet.buildHighestBondedUuidBundle(US, ADMIN_CA);
+ assertGt(count, 0);
+ bool foundCountry;
+ for (uint256 i = 0; i < count; i++) {
+ if (uuids[i] == _uuid(2000)) foundCountry = true;
+ }
+ assertTrue(foundCountry, "Country fleet should appear in bundle");
+ }
+
+ // --- buildHighestBondedUuidBundle (shared-cursor fair-stop) ---
+
+ // ── Empty / Single-level basics ──
+
+ function test_buildBundle_emptyReturnsZero() public view {
+ (, uint256 count) = fleet.buildHighestBondedUuidBundle(US, ADMIN_CA);
+ assertEq(count, 0);
+ }
+
+ function test_RevertIf_buildBundle_adminCodeZero() public {
+ vm.prank(alice);
+ fleet.registerFleetCountry(UUID_1, US, 0);
+
+ vm.expectRevert(FleetIdentityUpgradeable.AdminAreaRequired.selector);
+ fleet.buildHighestBondedUuidBundle(US, 0);
+ }
+
+ function test_buildBundle_singleCountry() public {
+ vm.prank(alice);
+ fleet.registerFleetCountry(UUID_1, US, 0);
+
+ (bytes16[] memory uuids, uint256 count) = fleet.buildHighestBondedUuidBundle(US, ADMIN_CA);
+ assertEq(count, 1);
+ assertEq(uuids[0], UUID_1);
+ }
+
+ function test_buildBundle_singleLocal() public {
+ vm.prank(alice);
+ fleet.registerFleetLocal(UUID_1, US, ADMIN_CA, 0);
+
+ (bytes16[] memory uuids, uint256 count) = fleet.buildHighestBondedUuidBundle(US, ADMIN_CA);
+ assertEq(count, 1);
+ assertEq(uuids[0], UUID_1);
+ }
+
+ // ── Same cursor, both levels at tier 0 ──
+
+ function test_buildBundle_bothLevelsTied_levelPriorityOrder() public {
+ // Both at tier 0 → shared cursor 0 → level priority: local, country
+ vm.prank(alice);
+ fleet.registerFleetLocal(UUID_2, US, ADMIN_CA, 0);
+ vm.prank(alice);
+ fleet.registerFleetCountry(UUID_1, US, 0);
+
+ (bytes16[] memory uuids, uint256 count) = fleet.buildHighestBondedUuidBundle(US, ADMIN_CA);
+ assertEq(count, 2);
+ assertEq(uuids[0], UUID_2); // local first
+ assertEq(uuids[1], UUID_1); // country second
+ }
+
+ function test_buildBundle_2LevelsTier0_fullCapacity() public {
+ // 4 local + 4 country at tier 0 = 8
+ // Bundle fits all since max is 20
+ _registerNLocalAt(alice, US, ADMIN_CA, 4, 1000, 0);
+ _registerNCountryAt(alice, US, 4, 2000, 0);
+
+ (, uint256 count) = fleet.buildHighestBondedUuidBundle(US, ADMIN_CA);
+ assertEq(count, 8);
+ }
+
+ function test_buildBundle_2LevelsTier0_partialFill() public {
+ // 3 local + 2 country = 5
+ _registerNLocalAt(alice, US, ADMIN_CA, 3, 1000, 0);
+ _registerNCountryAt(alice, US, 2, 2000, 0);
+
+ (, uint256 count) = fleet.buildHighestBondedUuidBundle(US, ADMIN_CA);
+ assertEq(count, 5);
+ }
+
+ // ── Bond priority: higher tier index = higher bond = comes first ──
+
+ function test_buildBundle_higherBondFirst() public {
+ // Country: promote to tier 2 (bond=8*4*BASE)
+ vm.prank(alice);
+ uint256 usId = fleet.registerFleetCountry(UUID_1, US, 0);
+ vm.prank(alice);
+ fleet.reassignTier(usId, 2);
+ // Local: tier 0 (bond=BASE)
+ vm.prank(alice);
+ fleet.registerFleetLocal(UUID_2, US, ADMIN_CA, 0);
+
+ (bytes16[] memory uuids, uint256 count) = fleet.buildHighestBondedUuidBundle(US, ADMIN_CA);
+ assertEq(count, 2);
+ assertEq(uuids[0], UUID_1); // highest bond first (country tier 2)
+ assertEq(uuids[1], UUID_2); // local tier 0
+ }
+
+ function test_buildBundle_multiTierDescendingBond() public {
+ // Local tier 2 (bond=4*BASE)
+ vm.prank(alice);
+ uint256 id1 = fleet.registerFleetLocal(UUID_1, US, ADMIN_CA, 0);
+ vm.prank(alice);
+ fleet.reassignTier(id1, 2);
+
+ // Country tier 1 (bond=8*2*BASE)
+ vm.prank(alice);
+ uint256 id2 = fleet.registerFleetCountry(UUID_2, US, 0);
+ vm.prank(alice);
+ fleet.reassignTier(id2, 1);
+
+ // Local tier 0 (bond=BASE)
+ vm.prank(alice);
+ fleet.registerFleetLocal(UUID_3, US, ADMIN_CA, 0);
+
+ (bytes16[] memory uuids, uint256 count) = fleet.buildHighestBondedUuidBundle(US, ADMIN_CA);
+ assertEq(count, 3);
+ assertEq(uuids[0], UUID_1); // local tier 2: bond=4*BASE
+ assertEq(uuids[1], UUID_2); // country tier 1: bond=16*BASE (but added after local at cursor)
+ }
+
+ function test_buildBundle_multiTierMultiLevel_correctOrder() public {
+ // Admin: tier 0 (4 members) + tier 1 (1 member)
+ _registerNLocalAt(alice, US, ADMIN_CA, 4, 8000, 0);
+ vm.prank(alice);
+ fleet.registerFleetLocal(_uuid(8100), US, ADMIN_CA, 1);
+
+ // Country: promote to tier 1 (bond=8*2*BASE)
+ vm.prank(alice);
+ uint256 countryId = fleet.registerFleetCountry(_uuid(8200), US, 0);
+ vm.prank(alice);
+ fleet.reassignTier(countryId, 1);
+
+ // Country: promote to tier 2 (bond=8*4*BASE)
+ vm.prank(alice);
+ uint256 country2Id = fleet.registerFleetCountry(_uuid(8300), US, 0);
+ vm.prank(alice);
+ fleet.reassignTier(country2Id, 2);
+
+ (bytes16[] memory uuids, uint256 count) = fleet.buildHighestBondedUuidBundle(US, ADMIN_CA);
+ // Cursor=2: country(1)→include. Count=1.
+ // Cursor=1: local(1)+country(1)→include. Count=3.
+ // Cursor=0: local(4)→include. Count=7.
+ assertEq(count, 7);
+ assertEq(uuids[0], fleet.tokenUuid(country2Id)); // tier 2 first
+ }
+
+ // ── All-or-nothing ──
+
+ function test_buildBundle_allOrNothing_tierSkippedWhenDoesNotFit() public {
+ // Fill room so that at a cursor position a tier can't fit.
+ // Admin tier 1: 4 members
+ for (uint256 i = 0; i < 4; i++) {
+ vm.prank(alice);
+ fleet.registerFleetLocal(_uuid(5100 + i), US, ADMIN_CA, 1);
+ }
+ // Country tier 1: 4 members
+ for (uint256 i = 0; i < 4; i++) {
+ vm.prank(alice);
+ fleet.registerFleetCountry(_uuid(6100 + i), US, 1);
+ }
+
+ // Tier 0: local(4), country(3)
+ _registerNLocalAt(alice, US, ADMIN_CA, 4, 5000, 0);
+ _registerNCountryAt(alice, US, 3, 6000, 0);
+
+ (, uint256 count) = fleet.buildHighestBondedUuidBundle(US, ADMIN_CA);
+ // Cursor=1: local(4)+country(4)=8. Count=8, room=12.
+ // Cursor=0: local(4)≤12→include[count=12,room=8]. country(3)≤8→include[count=15,room=5].
+ assertEq(count, 15);
+ }
+
+ function test_buildBundle_allOrNothing_noPartialCollection() public {
+ // Room=3, tier has 5 members → some members skipped.
+ // Local tier 1: 4 members
+ for (uint256 i = 0; i < 4; i++) {
+ vm.prank(alice);
+ fleet.registerFleetLocal(_uuid(2000 + i), US, ADMIN_CA, 1);
+ }
+ // Country tier 1: 4 members
+ for (uint256 i = 0; i < 4; i++) {
+ vm.prank(alice);
+ fleet.registerFleetCountry(_uuid(3000 + i), US, 1);
+ }
+
+ (, uint256 count) = fleet.buildHighestBondedUuidBundle(US, ADMIN_CA);
+ // Cursor=1: local(4)+country(4)=8. Count=8.
+ // Cursor=0: all empty at tier 0. Done.
+ assertEq(count, 8);
+ }
+
+ function test_buildBundle_partialInclusion_fillsRemainingSlots() public {
+ uint256 cap = fleet.TIER_CAPACITY();
+ // With partial inclusion: bundle fills remaining slots.
+ // Country tier 0: cap members
+ _registerNCountryAt(alice, US, cap, 0, 0);
+
+ // Local: cap at tier 0 + cap at tier 1
+ _registerNLocalAt(alice, US, ADMIN_CA, cap, 5000, 0);
+ for (uint256 i = 0; i < cap; i++) {
+ vm.prank(alice);
+ fleet.registerFleetLocal(_uuid(5100 + i), US, ADMIN_CA, 1);
+ }
+
+ (bytes16[] memory uuids, uint256 count) = fleet.buildHighestBondedUuidBundle(US, ADMIN_CA);
+ // Tier 1: local=cap. Tier 0: local=cap + country=cap.
+ // Total = 3*cap, capped at MAX_BONDED_UUID_BUNDLE_SIZE.
+ uint256 total = 3 * cap;
+ uint256 expectedCount = total > fleet.MAX_BONDED_UUID_BUNDLE_SIZE() ? fleet.MAX_BONDED_UUID_BUNDLE_SIZE() : total;
+ assertEq(count, expectedCount);
+
+ // Verify country UUIDs ARE in the result (if bundle has room)
+ uint256 countryCount;
+ for (uint256 i = 0; i < count; i++) {
+ uint256 tokenId = _findTokenId(uuids[i], US, ADMIN_CA);
+ uint32 region = fleet.tokenRegion(tokenId);
+ if (region == _regionUS()) countryCount++;
+ }
+ // With cap=10, bundle=20: tier 1 local (10) + tier 0 local (10) = 20, no room for country
+ // With cap=4, bundle=20: tier 1 local (4) + tier 0 local (4) + country (4) = 12
+ uint256 localSlots = 2 * cap; // tier 0 and tier 1 locals
+ uint256 remainingRoom = fleet.MAX_BONDED_UUID_BUNDLE_SIZE() > localSlots ?
+ fleet.MAX_BONDED_UUID_BUNDLE_SIZE() - localSlots : 0;
+ uint256 expectedCountry = remainingRoom > cap ? cap : remainingRoom;
+ assertEq(countryCount, expectedCountry, "country members included based on remaining room");
+ }
+
+ // ── Partial inclusion (replaces all-or-nothing + fair-stop) ──
+
+ function test_buildBundle_partialInclusion_fillsBundleCompletely() public {
+ // With partial inclusion, we fill the bundle completely by including
+ // as many members as fit, in array order.
+
+ // Consume 6 slots at tier 1.
+ for (uint256 i = 0; i < 3; i++) {
+ vm.prank(alice);
+ fleet.registerFleetLocal(_uuid(1000 + i), US, ADMIN_CA, 1);
+ }
+ for (uint256 i = 0; i < 3; i++) {
+ vm.prank(alice);
+ fleet.registerFleetCountry(_uuid(2000 + i), US, 1);
+ }
+
+ // Tier 0: full capacities (TIER_CAPACITY = 4).
+ _registerNLocalAt(alice, US, ADMIN_CA, 4, 3000, 0);
+ _registerNCountryAt(alice, US, 4, 4000, 0);
+
+ (, uint256 count) = fleet.buildHighestBondedUuidBundle(US, ADMIN_CA);
+ // Cursor=1: local(3)+country(3)=6. Count=6, room=14.
+ // Cursor=0: local(4)≤14→include 4[count=10,room=10].
+ // country(4)≤10→include 4[count=14,room=6].
+ assertEq(count, 14);
+ }
+
+ function test_buildBundle_partialFill_localAndCountry() public {
+ // Two local tiers consume 8 slots, leaving 12 for cursor=0.
+ // At cursor=0: local(4) fits. country(4) included.
+
+ for (uint256 i = 0; i < 4; i++) {
+ vm.prank(alice);
+ fleet.registerFleetLocal(_uuid(1000 + i), US, ADMIN_CA, 1);
+ }
+ for (uint256 i = 0; i < 4; i++) {
+ vm.prank(alice);
+ fleet.registerFleetLocal(_uuid(2000 + i), US, ADMIN_CA, 2);
+ }
+
+ // Tier 0: 4 local + 4 country (TIER_CAPACITY = 4)
+ _registerNLocalAt(alice, US, ADMIN_CA, 4, 3000, 0);
+ _registerNCountryAt(alice, US, 4, 4000, 0);
+
+ (, uint256 count) = fleet.buildHighestBondedUuidBundle(US, ADMIN_CA);
+ // Cursor=2: local(4)→include. Count=4.
+ // Cursor=1: local(4)→include. Count=8, room=12.
+ // Cursor=0: local(4)≤12→include[count=12,room=8]. country(4)≤8→include[count=16,room=4].
+ assertEq(count, 16);
+ }
+
+ function test_buildBundle_partialInclusion_allLevelsPartiallyIncluded() public {
+ // With partial inclusion, both levels get included partially if needed.
+
+ // Consume 8 slots at tier 1.
+ for (uint256 i = 0; i < 4; i++) {
+ vm.prank(alice);
+ fleet.registerFleetLocal(_uuid(1000 + i), US, ADMIN_CA, 1);
+ }
+ for (uint256 i = 0; i < 4; i++) {
+ vm.prank(alice);
+ fleet.registerFleetCountry(_uuid(2000 + i), US, 1);
+ }
+
+ // Tier 0: local=4, country=4 (TIER_CAPACITY = 4)
+ _registerNLocalAt(alice, US, ADMIN_CA, 4, 3000, 0);
+ _registerNCountryAt(alice, US, 4, 4000, 0);
+
+ (bytes16[] memory uuids, uint256 count) = fleet.buildHighestBondedUuidBundle(US, ADMIN_CA);
+ // Cursor=1: local(4)+country(4)=8. Count=8, room=12.
+ // Cursor=0: local(4)≤12→include 4[count=12,room=8].
+ // country(4)≤8→include 4[count=16].
+ assertEq(count, 16);
+
+ // Verify local tier 0 is present
+ bool foundLocal = false;
+ for (uint256 i = 0; i < count; i++) {
+ if (uuids[i] == _uuid(3000)) foundLocal = true;
+ }
+ assertTrue(foundLocal, "local tier 0 should be included");
+
+ // Count how many country tier 0 members are included
+ uint256 countryT0Count;
+ for (uint256 i = 0; i < count; i++) {
+ uint256 tokenId = _findTokenId(uuids[i], US, ADMIN_CA);
+ if (fleet.tokenRegion(tokenId) == _regionUS() && fleet.fleetTier(tokenId) == 0) countryT0Count++;
+ }
+ assertEq(countryT0Count, 4, "4 country tier 0 members included");
+ }
+
+ function test_buildBundle_doesNotDescendAfterBundleFull() public {
+ // When cursor=1 fills bundle, cursor=0 tiers are NOT included.
+
+ // Tier 1: local(4) + country(4) + more local(4) + more country(4) = 16
+ for (uint256 i = 0; i < 4; i++) {
+ vm.prank(alice);
+ fleet.registerFleetLocal(_uuid(1000 + i), US, ADMIN_CA, 1);
+ }
+ for (uint256 i = 0; i < 4; i++) {
+ vm.prank(alice);
+ fleet.registerFleetCountry(_uuid(2000 + i), US, 1);
+ }
+ for (uint256 i = 0; i < 4; i++) {
+ vm.prank(alice);
+ fleet.registerFleetLocal(_uuid(3000 + i), US, ADMIN_CA, 2);
+ }
+
+ // Tier 0: extras that might not all fit
+ _registerNLocalAt(alice, US, ADMIN_CA, 4, 4000, 0);
+ _registerNCountryAt(alice, US, 4, 5000, 0);
+
+ (, uint256 count) = fleet.buildHighestBondedUuidBundle(US, ADMIN_CA);
+ // Cursor=1: admin(8)+country(8)+global(4)=20. Bundle full.
+ assertEq(count, 20);
+ }
+
+ function test_buildBundle_partialInclusion_fillsAtHighTier() public {
+ // With TIER_CAPACITY = 4:
+ // Cursor=2: local(3)→include. Count=3.
+ // Cursor=1: local(4)+country(4)=8→include. Count=11, room=9.
+ // Cursor=0: local(1)≤9→include[count=12,room=8]. country(1)≤8→include[count=13,room=7].
+
+ for (uint256 i = 0; i < 3; i++) {
+ vm.prank(alice);
+ fleet.registerFleetLocal(_uuid(1000 + i), US, ADMIN_CA, 2);
+ }
+ for (uint256 i = 0; i < 4; i++) {
+ vm.prank(alice);
+ fleet.registerFleetLocal(_uuid(2000 + i), US, ADMIN_CA, 1);
+ }
+ for (uint256 i = 0; i < 4; i++) {
+ vm.prank(alice);
+ fleet.registerFleetCountry(_uuid(3000 + i), US, 1);
+ }
+
+ // Tier 0 extras (would be included with more room):
+ vm.prank(alice);
+ fleet.registerFleetLocal(_uuid(5000), US, ADMIN_CA, 0);
+ vm.prank(alice);
+ fleet.registerFleetCountry(_uuid(5001), US, 0);
+
+ (bytes16[] memory uuids, uint256 count) = fleet.buildHighestBondedUuidBundle(US, ADMIN_CA);
+ // Cursor=2: local(3)→include. Count=3, room=17.
+ // Cursor=1: local(4)+country(4)→include. Count=11, room=9.
+ // Cursor=0: local(1)≤9→include[count=12,room=8]. country(1)≤8→include[count=13,room=7].
+ assertEq(count, 13);
+ }
+
+ function test_buildBundle_partialInclusion_higherPriorityFirst() public {
+ // Partial inclusion fills higher-priority levels first at each tier.
+ // Local gets slots before country.
+
+ // Local tier 1: 4, Country tier 1: 4
+ for (uint256 i = 0; i < 4; i++) {
+ vm.prank(alice);
+ fleet.registerFleetLocal(_uuid(1000 + i), US, ADMIN_CA, 1);
+ }
+ for (uint256 i = 0; i < 4; i++) {
+ vm.prank(alice);
+ fleet.registerFleetCountry(_uuid(2000 + i), US, 1);
+ }
+
+ // Tier 0: local=4, country=4 (TIER_CAPACITY = 4)
+ _registerNLocalAt(alice, US, ADMIN_CA, 4, 3000, 0);
+ _registerNCountryAt(alice, US, 4, 4000, 0);
+
+ (bytes16[] memory uuids, uint256 count) = fleet.buildHighestBondedUuidBundle(US, ADMIN_CA);
+ // Cursor=1: local(4)+country(4)=8. Count=8, room=12.
+ // Cursor=0: local(4)≤12→include 4[count=12,room=8]. country(4)≤8→include 4[count=16].
+ assertEq(count, 16);
+
+ // Verify local tier 0 full inclusion (4 of 4)
+ uint256 localT0Count;
+ for (uint256 i = 0; i < count; i++) {
+ uint256 tokenId = _findTokenId(uuids[i], US, ADMIN_CA);
+ if (fleet.tokenRegion(tokenId) == _regionUSCA() && fleet.fleetTier(tokenId) == 0) localT0Count++;
+ }
+ assertEq(localT0Count, 4, "4 local tier 0 included");
+ }
+
+ // ── Tie-breaker: local before country at same cursor ──
+
+ function test_buildBundle_tieBreaker_localBeforeCountry() public {
+ // Room=8 after higher tiers. Local tier 0 (4) tried before country tier 0 (4).
+ // Local fits (4), then country (4).
+
+ // Eat 12 room at tier 1 and 2.
+ for (uint256 i = 0; i < 4; i++) {
+ vm.prank(alice);
+ fleet.registerFleetLocal(_uuid(1000 + i), US, ADMIN_CA, 1);
+ }
+ for (uint256 i = 0; i < 4; i++) {
+ vm.prank(alice);
+ fleet.registerFleetCountry(_uuid(2000 + i), US, 1);
+ }
+ for (uint256 i = 0; i < 4; i++) {
+ vm.prank(alice);
+ fleet.registerFleetLocal(_uuid(3000 + i), US, ADMIN_CA, 2);
+ }
+
+ // Tier 0: local=4, country=4 (TIER_CAPACITY = 4)
+ _registerNLocalAt(alice, US, ADMIN_CA, 4, 4000, 0);
+ _registerNCountryAt(alice, US, 4, 5000, 0);
+
+ (bytes16[] memory uuids, uint256 count) = fleet.buildHighestBondedUuidBundle(US, ADMIN_CA);
+ // Cursor=2: local(4)→include. Count=4, room=16.
+ // Cursor=1: local(4)+country(4)=8→include. Count=12, room=8.
+ // Cursor=0: local(4)≤8→include[count=16,room=4]. country(4)≤4→include 4[count=20,room=0].
+ assertEq(count, 20);
+
+ // Verify: local(12) + country(8)
+ uint256 localCount;
+ uint256 countryCount;
+ for (uint256 i = 0; i < count; i++) {
+ uint256 tokenId = _findTokenId(uuids[i], US, ADMIN_CA);
+ uint32 region = fleet.tokenRegion(tokenId);
+ if (region == _regionUS()) countryCount++;
+ else if (region == _regionUSCA()) localCount++;
+ }
+ assertEq(localCount, 12); // tier 0 (4) + tier 1 (4) + tier 2 (4)
+ assertEq(countryCount, 8); // tier 1 (4) + tier 0 (4)
+ }
+
+ // ── Empty tiers and gaps ──
+
+ function test_buildBundle_emptyTiersSkippedCleanly() public {
+ // Register at tier 0 then promote to tier 2, leaving tier 1 empty.
+ vm.prank(alice);
+ uint256 id = fleet.registerFleetLocal(UUID_1, US, ADMIN_CA, 0);
+ vm.prank(alice);
+ fleet.reassignTier(id, 2);
+
+ vm.prank(alice);
+ fleet.registerFleetCountry(UUID_2, US, 0);
+
+ (bytes16[] memory uuids, uint256 count) = fleet.buildHighestBondedUuidBundle(US, ADMIN_CA);
+ // Cursor=2: local(1)→include. Count=1.
+ // Cursor=1: all empty. No skip. Descend.
+ // Cursor=0: country(1)→include. Count=2.
+ assertEq(count, 2);
+ assertEq(uuids[0], UUID_1);
+ assertEq(uuids[1], UUID_2);
+ }
+
+ function test_buildBundle_multipleEmptyTiersInMiddle() public {
+ // Local at tier 5, country at tier 0. Tiers 1-4 empty.
+ vm.prank(alice);
+ uint256 id = fleet.registerFleetLocal(UUID_1, US, ADMIN_CA, 0);
+ vm.prank(alice);
+ fleet.reassignTier(id, 5);
+ vm.prank(alice);
+ fleet.registerFleetCountry(UUID_2, US, 0);
+
+ (, uint256 count) = fleet.buildHighestBondedUuidBundle(US, ADMIN_CA);
+ assertEq(count, 2);
+ }
+
+ function test_buildBundle_emptyTiersInMiddle_countryToo() public {
+ // Country: register at tier 0 and tier 2 (tier 1 empty)
+ vm.prank(alice);
+ fleet.registerFleetCountry(UUID_1, US, 0);
+ vm.prank(alice);
+ fleet.registerFleetCountry(UUID_2, US, 2);
+
+ (bytes16[] memory uuids, uint256 count) = fleet.buildHighestBondedUuidBundle(US, ADMIN_CA);
+ assertEq(count, 2);
+ assertEq(uuids[0], UUID_2); // higher bond first
+ assertEq(uuids[1], UUID_1);
+ }
+
+ // ── Local isolation ──
+
+ function test_buildBundle_multipleAdminAreas_isolated() public {
+ _registerNLocalAt(alice, US, ADMIN_CA, 4, 1000, 0);
+ _registerNLocalAt(alice, US, ADMIN_NY, 4, 2000, 0);
+
+ (, uint256 countCA) = fleet.buildHighestBondedUuidBundle(US, ADMIN_CA);
+ // CA locals + any country
+ assertEq(countCA, 4);
+ (, uint256 countNY) = fleet.buildHighestBondedUuidBundle(US, ADMIN_NY);
+ // NY locals + any country (same country)
+ assertEq(countNY, 4);
+ }
+
+ // ── Single level, multiple tiers ──
+
+ function test_buildBundle_singleLevelMultipleTiers() public {
+ // Only country, multiple tiers. Country fleets fill all available slots.
+ _registerNCountryAt(alice, US, 4, 1000, 0); // tier 0: 4 members
+ _registerNCountryAt(alice, US, 4, 2000, 1); // tier 1: 4 members
+ _registerNCountryAt(alice, US, 4, 3000, 2); // tier 2: 4 members
+
+ (bytes16[] memory uuids, uint256 count) = fleet.buildHighestBondedUuidBundle(US, ADMIN_CA);
+ assertEq(count, 12); // all country fleets included
+ // Verify order: tier 2 first (highest bond)
+ uint256[] memory t2 = fleet.getTierMembers(_regionUS(), 2);
+ for (uint256 i = 0; i < 4; i++) {
+ assertEq(uuids[i], bytes16(uint128(t2[i])));
+ }
+ }
+
+ function test_buildBundle_singleLevelOnlyLocal() public {
+ _registerNLocalAt(alice, US, ADMIN_CA, 4, 1000, 0);
+ (, uint256 count) = fleet.buildHighestBondedUuidBundle(US, ADMIN_CA);
+ assertEq(count, 4);
+ }
+
+ function test_buildBundle_onlyCountry() public {
+ // TIER_CAPACITY = 4, so split across two tiers
+ _registerNCountryAt(alice, US, 4, 1000, 0);
+ _registerNCountryAt(alice, US, 4, 1100, 1);
+
+ (bytes16[] memory uuids, uint256 count) = fleet.buildHighestBondedUuidBundle(US, ADMIN_CA);
+ assertEq(count, 8);
+ assertEq(uuids[0], _uuid(1100)); // tier 1 comes first (higher bond)
+ }
+
+ function test_buildBundle_countryFillsSlots() public {
+ // Test that country fleets fill bundle slots when room is available.
+ //
+ // Setup: 2 local fleets + 12 country fleets across 3 tiers
+ // Expected: All 14 should be included since bundle has room
+ _registerNLocalAt(alice, US, ADMIN_CA, 2, 1000, 0);
+ _registerNCountryAt(alice, US, 4, 2000, 0); // tier 0: 4 country
+ _registerNCountryAt(alice, US, 4, 3000, 1); // tier 1: 4 country
+ _registerNCountryAt(alice, US, 4, 4000, 2); // tier 2: 4 country
+
+ (bytes16[] memory uuids, uint256 count) = fleet.buildHighestBondedUuidBundle(US, ADMIN_CA);
+
+ // All 14 should be included: 2 local + 12 country
+ assertEq(count, 14);
+
+ // Verify order: tier 2 country (highest bond) → tier 1 country → tier 0 local/country
+ // First 4 should be tier 2 country fleets
+ for (uint256 i = 0; i < 4; i++) {
+ assertEq(uuids[i], _uuid(4000 + i));
+ }
+ }
+
+ function test_buildBundle_localsPriorityWithinTier() public {
+ // When locals and country compete at same tier, locals are included first.
+ //
+ // Setup: 8 local fleets + 12 country fleets
+ _registerNLocalAt(alice, US, ADMIN_CA, 4, 1000, 0);
+ _registerNLocalAt(alice, US, ADMIN_CA, 4, 1100, 1);
+ _registerNCountryAt(alice, US, 4, 2000, 0);
+ _registerNCountryAt(alice, US, 4, 3000, 1);
+ _registerNCountryAt(alice, US, 4, 4000, 2);
+
+ (, uint256 count) = fleet.buildHighestBondedUuidBundle(US, ADMIN_CA);
+
+ // Total: 8 local + 12 country = 20 (bundle max)
+ assertEq(count, 20);
+ }
+
+ // ── Shared cursor: different max tier indices per level ──
+
+ function test_buildBundle_sharedCursor_levelsAtDifferentMaxTiers() public {
+ // Local at tier 3, Country at tier 1.
+ vm.prank(alice);
+ uint256 id1 = fleet.registerFleetLocal(UUID_1, US, ADMIN_CA, 0);
+ vm.prank(alice);
+ fleet.reassignTier(id1, 3);
+ vm.prank(alice);
+ uint256 id2 = fleet.registerFleetCountry(UUID_2, US, 0);
+ vm.prank(alice);
+ fleet.reassignTier(id2, 1);
+ vm.prank(alice);
+ fleet.registerFleetLocal(UUID_3, US, ADMIN_CA, 0);
+
+ (bytes16[] memory uuids, uint256 count) = fleet.buildHighestBondedUuidBundle(US, ADMIN_CA);
+ assertEq(count, 3);
+ assertEq(uuids[0], UUID_1); // tier 3
+ assertEq(uuids[1], UUID_2); // tier 1
+ assertEq(uuids[2], UUID_3); // tier 0
+ }
+
+ function test_buildBundle_sharedCursor_sameTierIndex_differentBondByRegion() public view {
+ // Local tier 0 = BASE_BOND, Country tier 0 = BASE_BOND * fleet.countryBondMultiplier() (multiplier)
+ assertEq(fleet.tierBond(0, false), BASE_BOND);
+ assertEq(fleet.tierBond(0, true), BASE_BOND * fleet.countryBondMultiplier());
+ assertEq(fleet.tierBond(1, false), BASE_BOND * 2);
+ assertEq(fleet.tierBond(1, true), BASE_BOND * fleet.countryBondMultiplier() * 2);
+ }
+
+ // ── Lifecycle ──
+
+ function test_buildBundle_afterBurn_reflects() public {
+ vm.prank(alice);
+ uint256 id1 = fleet.registerFleetLocal(UUID_1, US, ADMIN_CA, 0);
+ vm.prank(bob);
+ fleet.registerFleetLocal(UUID_2, US, ADMIN_CA, 0);
+ vm.prank(carol);
+ fleet.registerFleetLocal(UUID_3, US, ADMIN_CA, 0);
+
+ (, uint256 countBefore) = fleet.buildHighestBondedUuidBundle(US, ADMIN_CA);
+ assertEq(countBefore, 3);
+
+ vm.prank(alice);
+ fleet.burn(id1);
+
+ (, uint256 countAfter) = fleet.buildHighestBondedUuidBundle(US, ADMIN_CA);
+ assertEq(countAfter, 2);
+ }
+
+ function test_buildBundle_exhaustsBothLevels() public {
+ vm.prank(alice);
+ fleet.registerFleetCountry(UUID_1, US, 0);
+ vm.prank(alice);
+ fleet.registerFleetLocal(UUID_2, US, ADMIN_CA, 0);
+
+ (bytes16[] memory uuids, uint256 count) = fleet.buildHighestBondedUuidBundle(US, ADMIN_CA);
+ assertEq(count, 2);
+ bool found1;
+ bool found2;
+ for (uint256 i = 0; i < count; i++) {
+ if (uuids[i] == UUID_1) found1 = true;
+ if (uuids[i] == UUID_2) found2 = true;
+ }
+ assertTrue(found1 && found2);
+ }
+
+ function test_buildBundle_lifecycle_promotionsAndBurns() public {
+ vm.prank(alice);
+ uint256 l1 = fleet.registerFleetLocal(_uuid(100), US, ADMIN_CA, 0);
+ vm.prank(alice);
+ fleet.registerFleetLocal(_uuid(101), US, ADMIN_CA, 0);
+ vm.prank(alice);
+ fleet.registerFleetLocal(_uuid(102), US, ADMIN_CA, 0);
+
+ vm.prank(alice);
+ uint256 c1 = fleet.registerFleetCountry(_uuid(200), US, 0);
+ vm.prank(alice);
+ fleet.registerFleetCountry(_uuid(201), US, 0);
+
+ vm.prank(alice);
+ fleet.registerFleetLocal(_uuid(300), US, ADMIN_CA, 0);
+
+ vm.prank(alice);
+ fleet.reassignTier(l1, 3);
+ vm.prank(alice);
+ fleet.reassignTier(c1, 1);
+
+ (, uint256 count) = fleet.buildHighestBondedUuidBundle(US, ADMIN_CA);
+ // Cursor=3: local(1)→include. Count=1.
+ // Cursor=2: empty. Descend.
+ // Cursor=1: country(1)→include. Count=2.
+ // Cursor=0: local(3)+country(1)=4→include. Count=6.
+ assertEq(count, 6);
+
+ vm.prank(alice);
+ fleet.burn(l1);
+
+ (, count) = fleet.buildHighestBondedUuidBundle(US, ADMIN_CA);
+ assertEq(count, 5);
+ }
+
+ // ── Cap enforcement ──
+
+ function test_buildBundle_capsAt20() public {
+ // Fill local: 4+4+4 = 12 in 3 tiers
+ _registerNLocalAt(alice, US, ADMIN_CA, 4, 0, 0);
+ _registerNLocalAt(alice, US, ADMIN_CA, 4, 100, 1);
+ _registerNLocalAt(alice, US, ADMIN_CA, 4, 200, 2);
+ // Fill country US: 4+4 = 8 in 2 tiers (TIER_CAPACITY = 4)
+ _registerNCountryAt(bob, US, 4, 1000, 0);
+ _registerNCountryAt(bob, US, 4, 1100, 1);
+
+ (, uint256 count) = fleet.buildHighestBondedUuidBundle(US, ADMIN_CA);
+ assertEq(count, 20);
+ }
+
+ function test_buildBundle_exactlyFillsToCapacity() public {
+ // 12 local + 8 country = 20 exactly, spread across tiers (TIER_CAPACITY = 4).
+ _registerNLocalAt(alice, US, ADMIN_CA, 4, 1000, 0);
+ _registerNLocalAt(alice, US, ADMIN_CA, 4, 1100, 1);
+ _registerNLocalAt(alice, US, ADMIN_CA, 4, 1200, 2);
+ _registerNCountryAt(alice, US, 4, 2000, 0);
+ _registerNCountryAt(alice, US, 4, 2100, 1);
+
+ (, uint256 count) = fleet.buildHighestBondedUuidBundle(US, ADMIN_CA);
+ assertEq(count, 20);
+ }
+
+ function test_buildBundle_twentyOneMembers_partialInclusion() public {
+ // 21 total: local 12 + country 8 + 1 extra country at tier 2.
+ // With partial inclusion, bundle fills to 20.
+ // TIER_CAPACITY = 4, so spread across tiers.
+ _registerNLocalAt(alice, US, ADMIN_CA, 4, 1000, 0);
+ _registerNLocalAt(alice, US, ADMIN_CA, 4, 1100, 1);
+ _registerNLocalAt(alice, US, ADMIN_CA, 4, 1200, 2);
+ _registerNCountryAt(alice, US, 4, 2000, 0);
+ _registerNCountryAt(alice, US, 4, 2100, 1);
+ vm.prank(alice);
+ fleet.registerFleetCountry(_uuid(3000), US, 2);
+
+ // Cursor=2: local(4)+country(1)=5. Count=5, room=15.
+ // Cursor=1: local(4)+country(4)=8. Count=13, room=7.
+ // Cursor=0: local(4)≤7→include 4[count=17,room=3].
+ // country(4)>3→include 3 of 4[count=20,room=0].
+ (, uint256 count) = fleet.buildHighestBondedUuidBundle(US, ADMIN_CA);
+ assertEq(count, 20); // caps at max bundle size
+ }
+
+ // ── Integrity ──
+
+ function test_buildBundle_noDuplicateUUIDs() public {
+ _registerNLocalAt(alice, US, ADMIN_CA, 4, 1000, 0);
+ _registerNCountryAt(bob, US, 4, 2000, 0);
+
+ (bytes16[] memory uuids, uint256 count) = fleet.buildHighestBondedUuidBundle(US, ADMIN_CA);
+ for (uint256 i = 0; i < count; i++) {
+ for (uint256 j = i + 1; j < count; j++) {
+ assertTrue(uuids[i] != uuids[j], "Duplicate UUID found");
+ }
+ }
+ }
+
+ function test_buildBundle_noNonExistentUUIDs() public {
+ _registerNLocalAt(alice, US, ADMIN_CA, 3, 1000, 0);
+ _registerNCountryAt(bob, US, 2, 2000, 0);
+ vm.prank(carol);
+ fleet.registerFleetLocal(UUID_1, US, ADMIN_CA, 0);
+
+ (bytes16[] memory uuids, uint256 count) = fleet.buildHighestBondedUuidBundle(US, ADMIN_CA);
+ assertEq(count, 6);
+ for (uint256 i = 0; i < count; i++) {
+ uint256 tokenId = _findTokenId(uuids[i], US, ADMIN_CA);
+ assertTrue(fleet.ownerOf(tokenId) != address(0));
+ }
+ }
+
+ function test_buildBundle_allReturnedAreFromCorrectRegions() public {
+ // Verify returned UUIDs are from local or country regions.
+ _registerNLocalAt(alice, US, ADMIN_CA, 4, 1000, 0);
+ _registerNCountryAt(alice, US, 3, 2000, 0);
+
+ (bytes16[] memory uuids, uint256 count) = fleet.buildHighestBondedUuidBundle(US, ADMIN_CA);
+
+ uint256 localFound;
+ uint256 countryFound;
+ for (uint256 i = 0; i < count; i++) {
+ uint256 tid = _findTokenId(uuids[i], US, ADMIN_CA);
+ uint32 region = fleet.tokenRegion(tid);
+ if (region == _regionUSCA()) localFound++;
+ else if (region == _regionUS()) countryFound++;
+ }
+ assertEq(localFound, 4, "local count");
+ assertEq(countryFound, 3, "country count");
+ }
+
+ // ── Fuzz ──
+
+ function testFuzz_buildBundle_neverExceeds20(uint8 cCount, uint8 lCount) public {
+ cCount = uint8(bound(cCount, 0, 15));
+ lCount = uint8(bound(lCount, 0, 15));
+
+ for (uint256 i = 0; i < cCount; i++) {
+ vm.prank(alice);
+ fleet.registerFleetCountry(_uuid(31_000 + i), US, i / 4);
+ }
+ for (uint256 i = 0; i < lCount; i++) {
+ vm.prank(alice);
+ fleet.registerFleetLocal(_uuid(32_000 + i), US, ADMIN_CA, i / 4);
+ }
+
+ (, uint256 count) = fleet.buildHighestBondedUuidBundle(US, ADMIN_CA);
+ assertLe(count, 20);
+ }
+
+ function testFuzz_buildBundle_noDuplicates(uint8 cCount, uint8 lCount) public {
+ cCount = uint8(bound(cCount, 0, 12));
+ lCount = uint8(bound(lCount, 0, 12));
+
+ for (uint256 i = 0; i < cCount; i++) {
+ vm.prank(alice);
+ fleet.registerFleetCountry(_uuid(41_000 + i), US, i / 4);
+ }
+ for (uint256 i = 0; i < lCount; i++) {
+ vm.prank(alice);
+ fleet.registerFleetLocal(_uuid(42_000 + i), US, ADMIN_CA, i / 4);
+ }
+
+ (bytes16[] memory uuids, uint256 count) = fleet.buildHighestBondedUuidBundle(US, ADMIN_CA);
+ for (uint256 i = 0; i < count; i++) {
+ for (uint256 j = i + 1; j < count; j++) {
+ assertTrue(uuids[i] != uuids[j], "Fuzz: duplicate UUID");
+ }
+ }
+ }
+
+ function testFuzz_buildBundle_allReturnedUUIDsExist(uint8 cCount, uint8 lCount) public {
+ cCount = uint8(bound(cCount, 0, 12));
+ lCount = uint8(bound(lCount, 0, 12));
+
+ for (uint256 i = 0; i < cCount; i++) {
+ vm.prank(alice);
+ fleet.registerFleetCountry(_uuid(51_000 + i), US, i / 4);
+ }
+ for (uint256 i = 0; i < lCount; i++) {
+ vm.prank(alice);
+ fleet.registerFleetLocal(_uuid(52_000 + i), US, ADMIN_CA, i / 4);
+ }
+
+ (bytes16[] memory uuids, uint256 count) = fleet.buildHighestBondedUuidBundle(US, ADMIN_CA);
+ for (uint256 i = 0; i < count; i++) {
+ uint256 tokenId = _findTokenId(uuids[i], US, ADMIN_CA);
+ assertTrue(fleet.ownerOf(tokenId) != address(0), "Fuzz: UUID does not exist");
+ }
+ }
+
+ function testFuzz_buildBundle_partialInclusionInvariant(uint8 cCount, uint8 lCount) public {
+ cCount = uint8(bound(cCount, 0, 12));
+ lCount = uint8(bound(lCount, 0, 12));
+
+ for (uint256 i = 0; i < cCount; i++) {
+ vm.prank(alice);
+ fleet.registerFleetCountry(_uuid(61_000 + i), US, i / 4);
+ }
+ for (uint256 i = 0; i < lCount; i++) {
+ vm.prank(alice);
+ fleet.registerFleetLocal(_uuid(62_000 + i), US, ADMIN_CA, i / 4);
+ }
+
+ (bytes16[] memory uuids2, uint256 count2) = fleet.buildHighestBondedUuidBundle(US, ADMIN_CA);
+
+ // With partial inclusion: for each (region, tier) group in the bundle,
+ // the included members should be a PREFIX of the full tier (registration order).
+ // We verify this by checking that included members are the first N in the tier's array.
+ for (uint256 i = 0; i < count2; i++) {
+ uint256 tid = _findTokenId(uuids2[i], US, ADMIN_CA);
+ uint32 region = fleet.tokenRegion(tid);
+ uint256 tier = fleet.fleetTier(tid);
+
+ // Count how many from this (region, tier) are in the bundle
+ uint256 inBundle;
+ for (uint256 j = 0; j < count2; j++) {
+ uint256 tjd = _findTokenId(uuids2[j], US, ADMIN_CA);
+ if (fleet.tokenRegion(tjd) == region && fleet.fleetTier(tjd) == tier) {
+ inBundle++;
+ }
+ }
+
+ // Get the full tier members
+ uint256[] memory tierMembers = fleet.getTierMembers(region, tier);
+
+ // The included count should be <= total tier members
+ assertLe(inBundle, tierMembers.length, "Fuzz: more included than exist");
+
+ // Verify the included members are exactly the first `inBundle` members of the tier
+ // (prefix property for partial inclusion)
+ uint256 found;
+ for (uint256 m = 0; m < inBundle && m < tierMembers.length; m++) {
+ bytes16 expectedUuid = bytes16(uint128(tierMembers[m]));
+ for (uint256 j = 0; j < count2; j++) {
+ if (uuids2[j] == expectedUuid) {
+ found++;
+ break;
+ }
+ }
+ }
+ assertEq(found, inBundle, "Fuzz: included members not a prefix of tier");
+ }
+ }
+
+ // ══════════════════════════════════════════════════════════════════════════════════
+ // Edge Cases: _findCheapestInclusionTier & MaxTiersReached
+ // ══════════════════════════════════════════════════════════════════════════════════
+
+ /// @notice When all 24 tiers of a region are full, localInclusionHint should revert.
+ function test_RevertIf_localInclusionHint_allTiersFull() public {
+ uint256 cap = fleet.TIER_CAPACITY();
+ uint256 maxTiers = fleet.MAX_TIERS();
+ // Fill all tiers of US/ADMIN_CA (cap members each)
+ for (uint256 tier = 0; tier < maxTiers; tier++) {
+ for (uint256 i = 0; i < cap; i++) {
+ vm.prank(alice);
+ fleet.registerFleetLocal(_uuid(tier * 100 + i), US, ADMIN_CA, tier);
+ }
+ }
+
+ // Verify all tiers are full
+ for (uint256 tier = 0; tier < maxTiers; tier++) {
+ assertEq(fleet.tierMemberCount(fleet.makeAdminRegion(US, ADMIN_CA), tier), cap);
+ }
+
+ // localInclusionHint should revert
+ vm.expectRevert(FleetIdentityUpgradeable.MaxTiersReached.selector);
+ fleet.localInclusionHint(US, ADMIN_CA);
+ }
+
+ /// @notice When all tiers are full, registering at any tier should revert with TierFull.
+ function test_RevertIf_registerFleetLocal_allTiersFull() public {
+ uint256 cap = fleet.TIER_CAPACITY();
+ uint256 maxTiers = fleet.MAX_TIERS();
+ // Fill all tiers
+ for (uint256 tier = 0; tier < maxTiers; tier++) {
+ for (uint256 i = 0; i < cap; i++) {
+ vm.prank(alice);
+ fleet.registerFleetLocal(_uuid(tier * 100 + i), US, ADMIN_CA, tier);
+ }
+ }
+
+ // Registration at tier 0 (or any full tier) should revert with TierFull
+ vm.prank(bob);
+ vm.expectRevert(FleetIdentityUpgradeable.TierFull.selector);
+ fleet.registerFleetLocal(_uuid(99999), US, ADMIN_CA, 0);
+ }
+
+ /// @notice countryInclusionHint reverts when all tiers in the country region are full.
+ function test_RevertIf_countryInclusionHint_allTiersFull() public {
+ uint256 cap = fleet.TIER_CAPACITY();
+ uint256 maxTiers = fleet.MAX_TIERS();
+ // Fill all tiers of country US (cap members each)
+ for (uint256 tier = 0; tier < maxTiers; tier++) {
+ for (uint256 i = 0; i < cap; i++) {
+ vm.prank(alice);
+ fleet.registerFleetCountry(_uuid(tier * 100 + i), US, tier);
+ }
+ }
+
+ vm.expectRevert(FleetIdentityUpgradeable.MaxTiersReached.selector);
+ fleet.countryInclusionHint(US);
+ }
+
+ /// @notice Proves cheapest inclusion tier can be ABOVE maxTierIndex when bundle is
+ /// constrained by higher-priority levels at existing tiers.
+ ///
+ /// Scenario:
+ /// - Fill admin tiers 0, 1, 2 with 4 members each (full)
+ /// - Country US has 4 fleets at tier 2 (maxTierIndex)
+ /// - Admin tier 0-2 are FULL (4 members each), so a new fleet cannot join any.
+ /// - Cheapest inclusion should be tier 3 (above maxTierIndex=2).
+ function test_cheapestInclusionTier_aboveMaxTierIndex() public {
+ uint256 cap = fleet.TIER_CAPACITY();
+ // Fill admin tiers 0, 1, 2 with TIER_CAPACITY members each
+ _registerNLocalAt(alice, US, ADMIN_CA, cap, 4000, 0);
+ _registerNLocalAt(alice, US, ADMIN_CA, cap, 5000, 1);
+ _registerNLocalAt(alice, US, ADMIN_CA, cap, 6000, 2);
+ // Country at tier 2 (sets maxTierIndex across regions)
+ _registerNCountryAt(alice, US, cap, 7000, 2);
+
+ // Verify tier 2 is maxTierIndex
+ assertEq(fleet.regionTierCount(uint32(US)), 3);
+ assertEq(fleet.regionTierCount(fleet.makeAdminRegion(US, ADMIN_CA)), 3);
+
+ // All admin tiers 0-2 are full (TIER_CAPACITY members each)
+ assertEq(fleet.tierMemberCount(fleet.makeAdminRegion(US, ADMIN_CA), 0), cap);
+ assertEq(fleet.tierMemberCount(fleet.makeAdminRegion(US, ADMIN_CA), 1), cap);
+ assertEq(fleet.tierMemberCount(fleet.makeAdminRegion(US, ADMIN_CA), 2), cap);
+
+ // At tiers 0-2: all tiers are full, cannot join.
+ // At tier 3: above maxTierIndex, countBefore = 0, has room.
+ (uint256 inclusionTier, uint256 bond) = fleet.localInclusionHint(US, ADMIN_CA);
+ assertEq(inclusionTier, 3, "Should recommend tier 3 (above maxTierIndex=2)");
+ assertEq(bond, BASE_BOND * 8); // local tier 3 bond = BASE_BOND * 2^3
+
+ // Verify registration at tier 3 works
+ vm.prank(bob);
+ uint256 tokenId = fleet.registerFleetLocal(_uuid(9999), US, ADMIN_CA, 3);
+ assertEq(fleet.fleetTier(tokenId), 3);
+
+ // Confirm new fleet appears in bundle at the TOP (first position)
+ (bytes16[] memory uuids, uint256 count) = fleet.buildHighestBondedUuidBundle(US, ADMIN_CA);
+ // tier 3 (1) + tier 2 admin (cap) + tier 2 country (cap) + tier 1 admin (cap) + tier 0 admin (cap)
+ // = 1 + 4*cap, capped at MAX_BONDED_UUID_BUNDLE_SIZE
+ uint256 expectedCount = 1 + 4 * cap;
+ if (expectedCount > fleet.MAX_BONDED_UUID_BUNDLE_SIZE()) {
+ expectedCount = fleet.MAX_BONDED_UUID_BUNDLE_SIZE();
+ }
+ assertEq(count, expectedCount);
+ assertEq(uuids[0], _uuid(9999), "Tier 3 fleet should be first in bundle");
+ }
+
+ /// @notice Edge case: bundle is full from tier maxTierIndex, and all tiers 0..maxTierIndex
+ /// at the candidate region are also full. The cheapest tier is above maxTierIndex.
+ function test_cheapestInclusionTier_aboveMaxTierIndex_candidateTiersFull() public {
+ uint256 cap = fleet.TIER_CAPACITY();
+ // Country tier 0 has TIER_CAPACITY fleets
+ _registerNCountryAt(alice, US, cap, 1000, 0);
+
+ // Admin tier 0 has TIER_CAPACITY fleets (full)
+ _registerNLocalAt(alice, US, ADMIN_CA, cap, 2000, 0);
+
+ // Verify admin tier 0 is full
+ assertEq(fleet.tierMemberCount(fleet.makeAdminRegion(US, ADMIN_CA), 0), cap);
+
+ // Admin tier 0 is full, so candidate must go elsewhere.
+ // Cheapest inclusion tier should be 1 (above maxTierIndex=0).
+ (uint256 inclusionTier,) = fleet.localInclusionHint(US, ADMIN_CA);
+ assertEq(inclusionTier, 1, "Should recommend tier 1 since tier 0 is full");
+ }
+
+ /// @notice When going above maxTierIndex would require tier >= MAX_TIERS, revert.
+ ///
+ /// Scenario: Fill global tiers 0-23 with 4 members each (96 global fleets).
+ /// A new LOCAL fleet cannot fit in any tier because:
+ /// - The bundle simulation runs through tiers 23→0
+ /// - At each tier, global's 4 members + potential admin members need to fit
+ /// - With global filling 4 slots at every tier, and country/admin potentially
+ /// competing, we design a scenario where no tier works.
+ ///
+ /// Simpler approach: Fill all 24 admin tiers AND make bundle full at every tier.
+ function test_RevertIf_cheapestInclusionTier_exceedsMaxTiers() public {
+ uint256 cap = fleet.TIER_CAPACITY();
+ // Fill all 24 tiers of admin area US/CA with TIER_CAPACITY members each
+ for (uint256 tier = 0; tier < fleet.MAX_TIERS(); tier++) {
+ for (uint256 i = 0; i < cap; i++) {
+ vm.prank(alice);
+ fleet.registerFleetLocal(_uuid(tier * 100 + i), US, ADMIN_CA, tier);
+ }
+ }
+
+ // Now all admin tiers 0-23 are full. A new admin fleet must go to tier 24,
+ // which exceeds MAX_TIERS=24 (valid tiers are 0-23).
+ vm.expectRevert(FleetIdentityUpgradeable.MaxTiersReached.selector);
+ fleet.localInclusionHint(US, ADMIN_CA);
+ }
+
+ /// @notice Verify that when bundle is full due to higher-tier members preventing
+ /// lower-tier inclusion, the hint correctly identifies the cheapest viable tier.
+ function test_cheapestInclusionTier_bundleFullFromHigherTiers() public {
+ uint256 cap = fleet.TIER_CAPACITY();
+ // Create a scenario where:
+ // - Admin tiers 0-5 are all full (TIER_CAPACITY each)
+ // - Country tier 5 has TIER_CAPACITY members
+ // All admin tiers 0-5 are full, so must go to tier 6.
+
+ // Fill admin tiers 0-5 with TIER_CAPACITY members each
+ for (uint256 tier = 0; tier <= 5; tier++) {
+ _registerNLocalAt(alice, US, ADMIN_CA, cap, 10000 + tier * 100, tier);
+ }
+ // Country at tier 5
+ _registerNCountryAt(alice, US, cap, 11000, 5);
+
+ // maxTierIndex = 5
+ // All admin tiers 0-5 are full. Cannot join any.
+ // At tier 6: above maxTierIndex, countBefore = 0. Has room.
+ (uint256 inclusionTier,) = fleet.localInclusionHint(US, ADMIN_CA);
+ assertEq(inclusionTier, 6, "Must go above maxTierIndex=5 to tier 6");
+ }
+
+ /// @notice Verifies the bundle correctly includes a fleet registered above maxTierIndex.
+ function test_buildBundle_includesFleetAboveMaxTierIndex() public {
+ uint256 cap = fleet.TIER_CAPACITY();
+ // Only country tier 0 has fleets (maxTierIndex = 0)
+ _registerNCountryAt(alice, US, cap, 20000, 0);
+
+ // New admin registers at tier 2 (above maxTierIndex)
+ vm.prank(bob);
+ uint256 adminToken = fleet.registerFleetLocal(_uuid(21000), US, ADMIN_CA, 2);
+
+ // Bundle should include admin tier 2 first (highest), then country tier 0
+ (bytes16[] memory uuids, uint256 count) = fleet.buildHighestBondedUuidBundle(US, ADMIN_CA);
+ // Admin tier 2 (1) + Country tier 0 (cap)
+ uint256 expectedCount = 1 + cap;
+ if (expectedCount > fleet.MAX_BONDED_UUID_BUNDLE_SIZE()) {
+ expectedCount = fleet.MAX_BONDED_UUID_BUNDLE_SIZE();
+ }
+ assertEq(count, expectedCount);
+
+ // First should be admin tier 2
+ assertEq(_tokenId(uuids[0], _regionUSCA()), adminToken, "Admin tier 2 fleet should be first");
+ }
+
+ // ══════════════════════════════════════════════════════════════════════════════════
+ // Demonstration: Partial inclusion prevents total tier displacement
+ // ══════════════════════════════════════════════════════════════════════════════════
+
+ /// @notice DEMONSTRATES that partial inclusion prevents the scenario where a single
+ /// fleet registration could push an entire tier out of the bundle.
+ ///
+ /// Scenario (2-level system: country + local):
+ /// BEFORE:
+ /// - Admin tier 0: 4 members
+ /// - Country tier 0: 4 members
+ /// - Bundle: all 8 members included (4+4=8)
+ ///
+ /// AFTER (single admin tier 1 registration):
+ /// - Admin tier 1: 1 member (NEW - above previous maxTierIndex)
+ /// - With PARTIAL INCLUSION:
+ /// - Tier 1: admin(1) → count=1
+ /// - Tier 0: admin(4) + country(4) = 8, count=9
+ /// - Final bundle: 9 members (all fit)
+ ///
+ /// Result: All original fleets remain included.
+ function test_DEMO_partialInclusionPreventsFullDisplacement() public {
+ // === BEFORE STATE ===
+ uint32 countryRegion = uint32(US);
+
+ // Fill with admin(4) + country(4) = 8
+ uint256[] memory localIds = _registerNLocalAt(alice, US, ADMIN_CA, 4, 30000, 0); // Admin tier 0: 4
+ uint256[] memory countryIds = _registerNCountryAt(alice, US, 4, 31000, 0); // Country tier 0: 4
+
+ // Verify BEFORE: all 8 members in bundle
+ (bytes16[] memory uuidsBefore, uint256 countBefore) = fleet.buildHighestBondedUuidBundle(US, ADMIN_CA);
+ assertEq(countBefore, 8, "BEFORE: All 8 members should be in bundle");
+
+ // Verify all 4 country fleets are included BEFORE
+ uint256 countryCountBefore;
+ for (uint256 i = 0; i < countBefore; i++) {
+ uint256 tokenId = _findTokenId(uuidsBefore[i], US, ADMIN_CA);
+ if (fleet.tokenRegion(tokenId) == countryRegion) countryCountBefore++;
+ }
+ assertEq(countryCountBefore, 4, "BEFORE: All 4 country fleets in bundle");
+
+ // === SINGLE REGISTRATION ===
+ // Bob registers just ONE fleet at admin tier 1
+ vm.prank(bob);
+ fleet.registerFleetLocal(_uuid(99999), US, ADMIN_CA, 1);
+
+ // === AFTER STATE ===
+ (bytes16[] memory uuidsAfter, uint256 countAfter) = fleet.buildHighestBondedUuidBundle(US, ADMIN_CA);
+
+ // Bundle now has 9 members (tier 1: 1 + tier 0: 4+4)
+ assertEq(countAfter, 9, "AFTER: Bundle should have 9 members");
+
+ // Count how many country fleets are included AFTER
+ uint256 countryCountAfter;
+ for (uint256 i = 0; i < countAfter; i++) {
+ uint256 tokenId = _findTokenId(uuidsAfter[i], US, ADMIN_CA);
+ if (fleet.tokenRegion(tokenId) == countryRegion) countryCountAfter++;
+ }
+ assertEq(countryCountAfter, 4, "AFTER: All 4 country fleets still in bundle");
+
+ // Verify all country fleets are still included
+ bool[] memory countryIncluded = new bool[](4);
+ for (uint256 i = 0; i < countAfter; i++) {
+ uint256 tokenId = _findTokenId(uuidsAfter[i], US, ADMIN_CA);
+ for (uint256 c = 0; c < 4; c++) {
+ if (tokenId == countryIds[c]) countryIncluded[c] = true;
+ }
+ }
+ assertTrue(countryIncluded[0], "First country fleet included");
+ assertTrue(countryIncluded[1], "Second country fleet included");
+ assertTrue(countryIncluded[2], "Third country fleet included");
+ assertTrue(countryIncluded[3], "Fourth country fleet included");
+
+ // === IMPROVEMENT SUMMARY ===
+ emit log_string("=== PARTIAL INCLUSION FIX DEMONSTRATED ===");
+ emit log_string("A single tier-1 registration does not displace any country fleets");
+ emit log_named_uint("Country fleets displaced", 0);
+ emit log_named_uint("Country fleets still included", 4);
+ }
+
+ // ══════════════════════════════════════════════════════════════════════════════
+ // buildCountryOnlyBundle tests
+ // ══════════════════════════════════════════════════════════════════════════════
+
+ function test_buildCountryOnlyBundle_emptyCountry() public view {
+ // No fleets registered yet
+ (bytes16[] memory uuids, uint256 count) = fleet.buildCountryOnlyBundle(US);
+ assertEq(count, 0, "Empty country should have 0 UUIDs");
+ assertEq(uuids.length, 0, "Array should be trimmed to 0");
+ }
+
+ function test_buildCountryOnlyBundle_onlyCountryFleets() public {
+ // Register 3 country fleets at different tiers
+ vm.prank(alice);
+ fleet.registerFleetCountry(_uuid(1), US, 0);
+ vm.prank(alice);
+ fleet.registerFleetCountry(_uuid(2), US, 1);
+ vm.prank(alice);
+ fleet.registerFleetCountry(_uuid(3), US, 2);
+
+ (bytes16[] memory uuids, uint256 count) = fleet.buildCountryOnlyBundle(US);
+ assertEq(count, 3, "Should include all 3 country fleets");
+
+ // Verify tier priority order (highest first)
+ assertEq(uuids[0], _uuid(3), "Tier 2 should be first");
+ assertEq(uuids[1], _uuid(2), "Tier 1 should be second");
+ assertEq(uuids[2], _uuid(1), "Tier 0 should be third");
+ }
+
+ function test_buildCountryOnlyBundle_excludesLocalFleets() public {
+ // Register country fleet
+ vm.prank(alice);
+ fleet.registerFleetCountry(_uuid(1), US, 0);
+
+ // Register local fleet in same country
+ vm.prank(alice);
+ fleet.registerFleetLocal(_uuid(2), US, ADMIN_CA, 0);
+
+ // Country-only bundle should ONLY include country fleet
+ (bytes16[] memory uuids, uint256 count) = fleet.buildCountryOnlyBundle(US);
+ assertEq(count, 1, "Should only include country fleet");
+ assertEq(uuids[0], _uuid(1), "Should be the country fleet UUID");
+ }
+
+ function test_buildCountryOnlyBundle_respectsMaxBundleSize() public {
+ // Register 24 country fleets across 6 tiers (4 per tier = TIER_CAPACITY)
+ // This gives us more than MAX_BONDED_UUID_BUNDLE_SIZE (20)
+ for (uint256 tier = 0; tier < 6; tier++) {
+ for (uint256 i = 0; i < 4; i++) {
+ vm.prank(alice);
+ fleet.registerFleetCountry(_uuid(tier * 100 + i), US, tier);
+ }
+ }
+
+ (bytes16[] memory uuids, uint256 count) = fleet.buildCountryOnlyBundle(US);
+ assertEq(count, 20, "Should cap at 20 UUIDs");
+ assertEq(uuids.length, 20, "Array should be trimmed to 20");
+ }
+
+ function test_RevertIf_buildCountryOnlyBundle_invalidCountryCode() public {
+ vm.expectRevert(FleetIdentityUpgradeable.InvalidCountryCode.selector);
+ fleet.buildCountryOnlyBundle(0);
+
+ vm.expectRevert(FleetIdentityUpgradeable.InvalidCountryCode.selector);
+ fleet.buildCountryOnlyBundle(1000); // > MAX_COUNTRY_CODE (999)
+ }
+
+ function test_buildCountryOnlyBundle_multipleCountriesIndependent() public {
+ // Register in US (country 840)
+ vm.prank(alice);
+ fleet.registerFleetCountry(_uuid(1), US, 0);
+
+ // Register in Germany (country 276)
+ vm.prank(alice);
+ fleet.registerFleetCountry(_uuid(2), DE, 0);
+
+ // US bundle should only have US fleet
+ (bytes16[] memory usUuids, uint256 usCount) = fleet.buildCountryOnlyBundle(US);
+ assertEq(usCount, 1, "US should have 1 fleet");
+ assertEq(usUuids[0], _uuid(1), "Should be US fleet");
+
+ // Germany bundle should only have Germany fleet
+ (bytes16[] memory deUuids, uint256 deCount) = fleet.buildCountryOnlyBundle(DE);
+ assertEq(deCount, 1, "Germany should have 1 fleet");
+ assertEq(deUuids[0], _uuid(2), "Should be Germany fleet");
+ }
+
+ // ══════════════════════════════════════════════
+ // Operator Tests
+ // ══════════════════════════════════════════════
+
+ function test_operatorOf_defaultsToUuidOwner() public {
+ vm.prank(alice);
+ fleet.registerFleetLocal(UUID_1, US, ADMIN_CA, 0);
+
+ // No operator set, should default to uuidOwner
+ assertEq(fleet.operatorOf(UUID_1), alice);
+ }
+
+ function test_operatorOf_returnsSetOperator() public {
+ vm.prank(alice);
+ fleet.registerFleetLocal(UUID_1, US, ADMIN_CA, 0);
+
+ vm.prank(alice);
+ fleet.setOperator(UUID_1, bob);
+
+ assertEq(fleet.operatorOf(UUID_1), bob);
+ }
+
+ function test_setOperator_emitsEvent() public {
+ vm.prank(alice);
+ fleet.registerFleetLocal(UUID_1, US, ADMIN_CA, 0);
+
+ // Just verify setOperator succeeds and changes state
+ vm.prank(alice);
+ fleet.setOperator(UUID_1, bob);
+ assertEq(fleet.operatorOf(UUID_1), bob);
+ }
+
+ function test_setOperator_transfersTierExcess() public {
+ vm.prank(alice);
+ fleet.registerFleetLocal(UUID_1, US, ADMIN_CA, 2);
+
+ uint256 tierBonds = fleet.tierBond(2, false);
+ uint256 aliceBefore = bondToken.balanceOf(alice);
+ uint256 bobBefore = bondToken.balanceOf(bob);
+
+ vm.prank(alice);
+ fleet.setOperator(UUID_1, bob);
+
+ // Alice gets full tier bonds refunded, bob pays full tier bonds
+ assertEq(bondToken.balanceOf(alice), aliceBefore + tierBonds);
+ assertEq(bondToken.balanceOf(bob), bobBefore - tierBonds);
+ }
+
+ function test_setOperator_multiRegion_transfersAllTierExcess() public {
+ // Register in two local regions at different tiers
+ vm.prank(alice);
+ fleet.registerFleetLocal(UUID_1, US, ADMIN_CA, 2);
+ vm.prank(alice);
+ fleet.registerFleetLocal(UUID_1, US, ADMIN_NY, 1);
+
+ uint256 tierBondsFirst = fleet.tierBond(2, false);
+ uint256 tierBondsSecond = fleet.tierBond(1, false);
+ uint256 totalTierBonds = tierBondsFirst + tierBondsSecond;
+
+ uint256 aliceBefore = bondToken.balanceOf(alice);
+ uint256 bobBefore = bondToken.balanceOf(bob);
+
+ vm.prank(alice);
+ fleet.setOperator(UUID_1, bob);
+
+ assertEq(bondToken.balanceOf(alice), aliceBefore + totalTierBonds);
+ assertEq(bondToken.balanceOf(bob), bobBefore - totalTierBonds);
+ }
+
+ function test_setOperator_zeroTierExcess_noTransfer() public {
+ // Register at tier 0, tierBond = BASE_BOND
+ vm.prank(alice);
+ fleet.registerFleetLocal(UUID_1, US, ADMIN_CA, 0);
+
+ uint256 tierBonds = fleet.tierBond(0, false);
+ uint256 aliceBefore = bondToken.balanceOf(alice);
+ uint256 bobBefore = bondToken.balanceOf(bob);
+
+ vm.prank(alice);
+ fleet.setOperator(UUID_1, bob);
+
+ // Full tier bonds transferred (tierBond(0, false) = BASE_BOND)
+ assertEq(bondToken.balanceOf(alice), aliceBefore + tierBonds);
+ assertEq(bondToken.balanceOf(bob), bobBefore - tierBonds);
+ }
+
+ function test_setOperator_changeOperator_transfersBetweenOperators() public {
+ vm.prank(alice);
+ fleet.registerFleetLocal(UUID_1, US, ADMIN_CA, 2);
+
+ // Set bob as operator
+ vm.prank(alice);
+ fleet.setOperator(UUID_1, bob);
+
+ uint256 tierBonds = fleet.tierBond(2, false);
+ uint256 bobBefore = bondToken.balanceOf(bob);
+ uint256 carolBefore = bondToken.balanceOf(carol);
+
+ // Change operator from bob to carol
+ vm.prank(alice);
+ fleet.setOperator(UUID_1, carol);
+
+ assertEq(bondToken.balanceOf(bob), bobBefore + tierBonds);
+ assertEq(bondToken.balanceOf(carol), carolBefore - tierBonds);
+ }
+
+ function test_setOperator_clearOperator_refundsToOwner() public {
+ vm.prank(alice);
+ fleet.registerFleetLocal(UUID_1, US, ADMIN_CA, 2);
+
+ vm.prank(alice);
+ fleet.setOperator(UUID_1, bob);
+
+ uint256 tierBonds = fleet.tierBond(2, false);
+ uint256 aliceBefore = bondToken.balanceOf(alice);
+ uint256 bobBefore = bondToken.balanceOf(bob);
+
+ // Clear operator (set to address(0))
+ vm.prank(alice);
+ fleet.setOperator(UUID_1, address(0));
+
+ assertEq(bondToken.balanceOf(bob), bobBefore + tierBonds);
+ assertEq(bondToken.balanceOf(alice), aliceBefore - tierBonds);
+ assertEq(fleet.operatorOf(UUID_1), alice); // defaults to owner again
+ }
+
+ function test_RevertIf_setOperator_notUuidOwner() public {
+ vm.prank(alice);
+ fleet.registerFleetLocal(UUID_1, US, ADMIN_CA, 0);
+
+ vm.prank(bob);
+ vm.expectRevert(FleetIdentityUpgradeable.NotUuidOwner.selector);
+ fleet.setOperator(UUID_1, carol);
+ }
+
+ function test_setOperator_ownedOnly() public {
+ vm.prank(alice);
+ fleet.claimUuid(UUID_1, address(0));
+
+ // setOperator now works for owned-only UUIDs
+ vm.prank(alice);
+ fleet.setOperator(UUID_1, bob);
+ assertEq(fleet.operatorOf(UUID_1), bob);
+ }
+
+ function test_setOperator_country() public {
+ // Register at tier 2
+ vm.prank(alice);
+ fleet.registerFleetCountry(UUID_1, US, 2);
+
+ uint256 aliceAfterReg = bondToken.balanceOf(alice);
+ uint256 bobBefore = bondToken.balanceOf(bob);
+
+ // Now set operator - bob pays full tier bonds to alice
+ vm.prank(alice);
+ fleet.setOperator(UUID_1, bob);
+
+ uint256 tierBonds = fleet.tierBond(2, true);
+ assertEq(bondToken.balanceOf(alice), aliceAfterReg + tierBonds);
+ assertEq(bondToken.balanceOf(bob), bobBefore - tierBonds);
+ assertEq(fleet.operatorOf(UUID_1), bob);
+ assertEq(fleet.uuidOwner(UUID_1), alice);
+ }
+
+ function test_setOperator_local() public {
+ // Register at tier 2
+ vm.prank(alice);
+ fleet.registerFleetLocal(UUID_1, US, ADMIN_CA, 2);
+
+ uint256 aliceAfterReg = bondToken.balanceOf(alice);
+ uint256 bobBefore = bondToken.balanceOf(bob);
+
+ // Now set operator - bob pays full tier bonds to alice
+ vm.prank(alice);
+ fleet.setOperator(UUID_1, bob);
+
+ uint256 tierBonds = fleet.tierBond(2, false);
+ assertEq(bondToken.balanceOf(alice), aliceAfterReg + tierBonds);
+ assertEq(bondToken.balanceOf(bob), bobBefore - tierBonds);
+ assertEq(fleet.operatorOf(UUID_1), bob);
+ }
+
+ function test_operatorCanPromote() public {
+ // Register then set operator
+ vm.prank(alice);
+ fleet.registerFleetLocal(UUID_1, US, ADMIN_CA, 0);
+ vm.prank(alice);
+ fleet.setOperator(UUID_1, bob);
+
+ uint256 bobBefore = bondToken.balanceOf(bob);
+ uint256 tokenId = _tokenId(UUID_1, _makeAdminRegion(US, ADMIN_CA));
+
+ vm.prank(bob);
+ fleet.promote(tokenId);
+
+ assertEq(fleet.fleetTier(tokenId), 1);
+ // Bob paid the tier difference
+ uint256 tierDiff = fleet.tierBond(1, false) - fleet.tierBond(0, false);
+ assertEq(bondToken.balanceOf(bob), bobBefore - tierDiff);
+ }
+
+ function test_operatorCanDemote() public {
+ // Register then set operator
+ vm.prank(alice);
+ fleet.registerFleetLocal(UUID_1, US, ADMIN_CA, 2);
+ vm.prank(alice);
+ fleet.setOperator(UUID_1, bob);
+
+ uint256 bobBefore = bondToken.balanceOf(bob);
+ uint256 tokenId = _tokenId(UUID_1, _makeAdminRegion(US, ADMIN_CA));
+
+ vm.prank(bob);
+ fleet.reassignTier(tokenId, 0);
+
+ assertEq(fleet.fleetTier(tokenId), 0);
+ // Bob gets tier difference refunded
+ uint256 tierDiff = fleet.tierBond(2, false) - fleet.tierBond(0, false);
+ assertEq(bondToken.balanceOf(bob), bobBefore + tierDiff);
+ }
+
+ function test_RevertIf_ownerCannotPromoteWhenOperatorSet() public {
+ vm.prank(alice);
+ uint256 tokenId = fleet.registerFleetLocal(UUID_1, US, ADMIN_CA, 0);
+ vm.prank(alice);
+ fleet.setOperator(UUID_1, bob);
+
+ vm.prank(alice);
+ vm.expectRevert(FleetIdentityUpgradeable.NotOperator.selector);
+ fleet.promote(tokenId);
+ }
+
+ function test_operatorCanBurnRegisteredToken() public {
+ vm.prank(alice);
+ uint256 tokenId = fleet.registerFleetLocal(UUID_1, US, ADMIN_CA, 2);
+ vm.prank(alice);
+ fleet.setOperator(UUID_1, bob);
+
+ uint256 bobBefore = bondToken.balanceOf(bob);
+
+ // Operator (bob) burns the registered token
+ vm.prank(bob);
+ fleet.burn(tokenId);
+
+ // Bob (operator) gets full tierBond. Alice gets owned-only token minted.
+ assertEq(bondToken.balanceOf(bob), bobBefore + fleet.tierBond(2, false));
+
+ // Verify owned-only token was minted to owner
+ uint256 ownedTokenId = uint256(uint128(UUID_1));
+ assertEq(fleet.ownerOf(ownedTokenId), alice);
+ assertTrue(fleet.isOwnedOnly(UUID_1));
+ }
+
+ function test_RevertIf_ownerCannotBurnRegisteredWithOperator() public {
+ vm.prank(alice);
+ uint256 tokenId = fleet.registerFleetLocal(UUID_1, US, ADMIN_CA, 0);
+ vm.prank(alice);
+ fleet.setOperator(UUID_1, bob);
+
+ // Owner cannot burn when there's a separate operator
+ vm.prank(alice);
+ vm.expectRevert(FleetIdentityUpgradeable.NotOperator.selector);
+ fleet.burn(tokenId);
+ }
+
+ function test_burn_refundsOperatorAndPreservesOperator() public {
+ vm.prank(alice);
+ uint256 tokenId = fleet.registerFleetLocal(UUID_1, US, ADMIN_CA, 2);
+ vm.prank(alice);
+ fleet.setOperator(UUID_1, bob);
+
+ uint256 bobBefore = bondToken.balanceOf(bob);
+
+ // Operator burns registered token
+ vm.prank(bob);
+ fleet.burn(tokenId);
+
+ // Bob (operator) gets full tier bond refunded
+ uint256 tierBond = fleet.tierBond(2, false);
+ assertEq(bondToken.balanceOf(bob), bobBefore + tierBond);
+ // Operator is preserved (still bob) on the new owned-only token
+ assertEq(fleet.operatorOf(UUID_1), bob);
+ assertTrue(fleet.isOwnedOnly(UUID_1));
+ }
+
+ function test_registerFromOwned_preservesOperator() public {
+ // Alice claims UUID with no operator
+ vm.prank(alice);
+ fleet.claimUuid(UUID_1, address(0));
+
+ // She registers to local (which removes owned-only token and creates registered token)
+ vm.prank(alice);
+ uint256 registeredToken = fleet.registerFleetLocal(UUID_1, US, ADMIN_CA, 0);
+
+ assertEq(fleet.operatorOf(UUID_1), alice);
+
+ // Now alice can set an operator
+ vm.prank(alice);
+ fleet.setOperator(UUID_1, bob);
+ assertEq(fleet.operatorOf(UUID_1), bob);
+ }
+
+ function testFuzz_setOperator_tierExcessCalculation(uint8 tier1, uint8 tier2) public {
+ tier1 = uint8(bound(tier1, 0, 7));
+ tier2 = uint8(bound(tier2, 0, 7));
+
+ // Register in two local regions
+ vm.prank(alice);
+ fleet.registerFleetLocal(UUID_1, US, ADMIN_CA, tier1);
+ vm.prank(alice);
+ fleet.registerFleetLocal(UUID_1, US, ADMIN_NY, tier2);
+
+ uint256 expectedTierBonds =
+ fleet.tierBond(tier1, false) +
+ fleet.tierBond(tier2, false);
+
+ uint256 aliceBefore = bondToken.balanceOf(alice);
+ uint256 bobBefore = bondToken.balanceOf(bob);
+
+ vm.prank(alice);
+ fleet.setOperator(UUID_1, bob);
+
+ assertEq(bondToken.balanceOf(alice), aliceBefore + expectedTierBonds);
+ assertEq(bondToken.balanceOf(bob), bobBefore - expectedTierBonds);
+ }
+
+ // ══════════════════════════════════════════════
+ // Additional Operator Management Tests
+ // ══════════════════════════════════════════════
+
+ function test_claimUuid_withOperator() public {
+ // Alice claims UUID with bob as operator
+ vm.prank(alice);
+ uint256 tokenId = fleet.claimUuid(UUID_1, bob);
+
+ assertEq(fleet.operatorOf(UUID_1), bob);
+ assertEq(fleet.uuidOwner(UUID_1), alice);
+ assertEq(fleet.ownerOf(tokenId), alice);
+ }
+
+ function test_claimUuid_operatorPersistsOnRegister() public {
+ // Alice claims UUID with bob as operator
+ vm.prank(alice);
+ fleet.claimUuid(UUID_1, bob);
+
+ assertEq(fleet.operatorOf(UUID_1), bob);
+
+ // When registering, OPERATOR (bob) must call and pays tier bond
+ uint256 bobBefore = bondToken.balanceOf(bob);
+
+ vm.prank(bob);
+ fleet.registerFleetLocal(UUID_1, US, ADMIN_CA, 2);
+
+ uint256 tierBond = fleet.tierBond(2, false);
+ assertEq(bondToken.balanceOf(bob), bobBefore - tierBond);
+ assertEq(fleet.operatorOf(UUID_1), bob);
+ }
+
+ function test_claimUuid_emitsEventWithOperator() public {
+ vm.expectEmit(true, true, false, true);
+ emit FleetIdentityUpgradeable.UuidClaimed(alice, UUID_1, bob);
+
+ vm.prank(alice);
+ fleet.claimUuid(UUID_1, bob);
+ }
+
+ function test_claimUuid_withMsgSenderAsOperator() public {
+ // Using msg.sender should normalize to address(0) internally
+ vm.prank(alice);
+ fleet.claimUuid(UUID_1, alice);
+
+ // operatorOf should return owner when stored operator is address(0)
+ assertEq(fleet.operatorOf(UUID_1), alice);
+ assertEq(fleet.uuidOperator(UUID_1), address(0)); // stored as 0
+ }
+
+ function test_setOperator_ownedOnly_operatorPreservedOnRegister() public {
+ // Claim UUID in owned-only mode
+ vm.prank(alice);
+ fleet.claimUuid(UUID_1, address(0));
+
+ // Set operator while owned-only
+ vm.prank(alice);
+ fleet.setOperator(UUID_1, bob);
+
+ uint256 bobBefore = bondToken.balanceOf(bob);
+
+ // Register - OPERATOR (bob) must call and pays tier bond
+ vm.prank(bob);
+ fleet.registerFleetLocal(UUID_1, US, ADMIN_CA, 2);
+
+ assertEq(fleet.operatorOf(UUID_1), bob);
+ uint256 tierBond = fleet.tierBond(2, false);
+ assertEq(bondToken.balanceOf(bob), bobBefore - tierBond);
+ }
+
+ function test_burn_lastToken_preservesOperator() public {
+ // Register and set operator
+ vm.prank(alice);
+ uint256 tokenId = fleet.registerFleetLocal(UUID_1, US, ADMIN_CA, 2);
+ vm.prank(alice);
+ fleet.setOperator(UUID_1, bob);
+
+ // Operator burns the last registered token -> transitions to owned-only
+ vm.prank(bob);
+ fleet.burn(tokenId);
+
+ // Operator should still be bob
+ assertEq(fleet.operatorOf(UUID_1), bob);
+ assertTrue(fleet.isOwnedOnly(UUID_1));
+ }
+
+ function test_operatorNotChangedOnMultiRegionRegistration() public {
+ // Register first region and set operator
+ vm.prank(alice);
+ fleet.registerFleetLocal(UUID_1, US, ADMIN_CA, 0);
+ vm.prank(alice);
+ fleet.setOperator(UUID_1, bob);
+
+ uint256 bobBefore = bondToken.balanceOf(bob);
+
+ // Register second region - OPERATOR (bob) must call and pays tier bond
+ vm.prank(bob);
+ fleet.registerFleetLocal(UUID_1, US, ADMIN_NY, 2);
+
+ assertEq(fleet.operatorOf(UUID_1), bob);
+
+ // Bob pays full tier bond for new region
+ uint256 tierBond = fleet.tierBond(2, false);
+ assertEq(bondToken.balanceOf(bob), bobBefore - tierBond);
+ }
+
+ function test_freshRegistration_ownerIsOperator() public {
+ // Fresh registration without claim - owner pays BASE_BOND + tierBond
+ uint256 aliceBefore = bondToken.balanceOf(alice);
+
+ vm.prank(alice);
+ fleet.registerFleetLocal(UUID_1, US, ADMIN_CA, 2);
+
+ // Owner is operator when fresh registration
+ assertEq(fleet.operatorOf(UUID_1), alice);
+ assertEq(fleet.uuidOperator(UUID_1), address(0)); // stored as 0
+
+ // Owner paid BASE_BOND + tierBond
+ uint256 fullBond = BASE_BOND + fleet.tierBond(2, false);
+ assertEq(bondToken.balanceOf(alice), aliceBefore - fullBond);
+ }
+
+ function test_RevertIf_setOperator_notRegistered() public {
+ // UUID not registered at all - uuidOwner is address(0), so NotUuidOwner reverts first
+ vm.prank(alice);
+ vm.expectRevert(FleetIdentityUpgradeable.NotUuidOwner.selector);
+ fleet.setOperator(UUID_1, bob);
+ }
+
+ function test_operatorCanPromoteAfterOwnerTransfersOwnedToken() public {
+ // Alice claims with bob as operator
+ vm.prank(alice);
+ fleet.claimUuid(UUID_1, bob);
+
+ uint256 ownedTokenId = uint256(uint128(UUID_1));
+
+ // Alice transfers owned token to carol
+ vm.prank(alice);
+ fleet.transferFrom(alice, carol, ownedTokenId);
+
+ // Carol is now owner (uuidOwner transferred with token)
+ assertEq(fleet.uuidOwner(UUID_1), carol);
+
+ // Bob (operator) registers - only operator can register owned UUID
+ uint256 bobBefore = bondToken.balanceOf(bob);
+ vm.prank(bob);
+ fleet.registerFleetLocal(UUID_1, US, ADMIN_CA, 1);
+
+ // Bob paid full tier bond (owner already paid BASE_BOND via claim)
+ uint256 tierBond = fleet.tierBond(1, false);
+ assertEq(bondToken.balanceOf(bob), bobBefore - tierBond);
+
+ // Bob can promote as operator
+ uint256 tokenId = fleet.computeTokenId(UUID_1, fleet.makeAdminRegion(US, ADMIN_CA));
+
+ vm.prank(bob);
+ fleet.promote(tokenId);
+
+ assertEq(fleet.fleetTier(tokenId), 2);
+ }
+
+ function test_burnWithOperator_transitionsToOwnedOnly() public {
+ // Fresh registration at tier 2
+ vm.prank(alice);
+ uint256 tokenId = fleet.registerFleetLocal(UUID_1, US, ADMIN_CA, 2);
+
+ // Set operator
+ vm.prank(alice);
+ fleet.setOperator(UUID_1, bob);
+
+ uint256 bobBefore = bondToken.balanceOf(bob);
+
+ // OPERATOR burns -> transitions to owned-only, bob gets tier bond refund
+ vm.prank(bob);
+ fleet.burn(tokenId);
+
+ assertEq(bondToken.balanceOf(bob), bobBefore + fleet.tierBond(2, false));
+
+ // Owned-only token minted to owner
+ uint256 ownedTokenId = uint256(uint128(UUID_1));
+ assertEq(fleet.ownerOf(ownedTokenId), alice);
+ assertTrue(fleet.isOwnedOnly(UUID_1));
+ }
+
+ // ══════════════════════════════════════════════
+ // Bond Parameter Tests (setBaseBond, setCountryBondMultiplier)
+ // ══════════════════════════════════════════════
+
+ function test_setBaseBond_onlyOwner() public {
+ vm.prank(alice);
+ vm.expectRevert();
+ fleet.setBaseBond(200 ether);
+ }
+
+ function test_setBaseBond_emitsEvent() public {
+ vm.prank(owner);
+ vm.expectEmit(true, true, false, false, address(fleet));
+ emit BaseBondUpdated(BASE_BOND, 200 ether);
+ fleet.setBaseBond(200 ether);
+ }
+
+ function test_setBaseBond_revertsOnZero() public {
+ vm.prank(owner);
+ vm.expectRevert(FleetIdentityUpgradeable.InvalidBaseBond.selector);
+ fleet.setBaseBond(0);
+ }
+
+ function test_setBaseBond_updatesValue() public {
+ vm.prank(owner);
+ fleet.setBaseBond(200 ether);
+ assertEq(fleet.BASE_BOND(), 200 ether);
+ }
+
+ function test_setCountryBondMultiplier_onlyOwner() public {
+ vm.prank(alice);
+ vm.expectRevert();
+ fleet.setCountryBondMultiplier(32);
+ }
+
+ function test_setCountryBondMultiplier_emitsEvent() public {
+ vm.prank(owner);
+ vm.expectEmit(true, true, false, false, address(fleet));
+ emit CountryMultiplierUpdated(16, 32);
+ fleet.setCountryBondMultiplier(32);
+ }
+
+ function test_setCountryBondMultiplier_revertsOnZero() public {
+ vm.prank(owner);
+ vm.expectRevert(FleetIdentityUpgradeable.InvalidMultiplier.selector);
+ fleet.setCountryBondMultiplier(0);
+ }
+
+ function test_setCountryBondMultiplier_updatesValue() public {
+ vm.prank(owner);
+ fleet.setCountryBondMultiplier(32);
+ assertEq(fleet.countryBondMultiplier(), 32);
+ }
+
+ function test_countryBondMultiplier_defaultsTo16() public view {
+ // Fresh contract should return default 16
+ assertEq(fleet.countryBondMultiplier(), 16);
+ }
+
+ function test_setBondParameters_updatesBoth() public {
+ vm.prank(owner);
+ fleet.setBondParameters(200 ether, 32);
+ assertEq(fleet.BASE_BOND(), 200 ether);
+ assertEq(fleet.countryBondMultiplier(), 32);
+ }
+
+ function test_setBondParameters_onlyOwner() public {
+ vm.prank(alice);
+ vm.expectRevert();
+ fleet.setBondParameters(200 ether, 32);
+ }
+
+ function test_setBondParameters_revertsOnZeroBaseBond() public {
+ vm.prank(owner);
+ vm.expectRevert(FleetIdentityUpgradeable.InvalidBaseBond.selector);
+ fleet.setBondParameters(0, 32);
+ }
+
+ function test_setBondParameters_revertsOnZeroMultiplier() public {
+ vm.prank(owner);
+ vm.expectRevert(FleetIdentityUpgradeable.InvalidMultiplier.selector);
+ fleet.setBondParameters(200 ether, 0);
+ }
+
+ function test_setBondParameters_emitsBothEvents() public {
+ vm.prank(owner);
+ vm.expectEmit(true, true, false, false, address(fleet));
+ emit BaseBondUpdated(BASE_BOND, 200 ether);
+ vm.expectEmit(true, true, false, false, address(fleet));
+ emit CountryMultiplierUpdated(16, 32);
+ fleet.setBondParameters(200 ether, 32);
+ }
+
+ event BaseBondUpdated(uint256 indexed oldBaseBond, uint256 indexed newBaseBond);
+ event CountryMultiplierUpdated(uint256 indexed oldMultiplier, uint256 indexed newMultiplier);
+
+ // ══════════════════════════════════════════════
+ // Bond Snapshot Tests
+ // ══════════════════════════════════════════════
+
+ function test_snapshot_burnRefundsAtOriginalRate() public {
+ // Register at original BASE_BOND (100 ether)
+ vm.prank(alice);
+ uint256 tokenId = fleet.registerFleetLocal(UUID_1, US, ADMIN_CA, 0);
+
+ // Change baseBond to 200 ether
+ vm.prank(owner);
+ fleet.setBaseBond(200 ether);
+
+ // Burn should refund at original rate (100 ether tier 0 bond)
+ uint256 balanceBefore = bondToken.balanceOf(alice);
+ vm.prank(alice);
+ fleet.burn(tokenId);
+
+ uint256 expectedRefund = 100 ether; // Original tier 0 bond
+ assertEq(bondToken.balanceOf(alice), balanceBefore + expectedRefund);
+ }
+
+ function test_snapshot_promoteAfterBaseBondIncrease() public {
+ // Register at tier 0 with BASE_BOND = 100 ether
+ vm.prank(alice);
+ uint256 tokenId = fleet.registerFleetLocal(UUID_1, US, ADMIN_CA, 0);
+
+ // Change baseBond to 200 ether
+ vm.prank(owner);
+ fleet.setBaseBond(200 ether);
+
+ // Promote to tier 1 using reassignTier
+ // Current tier 1 bond at new rate = 200 << 1 = 400 ether
+ // Old tier 0 bond from snapshot = 100 ether
+ // Additional = 400 - 100 = 300 ether
+ uint256 balanceBefore = bondToken.balanceOf(alice);
+ vm.prank(alice);
+ fleet.reassignTier(tokenId, 1);
+
+ assertEq(bondToken.balanceOf(alice), balanceBefore - 300 ether);
+ }
+
+ function test_snapshot_demoteAfterPromoteUsesNewSnapshot() public {
+ // Register at tier 0 with BASE_BOND = 100 ether
+ vm.prank(alice);
+ uint256 tokenId = fleet.registerFleetLocal(UUID_1, US, ADMIN_CA, 0);
+
+ // Change baseBond to 200 ether
+ vm.prank(owner);
+ fleet.setBaseBond(200 ether);
+
+ // Promote to tier 1 - this updates the snapshot to new params
+ vm.prank(alice);
+ fleet.reassignTier(tokenId, 1);
+
+ // Demote back to tier 0
+ // Snapshot was updated on promote, so:
+ // Current (snapshot) tier 1 = 200 << 1 = 400 ether
+ // Target (snapshot) tier 0 = 200 << 0 = 200 ether
+ // Refund = 400 - 200 = 200 ether
+ uint256 balanceBefore = bondToken.balanceOf(alice);
+ vm.prank(alice);
+ fleet.reassignTier(tokenId, 0);
+
+ assertEq(bondToken.balanceOf(alice), balanceBefore + 200 ether);
+ }
+
+ function test_snapshot_claimUuidRefundsOriginalRate() public {
+ // Claim UUID at original BASE_BOND (100 ether)
+ vm.prank(alice);
+ uint256 tokenId = fleet.claimUuid(UUID_1, address(0));
+
+ // Change baseBond to 200 ether
+ vm.prank(owner);
+ fleet.setBaseBond(200 ether);
+
+ // Burn owned token should refund at original rate
+ uint256 balanceBefore = bondToken.balanceOf(alice);
+ vm.prank(alice);
+ fleet.burn(tokenId);
+
+ assertEq(bondToken.balanceOf(alice), balanceBefore + 100 ether);
+ }
+
+ function test_snapshot_countryMultiplierChangeRefund() public {
+ // Register country fleet at original multiplier (16)
+ vm.prank(alice);
+ uint256 tokenId = fleet.registerFleetCountry(UUID_1, US, 0);
+
+ // Original bond: 100 ether * 16 = 1600 ether + 100 ownership = 1700 total
+ // Change multiplier to 32
+ vm.prank(owner);
+ fleet.setCountryBondMultiplier(32);
+
+ // Burn should refund at original multiplier
+ uint256 balanceBefore = bondToken.balanceOf(alice);
+ vm.prank(alice);
+ fleet.burn(tokenId);
+
+ // Tier bond refund should be 1600 ether (original)
+ assertEq(bondToken.balanceOf(alice), balanceBefore + 1600 ether);
+ }
+
+ function test_snapshot_newRegistrationUsesCurrentParams() public {
+ // Change params before registration
+ vm.prank(owner);
+ fleet.setBaseBond(200 ether);
+ vm.prank(owner);
+ fleet.setCountryBondMultiplier(32);
+
+ uint256 balanceBefore = bondToken.balanceOf(alice);
+
+ vm.prank(alice);
+ fleet.registerFleetLocal(UUID_1, US, ADMIN_CA, 0);
+
+ // Should pull: 200 ether (ownership) + 200 ether (tier 0) = 400 ether
+ assertEq(bondToken.balanceOf(alice), balanceBefore - 400 ether);
+ }
+
+ // ══════════════════════════════════════════════
+ // Region Index Tests
+ // ══════════════════════════════════════════════
+
+ function test_getCountryAdminAreas_returnsRegisteredAreas() public {
+ vm.prank(alice);
+ fleet.registerFleetLocal(UUID_1, US, ADMIN_CA, 0);
+
+ vm.prank(alice);
+ fleet.registerFleetLocal(UUID_2, US, ADMIN_NY, 0);
+
+ uint32[] memory areas = fleet.getCountryAdminAreas(US);
+ assertEq(areas.length, 2);
+
+ // Areas should contain both admin area region keys
+ uint32 caRegion = fleet.makeAdminRegion(US, ADMIN_CA);
+ uint32 nyRegion = fleet.makeAdminRegion(US, ADMIN_NY);
+
+ bool foundCA = false;
+ bool foundNY = false;
+ for (uint256 i = 0; i < areas.length; i++) {
+ if (areas[i] == caRegion) foundCA = true;
+ if (areas[i] == nyRegion) foundNY = true;
+ }
+ assertTrue(foundCA);
+ assertTrue(foundNY);
+ }
+
+ function test_getCountryAdminAreas_emptyForNoRegistrations() public view {
+ uint32[] memory areas = fleet.getCountryAdminAreas(JP);
+ assertEq(areas.length, 0);
+ }
+}
diff --git a/test/FleetIdentityFairness.t.sol b/test/FleetIdentityFairness.t.sol
new file mode 100644
index 00000000..bcc1bca7
--- /dev/null
+++ b/test/FleetIdentityFairness.t.sol
@@ -0,0 +1,575 @@
+// SPDX-License-Identifier: MIT
+pragma solidity ^0.8.24;
+
+import {Test} from "forge-std/Test.sol";
+import {ERC1967Proxy} from "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol";
+import {FleetIdentityUpgradeable} from "../src/swarms/FleetIdentityUpgradeable.sol";
+import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol";
+
+/// @dev Minimal ERC-20 mock with public mint for testing.
+contract MockERC20Fairness is ERC20 {
+ constructor() ERC20("Mock Bond Token", "MBOND") {}
+
+ function mint(address to, uint256 amount) external {
+ _mint(to, amount);
+ }
+}
+
+/**
+ * @title FleetIdentityFairness Tests
+ * @notice Economic fairness analysis for FleetIdentity bundle allocation.
+ *
+ * @dev **Fairness Philosophy - Economic Advantage Model**
+ *
+ * The FleetIdentity contract uses a simple tier-descent algorithm:
+ * - Iterate from highest tier to lowest
+ * - At each tier: include local fleets first, then country fleets
+ * - Stop when bundle is full (20 slots)
+ *
+ * **Economic Fairness via COUNTRY_BOND_MULTIPLIER (16×)**
+ *
+ * Country fleets pay 16× more than local fleets at the same tier:
+ * - Local tier 0: BASE_BOND * 1 = 100 NODL
+ * - Country tier 0: BASE_BOND * 16 = 1600 NODL
+ * - Local tier 4: BASE_BOND * 16 = 1600 NODL (same cost!)
+ *
+ * This means a local player can reach tier 4 for the same cost a country player
+ * pays for tier 0. The 16× multiplier provides significant economic advantage to locals:
+ *
+ * | Tier | Local Bond | Country Bond | Country Overpay vs Local Same Tier |
+ * |------|------------|--------------|-----------------------------------|
+ * | 0 | 100 NODL | 1600 NODL | 16× |
+ * | 1 | 200 NODL | 3200 NODL | 16× |
+ * | 2 | 400 NODL | 6400 NODL | 16× |
+ * | 3 | 800 NODL | 12800 NODL | 16× |
+ *
+ * **Priority Rules**
+ *
+ * 1. Higher tier always wins (regardless of level)
+ * 2. Within same tier: local beats country
+ * 3. Within same tier + level: earlier registration wins
+ *
+ * **Whale Attack Analysis**
+ *
+ * A country whale trying to dominate must pay significantly more:
+ * - To fill 10 country slots at tier 3: 10 × 12800 NODL = 128,000 NODL
+ * - 10 locals could counter at tier 3 for: 10 × 800 NODL = 8,000 NODL
+ * - Whale pays 16× more to compete at the same tier level
+ */
+contract FleetIdentityFairnessTest is Test {
+ MockERC20Fairness bondToken;
+
+ // Test addresses representing different market participants
+ address[] localPlayers;
+ address[] countryPlayers;
+ address whale;
+
+ uint256 constant BASE_BOND = 100 ether;
+ uint256 constant NUM_LOCAL_PLAYERS = 20;
+ uint256 constant NUM_COUNTRY_PLAYERS = 10;
+
+ // Test country and admin areas
+ uint16 constant COUNTRY_US = 840;
+ uint16[] adminAreas;
+ uint256 constant NUM_ADMIN_AREAS = 5;
+
+ function setUp() public {
+ bondToken = new MockERC20Fairness();
+
+ // Create test players
+ whale = address(0xABCDEF);
+ for (uint256 i = 0; i < NUM_LOCAL_PLAYERS; i++) {
+ localPlayers.push(address(uint160(0x1000 + i)));
+ }
+ for (uint256 i = 0; i < NUM_COUNTRY_PLAYERS; i++) {
+ countryPlayers.push(address(uint160(0x2000 + i)));
+ }
+
+ // Create admin areas
+ for (uint16 i = 1; i <= NUM_ADMIN_AREAS; i++) {
+ adminAreas.push(i);
+ }
+
+ // Fund all players generously
+ uint256 funding = 1_000_000_000_000 ether;
+ bondToken.mint(whale, funding);
+ for (uint256 i = 0; i < NUM_LOCAL_PLAYERS; i++) {
+ bondToken.mint(localPlayers[i], funding);
+ }
+ for (uint256 i = 0; i < NUM_COUNTRY_PLAYERS; i++) {
+ bondToken.mint(countryPlayers[i], funding);
+ }
+ }
+
+ // ══════════════════════════════════════════════════════════════════════════════════
+ // Helper Functions
+ // ══════════════════════════════════════════════════════════════════════════════════
+
+ address fleetOwner = address(0x1111);
+
+ function _deployFleet() internal returns (FleetIdentityUpgradeable) {
+ // Deploy implementation
+ FleetIdentityUpgradeable impl = new FleetIdentityUpgradeable();
+
+ // Deploy proxy with initialize call
+ ERC1967Proxy proxy = new ERC1967Proxy(
+ address(impl),
+ abi.encodeCall(FleetIdentityUpgradeable.initialize, (fleetOwner, address(bondToken), BASE_BOND, 0))
+ );
+
+ // Cast proxy to contract type
+ FleetIdentityUpgradeable fleet = FleetIdentityUpgradeable(address(proxy));
+
+ // Approve all players
+ vm.prank(whale);
+ bondToken.approve(address(fleet), type(uint256).max);
+ for (uint256 i = 0; i < localPlayers.length; i++) {
+ vm.prank(localPlayers[i]);
+ bondToken.approve(address(fleet), type(uint256).max);
+ }
+ for (uint256 i = 0; i < countryPlayers.length; i++) {
+ vm.prank(countryPlayers[i]);
+ bondToken.approve(address(fleet), type(uint256).max);
+ }
+
+ return fleet;
+ }
+
+ function _uuid(uint256 seed) internal pure returns (bytes16) {
+ return bytes16(keccak256(abi.encodePacked("fleet-fairness-", seed)));
+ }
+
+ function _makeAdminRegion(uint16 cc, uint16 admin) internal pure returns (uint32) {
+ return (uint32(cc) << 10) | uint32(admin);
+ }
+
+ /// @dev Count how many slots in a bundle are from country vs local registrations
+ function _countBundleComposition(FleetIdentityUpgradeable fleet, uint16 cc, uint16 admin)
+ internal
+ view
+ returns (uint256 localCount, uint256 countryCount)
+ {
+ (bytes16[] memory uuids, uint256 count) = fleet.buildHighestBondedUuidBundle(cc, admin);
+ uint32 countryRegion = uint32(cc);
+
+ for (uint256 i = 0; i < count; i++) {
+ // Try to find token in country region first
+ uint256 countryTokenId = fleet.computeTokenId(uuids[i], countryRegion);
+ try fleet.ownerOf(countryTokenId) returns (address) {
+ countryCount++;
+ } catch {
+ localCount++;
+ }
+ }
+ }
+
+ // ══════════════════════════════════════════════════════════════════════════════════
+ // Scenario Tests: Priority & Economic Behavior
+ // ══════════════════════════════════════════════════════════════════════════════════
+
+ /**
+ * @notice Scenario A: Local-Heavy Market
+ * Many local players competing, few country players.
+ * Tests that locals correctly fill slots by tier-descent priority.
+ */
+ function test_scenarioA_localHeavyMarket() public {
+ FleetIdentityUpgradeable fleet = _deployFleet();
+ uint16 targetAdmin = adminAreas[0];
+
+ // 16 local players at tiers 0-3 (4 per tier due to TIER_CAPACITY)
+ for (uint256 i = 0; i < 16; i++) {
+ vm.prank(localPlayers[i % NUM_LOCAL_PLAYERS]);
+ fleet.registerFleetLocal(_uuid(1000 + i), COUNTRY_US, targetAdmin, i / 4);
+ }
+
+ // 4 country players at tier 0
+ for (uint256 i = 0; i < 4; i++) {
+ vm.prank(countryPlayers[i]);
+ fleet.registerFleetCountry(_uuid(2000 + i), COUNTRY_US, 0);
+ }
+
+ (uint256 localCount, uint256 countryCount) = _countBundleComposition(fleet, COUNTRY_US, targetAdmin);
+ (, uint256 totalCount) = fleet.buildHighestBondedUuidBundle(COUNTRY_US, targetAdmin);
+
+ emit log_string("=== Scenario A: Local-Heavy Market ===");
+ emit log_named_uint("Total bundle size", totalCount);
+ emit log_named_uint("Local slots used", localCount);
+ emit log_named_uint("Country slots used", countryCount);
+
+ // With tier-descent priority, all 16 locals fill first, then 4 country
+ assertEq(localCount, 16, "All 16 locals should be included");
+ assertEq(countryCount, 4, "All 4 country should fill remaining slots");
+ assertEq(totalCount, 20, "Bundle should be full");
+ }
+
+ /**
+ * @notice Scenario B: Country-Heavy Market
+ * Few local players, many country players at higher tiers.
+ * Tests that higher-tier country beats lower-tier local.
+ */
+ function test_scenarioB_countryHighTierDominance() public {
+ FleetIdentityUpgradeable fleet = _deployFleet();
+ uint16 targetAdmin = adminAreas[0];
+
+ // 4 local players at tier 0
+ for (uint256 i = 0; i < 4; i++) {
+ vm.prank(localPlayers[i]);
+ fleet.registerFleetLocal(_uuid(1000 + i), COUNTRY_US, targetAdmin, 0);
+ }
+
+ // 12 country players at tiers 1-3 (4 per tier)
+ // These are at HIGHER tiers, so they come first in bundle
+ for (uint256 i = 0; i < 12; i++) {
+ vm.prank(countryPlayers[i % NUM_COUNTRY_PLAYERS]);
+ fleet.registerFleetCountry(_uuid(2000 + i), COUNTRY_US, (i / 4) + 1);
+ }
+
+ (uint256 localCount, uint256 countryCount) = _countBundleComposition(fleet, COUNTRY_US, targetAdmin);
+ (, uint256 totalCount) = fleet.buildHighestBondedUuidBundle(COUNTRY_US, targetAdmin);
+
+ emit log_string("=== Scenario B: Country High-Tier Dominance ===");
+ emit log_named_uint("Total bundle size", totalCount);
+ emit log_named_uint("Local slots used", localCount);
+ emit log_named_uint("Country slots used", countryCount);
+
+ // Country at tiers 1-3 comes before locals at tier 0
+ assertEq(countryCount, 12, "All 12 country (higher tiers) included first");
+ assertEq(localCount, 4, "Tier-0 locals fill remaining slots");
+ assertEq(totalCount, 16, "Total should equal all registered fleets");
+ }
+
+ /**
+ * @notice Scenario C: Same-Tier Competition
+ * Locals and country at the same tier.
+ * Tests that locals get priority within the same tier.
+ */
+ function test_scenarioC_sameTierLocalPriority() public {
+ FleetIdentityUpgradeable fleet = _deployFleet();
+ uint16 targetAdmin = adminAreas[0];
+
+ // 4 local at tier 0
+ for (uint256 i = 0; i < 4; i++) {
+ vm.prank(localPlayers[i]);
+ fleet.registerFleetLocal(_uuid(1000 + i), COUNTRY_US, targetAdmin, 0);
+ }
+
+ // 4 country at tier 0 (same tier)
+ for (uint256 i = 0; i < 4; i++) {
+ vm.prank(countryPlayers[i]);
+ fleet.registerFleetCountry(_uuid(2000 + i), COUNTRY_US, 0);
+ }
+
+ (bytes16[] memory uuids, uint256 count) = fleet.buildHighestBondedUuidBundle(COUNTRY_US, targetAdmin);
+
+ emit log_string("=== Scenario C: Same-Tier Local Priority ===");
+ emit log_named_uint("Total bundle size", count);
+
+ // First 4 should be locals (priority within same tier)
+ for (uint256 i = 0; i < 4; i++) {
+ assertEq(uuids[i], _uuid(1000 + i), "Locals should come first");
+ }
+ // Next 4 should be country
+ for (uint256 i = 0; i < 4; i++) {
+ assertEq(uuids[4 + i], _uuid(2000 + i), "Country should follow locals");
+ }
+ }
+
+ /**
+ * @notice Scenario D: Country Whale at High Tier
+ * Single whale registers many high-tier country fleets.
+ * Tests that whale can dominate IF they outbid locals on tier level.
+ */
+ function test_scenarioD_countryWhaleHighTier() public {
+ FleetIdentityUpgradeable fleet = _deployFleet();
+ uint16 targetAdmin = adminAreas[0];
+
+ // 12 locals at tiers 0-2 (4 per tier)
+ for (uint256 i = 0; i < 12; i++) {
+ vm.prank(localPlayers[i]);
+ fleet.registerFleetLocal(_uuid(1000 + i), COUNTRY_US, targetAdmin, i / 4);
+ }
+
+ // Whale registers 8 country fleets at tiers 3-4 (4 per tier due to TIER_CAPACITY)
+ // This is above all locals (tiers 0-2)
+ for (uint256 i = 0; i < 8; i++) {
+ vm.prank(whale);
+ fleet.registerFleetCountry(_uuid(3000 + i), COUNTRY_US, 3 + (i / 4));
+ }
+
+ (, uint256 count) = fleet.buildHighestBondedUuidBundle(COUNTRY_US, targetAdmin);
+ (uint256 localCount, uint256 countryCount) = _countBundleComposition(fleet, COUNTRY_US, targetAdmin);
+
+ emit log_string("=== Scenario D: Country Whale at High Tier ===");
+ emit log_named_uint("Total bundle size", count);
+ emit log_named_uint("Local slots", localCount);
+ emit log_named_uint("Country slots", countryCount);
+
+ // Whale's tier-3/4 country fleets come first (highest tiers)
+ // Then locals at tiers 0-2 fill remaining slots
+ assertEq(countryCount, 8, "Whale's 8 high-tier country fleets included");
+ assertEq(localCount, 12, "All 12 locals at lower tiers included");
+ assertEq(count, 20, "Bundle full");
+ }
+
+ /**
+ * @notice Scenario E: Locals Counter Whale by Matching Tier
+ * Shows that locals can economically counter a country whale.
+ */
+ function test_scenarioE_localsCounterWhale() public {
+ FleetIdentityUpgradeable fleet = _deployFleet();
+ uint16 targetAdmin = adminAreas[0];
+
+ // Whale registers 4 country fleets at tier 3
+ // Cost: 4 × (BASE_BOND × 8 × 8) = 4 × 6400 = 25,600 NODL
+ for (uint256 i = 0; i < 4; i++) {
+ vm.prank(whale);
+ fleet.registerFleetCountry(_uuid(3000 + i), COUNTRY_US, 3);
+ }
+
+ // 4 locals match at tier 3 (same priority, but cheaper!)
+ // Cost: 4 × (BASE_BOND × 8) = 4 × 800 = 3,200 NODL
+ for (uint256 i = 0; i < 4; i++) {
+ vm.prank(localPlayers[i]);
+ fleet.registerFleetLocal(_uuid(1000 + i), COUNTRY_US, targetAdmin, 3);
+ }
+
+ (bytes16[] memory uuids, uint256 count) = fleet.buildHighestBondedUuidBundle(COUNTRY_US, targetAdmin);
+
+ emit log_string("=== Scenario E: Locals Counter Whale ===");
+ emit log_named_uint("Total bundle size", count);
+
+ // Locals get priority at tier 3 (same tier, local-first)
+ for (uint256 i = 0; i < 4; i++) {
+ assertEq(uuids[i], _uuid(1000 + i), "Locals come first at same tier");
+ }
+ for (uint256 i = 0; i < 4; i++) {
+ assertEq(uuids[4 + i], _uuid(3000 + i), "Country follows at same tier");
+ }
+
+ // Calculate cost ratio
+ uint256 whaleCost = 4 * fleet.tierBond(3, true); // 25,600 NODL
+ uint256 localCost = 4 * fleet.tierBond(3, false); // 3,200 NODL
+
+ emit log_named_uint("Whale total cost (ether)", whaleCost / 1 ether);
+ emit log_named_uint("Locals total cost (ether)", localCost / 1 ether);
+ emit log_named_uint("Whale overpay factor", whaleCost / localCost);
+
+ assertEq(whaleCost / localCost, 16, "Whale pays 16x more for same tier");
+ }
+
+ // ══════════════════════════════════════════════════════════════════════════════════
+ // Economic Metrics & Analysis
+ // ══════════════════════════════════════════════════════════════════════════════════
+
+ /**
+ * @notice Verify the 16× economic advantage constants.
+ */
+ function test_economicAdvantage_8xMultiplier() public {
+ FleetIdentityUpgradeable fleet = _deployFleet();
+
+ // Verify multiplier
+ assertEq(fleet.countryBondMultiplier(), 16, "Multiplier should be 16");
+
+ // At every tier, country pays exactly 16× local
+ for (uint256 tier = 0; tier < 6; tier++) {
+ uint256 localBond = fleet.tierBond(tier, false);
+ uint256 countryBond = fleet.tierBond(tier, true);
+ assertEq(countryBond, localBond * 16, "Country should pay 16x at every tier");
+ }
+ }
+
+ /**
+ * @notice Demonstrate that a local at tier N+4 costs the same as country at tier N.
+ */
+ function test_economicAdvantage_localTierEquivalence() public {
+ FleetIdentityUpgradeable fleet = _deployFleet();
+
+ // Local tier 4 = Country tier 0 (2^4 = 16)
+ assertEq(
+ fleet.tierBond(4, false),
+ fleet.tierBond(0, true),
+ "Local tier 4 should equal country tier 0"
+ );
+
+ // Local tier 5 = Country tier 1
+ assertEq(
+ fleet.tierBond(5, false),
+ fleet.tierBond(1, true),
+ "Local tier 5 should equal country tier 1"
+ );
+
+ // Local tier 6 = Country tier 2
+ assertEq(
+ fleet.tierBond(6, false),
+ fleet.tierBond(2, true),
+ "Local tier 6 should equal country tier 2"
+ );
+
+ emit log_string("=== Local Tier Equivalence ===");
+ emit log_string("Local tier N+4 costs the same as Country tier N");
+ emit log_string("This gives locals a 4-tier economic advantage");
+ }
+
+ /**
+ * @notice Analyze country registration efficiency across admin areas.
+ */
+ function test_economicAdvantage_multiRegionEfficiency() public {
+ FleetIdentityUpgradeable fleet = _deployFleet();
+
+ // Single country registration covers ALL admin areas
+ uint256 countryBond = fleet.tierBond(0, true); // 800 NODL
+
+ // To cover N admin areas locally, costs N × local_bond
+ uint256 localPerArea = fleet.tierBond(0, false); // 100 NODL
+
+ emit log_string("=== Multi-Region Efficiency Analysis ===");
+ emit log_named_uint("Country tier-0 bond (ether)", countryBond / 1 ether);
+ emit log_named_uint("Local tier-0 bond per area (ether)", localPerArea / 1 ether);
+
+ // Country is MORE efficient when covering > 8 admin areas
+ // Break-even: 8 local registrations = 1 country registration
+ uint256 breakEvenAreas = countryBond / localPerArea;
+ emit log_named_uint("Break-even admin areas", breakEvenAreas);
+
+ assertEq(breakEvenAreas, 16, "Country efficient for 16+ admin areas");
+ }
+
+ /**
+ * @notice Bond escalation analysis showing geometric growth.
+ */
+ function test_bondEscalationAnalysis() public {
+ FleetIdentityUpgradeable fleet = _deployFleet();
+
+ emit log_string("");
+ emit log_string("=== BOND ESCALATION ANALYSIS ===");
+ emit log_string("");
+ emit log_string("Tier | Local Bond (ether) | Country Bond (ether)");
+ emit log_string("-----+--------------------+---------------------");
+
+ for (uint256 tier = 0; tier <= 6; tier++) {
+ uint256 localBond = fleet.tierBond(tier, false);
+ uint256 countryBond = fleet.tierBond(tier, true);
+
+ // Verify geometric progression (2× per tier)
+ if (tier > 0) {
+ assertEq(localBond, fleet.tierBond(tier - 1, false) * 2, "Local should double each tier");
+ assertEq(countryBond, fleet.tierBond(tier - 1, true) * 2, "Country should double each tier");
+ }
+ }
+ }
+
+ // ══════════════════════════════════════════════════════════════════════════════════
+ // Invariant Tests
+ // ══════════════════════════════════════════════════════════════════════════════════
+
+ /**
+ * @notice CRITICAL: Core invariants that must ALWAYS hold.
+ */
+ function test_invariant_coreGuarantees() public {
+ FleetIdentityUpgradeable fleet = _deployFleet();
+
+ // Invariant 1: Country multiplier is exactly 16
+ assertEq(fleet.countryBondMultiplier(), 16, "INVARIANT: Country multiplier must be 16");
+
+ // Invariant 2: Tier capacity allows fair competition
+ assertEq(fleet.TIER_CAPACITY(), 10, "INVARIANT: Tier capacity must be 10");
+
+ // Invariant 3: Bundle size reasonable for discovery
+ assertEq(fleet.MAX_BONDED_UUID_BUNDLE_SIZE(), 20, "INVARIANT: Bundle size must be 20");
+
+ // Invariant 4: Bond doubles per tier (geometric)
+ for (uint256 t = 1; t <= 5; t++) {
+ assertEq(
+ fleet.tierBond(t, false),
+ fleet.tierBond(t - 1, false) * 2,
+ "INVARIANT: Bond must double per tier"
+ );
+ }
+
+ emit log_string("[PASS] All core invariants verified");
+ }
+
+ /**
+ * @notice Bundle always respects tier-descent priority.
+ */
+ function test_invariant_tierDescentPriority() public {
+ FleetIdentityUpgradeable fleet = _deployFleet();
+ uint16 targetAdmin = adminAreas[0];
+
+ // Mixed setup: locals at tier 1, country at tier 2
+ for (uint256 i = 0; i < 4; i++) {
+ vm.prank(localPlayers[i]);
+ fleet.registerFleetLocal(_uuid(1000 + i), COUNTRY_US, targetAdmin, 1);
+ }
+ for (uint256 i = 0; i < 4; i++) {
+ vm.prank(countryPlayers[i]);
+ fleet.registerFleetCountry(_uuid(2000 + i), COUNTRY_US, 2);
+ }
+
+ (bytes16[] memory uuids, uint256 count) = fleet.buildHighestBondedUuidBundle(COUNTRY_US, targetAdmin);
+
+ // Tier 2 (country) must come before tier 1 (local) - higher tier wins
+ for (uint256 i = 0; i < 4; i++) {
+ assertEq(uuids[i], _uuid(2000 + i), "INVARIANT: Higher tier must come first");
+ }
+ for (uint256 i = 0; i < 4; i++) {
+ assertEq(uuids[4 + i], _uuid(1000 + i), "Lower tier follows");
+ }
+
+ assertEq(count, 8);
+ }
+
+ // ══════════════════════════════════════════════════════════════════════════════════
+ // Fuzz Tests
+ // ══════════════════════════════════════════════════════════════════════════════════
+
+ /**
+ * @notice Fuzz test to verify bundle properties across random market conditions.
+ */
+ function testFuzz_bundleProperties(uint8 numLocals, uint8 numCountry) public {
+ // Bound inputs to reasonable ranges
+ numLocals = uint8(bound(numLocals, 1, 16));
+ numCountry = uint8(bound(numCountry, 1, 12));
+
+ FleetIdentityUpgradeable fleet = _deployFleet();
+ uint16 targetAdmin = adminAreas[0];
+
+ // Register local players (spread across tiers for variety)
+ for (uint256 i = 0; i < numLocals; i++) {
+ vm.prank(localPlayers[i % NUM_LOCAL_PLAYERS]);
+ fleet.registerFleetLocal(_uuid(8000 + i), COUNTRY_US, targetAdmin, i / 4);
+ }
+
+ // Register country players
+ for (uint256 i = 0; i < numCountry; i++) {
+ vm.prank(countryPlayers[i % NUM_COUNTRY_PLAYERS]);
+ fleet.registerFleetCountry(_uuid(9000 + i), COUNTRY_US, i / 4);
+ }
+
+ // Get bundle
+ (, uint256 count) = fleet.buildHighestBondedUuidBundle(COUNTRY_US, targetAdmin);
+
+ // Properties that must always hold:
+
+ // 1. Bundle never exceeds max size
+ assertLe(count, fleet.MAX_BONDED_UUID_BUNDLE_SIZE(), "Bundle must not exceed max");
+
+ // 2. Bundle includes as many as possible (up to registered count)
+ uint256 totalRegistered = uint256(numLocals) + uint256(numCountry);
+ uint256 expectedMax = totalRegistered < 20 ? totalRegistered : 20;
+ assertEq(count, expectedMax, "Bundle should maximize utilization");
+ }
+
+ /**
+ * @notice Fuzz that 16x multiplier always holds at any tier.
+ */
+ function testFuzz_constantMultiplier(uint8 tier) public {
+ tier = uint8(bound(tier, 0, 20));
+ FleetIdentityUpgradeable fleet = _deployFleet();
+
+ uint256 localBond = fleet.tierBond(tier, false);
+ uint256 countryBond = fleet.tierBond(tier, true);
+
+ assertEq(countryBond, localBond * 16, "16x multiplier must hold at all tiers");
+ }
+}
diff --git a/test/ServiceProvider.t.sol b/test/ServiceProvider.t.sol
new file mode 100644
index 00000000..d7f01b73
--- /dev/null
+++ b/test/ServiceProvider.t.sol
@@ -0,0 +1,171 @@
+// SPDX-License-Identifier: MIT
+pragma solidity ^0.8.24;
+
+import {Test} from "forge-std/Test.sol";
+import {ERC1967Proxy} from "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol";
+import {ServiceProviderUpgradeable} from "../src/swarms/ServiceProviderUpgradeable.sol";
+
+contract ServiceProviderTest is Test {
+ ServiceProviderUpgradeable provider;
+ address owner = address(0x1111);
+
+ address alice = address(0xA);
+ address bob = address(0xB);
+
+ string constant URL_1 = "https://backend.swarm.example.com/api/v1";
+ string constant URL_2 = "https://relay.nodle.network:8443";
+ string constant URL_3 = "https://provider.third.io";
+
+ event ProviderRegistered(address indexed owner, string url, uint256 indexed tokenId);
+ event ProviderBurned(address indexed owner, uint256 indexed tokenId);
+
+ function setUp() public {
+ // Deploy implementation
+ ServiceProviderUpgradeable impl = new ServiceProviderUpgradeable();
+
+ // Deploy proxy with initialize call
+ ERC1967Proxy proxy = new ERC1967Proxy(
+ address(impl),
+ abi.encodeCall(ServiceProviderUpgradeable.initialize, (owner))
+ );
+
+ // Cast proxy to contract type
+ provider = ServiceProviderUpgradeable(address(proxy));
+ }
+
+ // ==============================
+ // registerProvider
+ // ==============================
+
+ function test_registerProvider_mintsAndStoresURL() public {
+ vm.prank(alice);
+ uint256 tokenId = provider.registerProvider(URL_1);
+
+ assertEq(provider.ownerOf(tokenId), alice);
+ assertEq(keccak256(bytes(provider.providerUrls(tokenId))), keccak256(bytes(URL_1)));
+ }
+
+ function test_registerProvider_deterministicTokenId() public {
+ vm.prank(alice);
+ uint256 tokenId = provider.registerProvider(URL_1);
+
+ assertEq(tokenId, uint256(keccak256(bytes(URL_1))));
+ }
+
+ function test_registerProvider_emitsEvent() public {
+ uint256 expectedTokenId = uint256(keccak256(bytes(URL_1)));
+
+ vm.expectEmit(true, true, true, true);
+ emit ProviderRegistered(alice, URL_1, expectedTokenId);
+
+ vm.prank(alice);
+ provider.registerProvider(URL_1);
+ }
+
+ function test_registerProvider_multipleProviders() public {
+ vm.prank(alice);
+ uint256 id1 = provider.registerProvider(URL_1);
+
+ vm.prank(bob);
+ uint256 id2 = provider.registerProvider(URL_2);
+
+ assertEq(provider.ownerOf(id1), alice);
+ assertEq(provider.ownerOf(id2), bob);
+ assertTrue(id1 != id2);
+ }
+
+ function test_RevertIf_registerProvider_emptyURL() public {
+ vm.prank(alice);
+ vm.expectRevert(ServiceProviderUpgradeable.EmptyURL.selector);
+ provider.registerProvider("");
+ }
+
+ function test_RevertIf_registerProvider_duplicateURL() public {
+ vm.prank(alice);
+ provider.registerProvider(URL_1);
+
+ vm.prank(bob);
+ vm.expectRevert(); // ERC721: token already minted
+ provider.registerProvider(URL_1);
+ }
+
+ // ==============================
+ // burn
+ // ==============================
+
+ function test_burn_deletesURLAndToken() public {
+ vm.prank(alice);
+ uint256 tokenId = provider.registerProvider(URL_1);
+
+ vm.prank(alice);
+ provider.burn(tokenId);
+
+ // URL mapping cleared
+ assertEq(bytes(provider.providerUrls(tokenId)).length, 0);
+
+ // Token no longer exists
+ vm.expectRevert(); // ownerOf reverts for non-existent token
+ provider.ownerOf(tokenId);
+ }
+
+ function test_burn_emitsEvent() public {
+ vm.prank(alice);
+ uint256 tokenId = provider.registerProvider(URL_1);
+
+ vm.expectEmit(true, true, true, true);
+ emit ProviderBurned(alice, tokenId);
+
+ vm.prank(alice);
+ provider.burn(tokenId);
+ }
+
+ function test_RevertIf_burn_notOwner() public {
+ vm.prank(alice);
+ uint256 tokenId = provider.registerProvider(URL_1);
+
+ vm.prank(bob);
+ vm.expectRevert(ServiceProviderUpgradeable.NotTokenOwner.selector);
+ provider.burn(tokenId);
+ }
+
+ function test_burn_allowsReregistration() public {
+ vm.prank(alice);
+ uint256 tokenId = provider.registerProvider(URL_1);
+
+ vm.prank(alice);
+ provider.burn(tokenId);
+
+ // Same URL can now be registered by someone else
+ vm.prank(bob);
+ uint256 newTokenId = provider.registerProvider(URL_1);
+
+ assertEq(newTokenId, tokenId); // Same deterministic ID
+ assertEq(provider.ownerOf(newTokenId), bob);
+ }
+
+ // ==============================
+ // Fuzz Tests
+ // ==============================
+
+ function testFuzz_registerProvider_anyValidURL(string calldata url) public {
+ vm.assume(bytes(url).length > 0);
+
+ vm.prank(alice);
+ uint256 tokenId = provider.registerProvider(url);
+
+ assertEq(tokenId, uint256(keccak256(bytes(url))));
+ assertEq(provider.ownerOf(tokenId), alice);
+ }
+
+ function testFuzz_burn_onlyOwner(address caller) public {
+ vm.assume(caller != alice);
+ vm.assume(caller != address(0));
+
+ vm.prank(alice);
+ uint256 tokenId = provider.registerProvider(URL_1);
+
+ vm.prank(caller);
+ vm.expectRevert(ServiceProviderUpgradeable.NotTokenOwner.selector);
+ provider.burn(tokenId);
+ }
+}
diff --git a/test/SwarmRegistryL1.t.sol b/test/SwarmRegistryL1.t.sol
new file mode 100644
index 00000000..c8723002
--- /dev/null
+++ b/test/SwarmRegistryL1.t.sol
@@ -0,0 +1,1207 @@
+// SPDX-License-Identifier: MIT
+pragma solidity ^0.8.24;
+
+import {Test} from "forge-std/Test.sol";
+import {ERC1967Proxy} from "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol";
+import "../src/swarms/SwarmRegistryL1Upgradeable.sol";
+import {FleetIdentityUpgradeable} from "../src/swarms/FleetIdentityUpgradeable.sol";
+import {ServiceProviderUpgradeable} from "../src/swarms/ServiceProviderUpgradeable.sol";
+import {SwarmStatus, TagType, FingerprintSize} from "../src/swarms/interfaces/SwarmTypes.sol";
+import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol";
+
+contract MockBondTokenL1 is ERC20 {
+ constructor() ERC20("Mock Bond", "MBOND") {}
+
+ function mint(address to, uint256 amount) external {
+ _mint(to, amount);
+ }
+}
+
+contract SwarmRegistryL1Test is Test {
+ SwarmRegistryL1Upgradeable swarmRegistry;
+ FleetIdentityUpgradeable fleetContract;
+ ServiceProviderUpgradeable providerContract;
+ MockBondTokenL1 bondToken;
+
+ address contractOwner = address(0x1111);
+ address fleetOwner = address(0x1);
+ address providerOwner = address(0x2);
+ address caller = address(0x3);
+
+ uint256 constant FLEET_BOND = 100 ether;
+
+ // Region constants for fleet registration
+ uint16 constant US = 840;
+ uint16 constant ADMIN_CA = 6; // California
+
+ // Alias for FingerprintSize enum
+ FingerprintSize constant BITS_8 = FingerprintSize.BITS_8;
+ FingerprintSize constant BITS_16 = FingerprintSize.BITS_16;
+
+ event SwarmRegistered(
+ uint256 indexed swarmId, bytes16 indexed fleetUuid, uint256 indexed providerId, address owner
+ );
+ event SwarmStatusChanged(uint256 indexed swarmId, SwarmStatus status);
+ event SwarmProviderUpdated(uint256 indexed swarmId, uint256 indexed oldProvider, uint256 indexed newProvider);
+ event SwarmDeleted(uint256 indexed swarmId, bytes16 indexed fleetUuid, address indexed owner);
+ event SwarmPurged(uint256 indexed swarmId, bytes16 indexed fleetUuid, address indexed purgedBy);
+
+ function setUp() public {
+ bondToken = new MockBondTokenL1();
+
+ // Deploy FleetIdentity via proxy
+ FleetIdentityUpgradeable fleetImpl = new FleetIdentityUpgradeable();
+ ERC1967Proxy fleetProxy = new ERC1967Proxy(
+ address(fleetImpl),
+ abi.encodeCall(FleetIdentityUpgradeable.initialize, (contractOwner, address(bondToken), FLEET_BOND, 0))
+ );
+ fleetContract = FleetIdentityUpgradeable(address(fleetProxy));
+
+ // Deploy ServiceProvider via proxy
+ ServiceProviderUpgradeable providerImpl = new ServiceProviderUpgradeable();
+ ERC1967Proxy providerProxy = new ERC1967Proxy(
+ address(providerImpl), abi.encodeCall(ServiceProviderUpgradeable.initialize, (contractOwner))
+ );
+ providerContract = ServiceProviderUpgradeable(address(providerProxy));
+
+ // Deploy SwarmRegistry via proxy
+ SwarmRegistryL1Upgradeable registryImpl = new SwarmRegistryL1Upgradeable();
+ ERC1967Proxy registryProxy = new ERC1967Proxy(
+ address(registryImpl),
+ abi.encodeCall(
+ SwarmRegistryL1Upgradeable.initialize,
+ (address(fleetContract), address(providerContract), contractOwner)
+ )
+ );
+ swarmRegistry = SwarmRegistryL1Upgradeable(address(registryProxy));
+
+ // Fund fleet owner and approve
+ bondToken.mint(fleetOwner, 1_000_000 ether);
+ vm.prank(fleetOwner);
+ bondToken.approve(address(fleetContract), type(uint256).max);
+ }
+
+ // ==============================
+ // Helpers
+ // ==============================
+
+ function _registerFleet(address owner, bytes memory seed) internal returns (uint256) {
+ vm.prank(owner);
+ return fleetContract.registerFleetLocal(bytes16(keccak256(seed)), US, ADMIN_CA, 0);
+ }
+
+ function _getFleetUuid(uint256 fleetId) internal pure returns (bytes16) {
+ return bytes16(uint128(fleetId));
+ }
+
+ function _registerProvider(address owner, string memory url) internal returns (uint256) {
+ vm.prank(owner);
+ return providerContract.registerProvider(url);
+ }
+
+ function _registerSwarm(
+ address owner,
+ uint256 fleetId,
+ uint256 providerId,
+ bytes memory filter,
+ FingerprintSize fpSize,
+ TagType tagType
+ ) internal returns (uint256) {
+ bytes16 fleetUuid = _getFleetUuid(fleetId);
+ vm.prank(owner);
+ return swarmRegistry.registerSwarm(fleetUuid, providerId, filter, fpSize, tagType);
+ }
+
+ /// @dev Get expected hash indices and fingerprint for XOR filter verification
+ function getExpectedValues(bytes memory tagId, uint256 m, FingerprintSize fpSize)
+ public
+ pure
+ returns (uint32 h1, uint32 h2, uint32 h3, uint256 fp)
+ {
+ bytes32 h = keccak256(tagId);
+ h1 = uint32(uint256(h)) % uint32(m);
+ h2 = uint32(uint256(h) >> 32) % uint32(m);
+ h3 = uint32(uint256(h) >> 64) % uint32(m);
+ uint256 fpMask = fpSize == FingerprintSize.BITS_8 ? 0xFF : 0xFFFF;
+ fp = (uint256(h) >> 96) & fpMask;
+ }
+
+ function _write16Bit(bytes memory data, uint256 slotIndex, uint16 value) internal pure {
+ uint256 byteOffset = slotIndex * 2;
+ data[byteOffset] = bytes1(uint8(value >> 8));
+ data[byteOffset + 1] = bytes1(uint8(value));
+ }
+
+ function _write8Bit(bytes memory data, uint256 slotIndex, uint8 value) internal pure {
+ data[slotIndex] = bytes1(value);
+ }
+
+ // ==============================
+ // Constructor
+ // ==============================
+
+ function test_initialize_setsContracts() public view {
+ assertEq(address(swarmRegistry.FLEET_CONTRACT()), address(fleetContract));
+ assertEq(address(swarmRegistry.PROVIDER_CONTRACT()), address(providerContract));
+ }
+
+ function test_RevertIf_initialize_zeroFleetAddress() public {
+ SwarmRegistryL1Upgradeable impl = new SwarmRegistryL1Upgradeable();
+ vm.expectRevert(SwarmRegistryL1Upgradeable.InvalidSwarmData.selector);
+ new ERC1967Proxy(
+ address(impl),
+ abi.encodeCall(
+ SwarmRegistryL1Upgradeable.initialize, (address(0), address(providerContract), contractOwner)
+ )
+ );
+ }
+
+ function test_RevertIf_initialize_zeroProviderAddress() public {
+ SwarmRegistryL1Upgradeable impl = new SwarmRegistryL1Upgradeable();
+ vm.expectRevert(SwarmRegistryL1Upgradeable.InvalidSwarmData.selector);
+ new ERC1967Proxy(
+ address(impl),
+ abi.encodeCall(SwarmRegistryL1Upgradeable.initialize, (address(fleetContract), address(0), contractOwner))
+ );
+ }
+
+ function test_RevertIf_initialize_bothZero() public {
+ SwarmRegistryL1Upgradeable impl = new SwarmRegistryL1Upgradeable();
+ vm.expectRevert(SwarmRegistryL1Upgradeable.InvalidSwarmData.selector);
+ new ERC1967Proxy(
+ address(impl),
+ abi.encodeCall(SwarmRegistryL1Upgradeable.initialize, (address(0), address(0), contractOwner))
+ );
+ }
+
+ // ==============================
+ // registerSwarm — happy path
+ // ==============================
+
+ function test_registerSwarm_basicFlow() public {
+ uint256 fleetId = _registerFleet(fleetOwner, "my-fleet");
+ uint256 providerId = _registerProvider(providerOwner, "https://api.example.com");
+
+ uint256 swarmId =
+ _registerSwarm(fleetOwner, fleetId, providerId, new bytes(100), BITS_16, TagType.IBEACON_INCLUDES_MAC);
+
+ // Swarm ID is deterministic hash of (fleetUuid, filter, fpSize, tagType)
+ uint256 expectedId =
+ swarmRegistry.computeSwarmId(_getFleetUuid(fleetId), new bytes(100), BITS_16, TagType.IBEACON_INCLUDES_MAC);
+ assertEq(swarmId, expectedId);
+ }
+
+ function test_registerSwarm_storesMetadataCorrectly() public {
+ uint256 fleetId = _registerFleet(fleetOwner, "f1");
+ uint256 providerId = _registerProvider(providerOwner, "url1");
+
+ uint256 swarmId = _registerSwarm(fleetOwner, fleetId, providerId, new bytes(50), BITS_8, TagType.VENDOR_ID);
+
+ (
+ bytes16 storedFleetUuid,
+ uint256 storedProviderId,
+ address filterPointer,
+ FingerprintSize storedFpSize,
+ TagType storedTagType,
+ SwarmStatus storedStatus
+ ) = swarmRegistry.swarms(swarmId);
+
+ assertEq(storedFleetUuid, _getFleetUuid(fleetId));
+ assertEq(storedProviderId, providerId);
+ assertTrue(filterPointer != address(0));
+ assertEq(uint8(storedFpSize), uint8(BITS_8));
+ assertEq(uint8(storedTagType), uint8(TagType.VENDOR_ID));
+ assertEq(uint8(storedStatus), uint8(SwarmStatus.REGISTERED));
+ }
+
+ function test_registerSwarm_deterministicId() public {
+ uint256 fleetId = _registerFleet(fleetOwner, "f1");
+ uint256 providerId = _registerProvider(providerOwner, "url1");
+
+ bytes memory filter = new bytes(32);
+
+ uint256 expectedId = swarmRegistry.computeSwarmId(_getFleetUuid(fleetId), filter, BITS_8, TagType.EDDYSTONE_UID);
+
+ uint256 swarmId = _registerSwarm(fleetOwner, fleetId, providerId, filter, BITS_8, TagType.EDDYSTONE_UID);
+ assertEq(swarmId, expectedId);
+ }
+
+ function test_RevertIf_registerSwarm_duplicateSwarm() public {
+ uint256 fleetId = _registerFleet(fleetOwner, "f1");
+ uint256 providerId = _registerProvider(providerOwner, "url1");
+
+ _registerSwarm(fleetOwner, fleetId, providerId, new bytes(32), BITS_8, TagType.EDDYSTONE_UID);
+
+ vm.prank(fleetOwner);
+ vm.expectRevert(SwarmRegistryL1Upgradeable.SwarmAlreadyExists.selector);
+ swarmRegistry.registerSwarm(_getFleetUuid(fleetId), providerId, new bytes(32), BITS_8, TagType.EDDYSTONE_UID);
+ }
+
+ function test_registerSwarm_emitsSwarmRegistered() public {
+ uint256 fleetId = _registerFleet(fleetOwner, "f1");
+ uint256 providerId = _registerProvider(providerOwner, "url1");
+
+ bytes memory filter = new bytes(50);
+ uint256 expectedId =
+ swarmRegistry.computeSwarmId(_getFleetUuid(fleetId), filter, BITS_16, TagType.EDDYSTONE_UID);
+
+ vm.expectEmit(true, true, true, true);
+ emit SwarmRegistered(expectedId, _getFleetUuid(fleetId), providerId, fleetOwner);
+
+ _registerSwarm(fleetOwner, fleetId, providerId, filter, BITS_16, TagType.EDDYSTONE_UID);
+ }
+
+ function test_registerSwarm_linksUuidSwarms() public {
+ uint256 fleetId = _registerFleet(fleetOwner, "f1");
+ uint256 providerId1 = _registerProvider(providerOwner, "url1");
+ uint256 providerId2 = _registerProvider(providerOwner, "url2");
+
+ // Use different filters to create distinct swarms
+ bytes memory filter1 = new bytes(50);
+ filter1[0] = 0x01;
+ bytes memory filter2 = new bytes(50);
+ filter2[0] = 0x02;
+
+ uint256 swarmId1 = _registerSwarm(fleetOwner, fleetId, providerId1, filter1, BITS_8, TagType.EDDYSTONE_UID);
+ uint256 swarmId2 = _registerSwarm(fleetOwner, fleetId, providerId2, filter2, BITS_8, TagType.EDDYSTONE_UID);
+
+ assertEq(swarmRegistry.uuidSwarms(_getFleetUuid(fleetId), 0), swarmId1);
+ assertEq(swarmRegistry.uuidSwarms(_getFleetUuid(fleetId), 1), swarmId2);
+ }
+
+ function test_registerSwarm_allTagTypes() public {
+ uint256 fleetId1 = _registerFleet(fleetOwner, "f1");
+ uint256 fleetId2 = _registerFleet(fleetOwner, "f2");
+ uint256 fleetId3 = _registerFleet(fleetOwner, "f3");
+ uint256 fleetId4 = _registerFleet(fleetOwner, "f4");
+ uint256 providerId = _registerProvider(providerOwner, "url");
+
+ uint256 s1 =
+ _registerSwarm(fleetOwner, fleetId1, providerId, new bytes(32), BITS_8, TagType.IBEACON_PAYLOAD_ONLY);
+ uint256 s2 =
+ _registerSwarm(fleetOwner, fleetId2, providerId, new bytes(32), BITS_8, TagType.IBEACON_INCLUDES_MAC);
+ uint256 s3 = _registerSwarm(fleetOwner, fleetId3, providerId, new bytes(32), BITS_8, TagType.VENDOR_ID);
+ uint256 s4 = _registerSwarm(fleetOwner, fleetId4, providerId, new bytes(32), BITS_8, TagType.EDDYSTONE_UID);
+
+ (,,,, TagType t1,) = swarmRegistry.swarms(s1);
+ (,,,, TagType t2,) = swarmRegistry.swarms(s2);
+ (,,,, TagType t3,) = swarmRegistry.swarms(s3);
+ (,,,, TagType t4,) = swarmRegistry.swarms(s4);
+
+ assertEq(uint8(t1), uint8(TagType.IBEACON_PAYLOAD_ONLY));
+ assertEq(uint8(t2), uint8(TagType.IBEACON_INCLUDES_MAC));
+ assertEq(uint8(t3), uint8(TagType.VENDOR_ID));
+ assertEq(uint8(t4), uint8(TagType.EDDYSTONE_UID));
+ }
+
+ function test_registerSwarm_bothFingerprintSizes() public {
+ uint256 fleetId1 = _registerFleet(fleetOwner, "f1");
+ uint256 fleetId2 = _registerFleet(fleetOwner, "f2");
+ uint256 providerId = _registerProvider(providerOwner, "url");
+
+ uint256 s1 = _registerSwarm(fleetOwner, fleetId1, providerId, new bytes(32), BITS_8, TagType.EDDYSTONE_UID);
+ uint256 s2 = _registerSwarm(fleetOwner, fleetId2, providerId, new bytes(64), BITS_16, TagType.EDDYSTONE_UID);
+
+ (,,, FingerprintSize fp1,,) = swarmRegistry.swarms(s1);
+ (,,, FingerprintSize fp2,,) = swarmRegistry.swarms(s2);
+
+ assertEq(uint8(fp1), uint8(BITS_8));
+ assertEq(uint8(fp2), uint8(BITS_16));
+ }
+
+ // ==============================
+ // registerSwarm — reverts
+ // ==============================
+
+ function test_RevertIf_registerSwarm_notUuidOwner() public {
+ uint256 fleetId = _registerFleet(fleetOwner, "my-fleet");
+
+ vm.prank(caller);
+ vm.expectRevert(SwarmRegistryL1Upgradeable.NotUuidOwner.selector);
+ swarmRegistry.registerSwarm(_getFleetUuid(fleetId), 1, new bytes(10), BITS_16, TagType.EDDYSTONE_UID);
+ }
+
+ function test_RevertIf_registerSwarm_zeroUuid() public {
+ uint256 providerId = _registerProvider(providerOwner, "url1");
+
+ vm.prank(fleetOwner);
+ vm.expectRevert(SwarmRegistryL1Upgradeable.InvalidUuid.selector);
+ swarmRegistry.registerSwarm(bytes16(0), providerId, new bytes(32), BITS_8, TagType.EDDYSTONE_UID);
+ }
+
+ function test_RevertIf_registerSwarm_providerDoesNotExist() public {
+ uint256 fleetId = _registerFleet(fleetOwner, "f1");
+ uint256 nonExistentProvider = 12345;
+
+ vm.prank(fleetOwner);
+ vm.expectRevert(SwarmRegistryL1Upgradeable.ProviderDoesNotExist.selector);
+ swarmRegistry.registerSwarm(
+ _getFleetUuid(fleetId), nonExistentProvider, new bytes(32), BITS_8, TagType.EDDYSTONE_UID
+ );
+ }
+
+ function test_RevertIf_registerSwarm_emptyFilter() public {
+ uint256 fleetId = _registerFleet(fleetOwner, "f1");
+ uint256 providerId = _registerProvider(providerOwner, "url1");
+
+ vm.prank(fleetOwner);
+ vm.expectRevert(SwarmRegistryL1Upgradeable.InvalidFilterSize.selector);
+ swarmRegistry.registerSwarm(_getFleetUuid(fleetId), providerId, new bytes(0), BITS_8, TagType.EDDYSTONE_UID);
+ }
+
+ function test_RevertIf_registerSwarm_filterTooLarge() public {
+ uint256 fleetId = _registerFleet(fleetOwner, "f1");
+ uint256 providerId = _registerProvider(providerOwner, "url1");
+
+ vm.prank(fleetOwner);
+ vm.expectRevert(SwarmRegistryL1Upgradeable.InvalidFilterSize.selector);
+ swarmRegistry.registerSwarm(_getFleetUuid(fleetId), providerId, new bytes(24577), BITS_8, TagType.EDDYSTONE_UID);
+ }
+
+ function test_registerSwarm_maxFilterSize() public {
+ uint256 fleetId = _registerFleet(fleetOwner, "f1");
+ uint256 providerId = _registerProvider(providerOwner, "url1");
+
+ // Exactly 24576 bytes should succeed
+ uint256 swarmId =
+ _registerSwarm(fleetOwner, fleetId, providerId, new bytes(24576), BITS_8, TagType.EDDYSTONE_UID);
+ assertTrue(swarmId != 0);
+ }
+
+ function test_registerSwarm_minFilterSize() public {
+ uint256 fleetId = _registerFleet(fleetOwner, "f1");
+ uint256 providerId = _registerProvider(providerOwner, "url1");
+
+ // 1 byte filter
+ uint256 swarmId = _registerSwarm(fleetOwner, fleetId, providerId, new bytes(1), BITS_8, TagType.EDDYSTONE_UID);
+ assertTrue(swarmId != 0);
+ }
+
+ // ==============================
+ // acceptSwarm / rejectSwarm
+ // ==============================
+
+ function test_acceptSwarm_setsStatusAndEmits() public {
+ uint256 fleetId = _registerFleet(fleetOwner, "f1");
+ uint256 providerId = _registerProvider(providerOwner, "url1");
+ uint256 swarmId = _registerSwarm(fleetOwner, fleetId, providerId, new bytes(50), BITS_8, TagType.EDDYSTONE_UID);
+
+ vm.expectEmit(true, true, true, true);
+ emit SwarmStatusChanged(swarmId, SwarmStatus.ACCEPTED);
+
+ vm.prank(providerOwner);
+ swarmRegistry.acceptSwarm(swarmId);
+
+ (,,,,, SwarmStatus status) = swarmRegistry.swarms(swarmId);
+ assertEq(uint8(status), uint8(SwarmStatus.ACCEPTED));
+ }
+
+ function test_rejectSwarm_setsStatusAndEmits() public {
+ uint256 fleetId = _registerFleet(fleetOwner, "f1");
+ uint256 providerId = _registerProvider(providerOwner, "url1");
+ uint256 swarmId = _registerSwarm(fleetOwner, fleetId, providerId, new bytes(50), BITS_8, TagType.EDDYSTONE_UID);
+
+ vm.expectEmit(true, true, true, true);
+ emit SwarmStatusChanged(swarmId, SwarmStatus.REJECTED);
+
+ vm.prank(providerOwner);
+ swarmRegistry.rejectSwarm(swarmId);
+
+ (,,,,, SwarmStatus status) = swarmRegistry.swarms(swarmId);
+ assertEq(uint8(status), uint8(SwarmStatus.REJECTED));
+ }
+
+ function test_RevertIf_acceptSwarm_notProviderOwner() public {
+ uint256 fleetId = _registerFleet(fleetOwner, "f1");
+ uint256 providerId = _registerProvider(providerOwner, "url1");
+ uint256 swarmId = _registerSwarm(fleetOwner, fleetId, providerId, new bytes(50), BITS_8, TagType.EDDYSTONE_UID);
+
+ vm.prank(caller);
+ vm.expectRevert(SwarmRegistryL1Upgradeable.NotProviderOwner.selector);
+ swarmRegistry.acceptSwarm(swarmId);
+ }
+
+ function test_RevertIf_rejectSwarm_notProviderOwner() public {
+ uint256 fleetId = _registerFleet(fleetOwner, "f1");
+ uint256 providerId = _registerProvider(providerOwner, "url1");
+ uint256 swarmId = _registerSwarm(fleetOwner, fleetId, providerId, new bytes(50), BITS_8, TagType.EDDYSTONE_UID);
+
+ vm.prank(fleetOwner); // fleet owner != provider owner
+ vm.expectRevert(SwarmRegistryL1Upgradeable.NotProviderOwner.selector);
+ swarmRegistry.rejectSwarm(swarmId);
+ }
+
+ function test_acceptSwarm_afterReject() public {
+ uint256 fleetId = _registerFleet(fleetOwner, "f1");
+ uint256 providerId = _registerProvider(providerOwner, "url1");
+ uint256 swarmId = _registerSwarm(fleetOwner, fleetId, providerId, new bytes(50), BITS_8, TagType.EDDYSTONE_UID);
+
+ vm.prank(providerOwner);
+ swarmRegistry.rejectSwarm(swarmId);
+
+ // Provider changes mind
+ vm.prank(providerOwner);
+ swarmRegistry.acceptSwarm(swarmId);
+
+ (,,,,, SwarmStatus status) = swarmRegistry.swarms(swarmId);
+ assertEq(uint8(status), uint8(SwarmStatus.ACCEPTED));
+ }
+
+ function test_rejectSwarm_afterAccept() public {
+ uint256 fleetId = _registerFleet(fleetOwner, "f1");
+ uint256 providerId = _registerProvider(providerOwner, "url1");
+ uint256 swarmId = _registerSwarm(fleetOwner, fleetId, providerId, new bytes(50), BITS_8, TagType.EDDYSTONE_UID);
+
+ vm.prank(providerOwner);
+ swarmRegistry.acceptSwarm(swarmId);
+
+ vm.prank(providerOwner);
+ swarmRegistry.rejectSwarm(swarmId);
+
+ (,,,,, SwarmStatus status) = swarmRegistry.swarms(swarmId);
+ assertEq(uint8(status), uint8(SwarmStatus.REJECTED));
+ }
+
+ // ==============================
+ // checkMembership — XOR logic
+ // ==============================
+
+ function test_checkMembership_XORLogic16Bit() public {
+ uint256 fleetId = _registerFleet(fleetOwner, "f1");
+ uint256 providerId = _registerProvider(providerOwner, "u1");
+
+ bytes memory tagId = hex"1122334455";
+ uint256 dataLen = 100;
+ uint256 m = dataLen / 2; // 50 slots for 16-bit
+
+ (uint32 h1, uint32 h2, uint32 h3, uint256 expectedFp) = getExpectedValues(tagId, m, BITS_16);
+
+ // Skip if collision (extremely unlikely with 50 slots)
+ if (h1 == h2 || h1 == h3 || h2 == h3) {
+ return;
+ }
+
+ bytes memory filter = new bytes(dataLen);
+ _write16Bit(filter, h1, uint16(expectedFp));
+
+ uint256 swarmId = _registerSwarm(fleetOwner, fleetId, providerId, filter, BITS_16, TagType.EDDYSTONE_UID);
+
+ // Positive check
+ assertTrue(swarmRegistry.checkMembership(swarmId, keccak256(tagId)), "Valid tag should pass");
+
+ // Negative check
+ assertFalse(swarmRegistry.checkMembership(swarmId, keccak256(hex"999999")), "Invalid tag should fail");
+ }
+
+ function test_checkMembership_XORLogic8Bit() public {
+ uint256 fleetId = _registerFleet(fleetOwner, "f1");
+ uint256 providerId = _registerProvider(providerOwner, "u1");
+
+ bytes memory tagId = hex"AABBCCDD";
+ uint256 rawLen = 80;
+ uint256 m = rawLen; // 80 slots for 8-bit
+
+ (uint32 h1, uint32 h2, uint32 h3, uint256 expectedFp) = getExpectedValues(tagId, m, BITS_8);
+
+ if (h1 == h2 || h1 == h3 || h2 == h3) {
+ return;
+ }
+
+ bytes memory filter = new bytes(rawLen);
+ _write8Bit(filter, h1, uint8(expectedFp));
+
+ uint256 swarmId = _registerSwarm(fleetOwner, fleetId, providerId, filter, BITS_8, TagType.EDDYSTONE_UID);
+
+ assertTrue(swarmRegistry.checkMembership(swarmId, keccak256(tagId)), "8-bit valid tag should pass");
+ assertFalse(swarmRegistry.checkMembership(swarmId, keccak256(hex"FFFFFF")), "8-bit invalid tag should fail");
+ }
+
+ function test_RevertIf_checkMembership_swarmNotFound() public {
+ vm.expectRevert(SwarmRegistryL1Upgradeable.SwarmNotFound.selector);
+ swarmRegistry.checkMembership(999, keccak256("anything"));
+ }
+
+ function test_checkMembership_allZeroFilter_returnsConsistent() public {
+ uint256 fleetId = _registerFleet(fleetOwner, "f1");
+ uint256 providerId = _registerProvider(providerOwner, "u1");
+
+ // All-zero filter: f1^f2^f3 = 0^0^0 = 0
+ bytes memory filter = new bytes(64);
+ uint256 swarmId = _registerSwarm(fleetOwner, fleetId, providerId, filter, BITS_16, TagType.EDDYSTONE_UID);
+
+ // Some tags will match (those with expectedFp=0), most won't
+ // The point is it doesn't revert
+ swarmRegistry.checkMembership(swarmId, keccak256("test1"));
+ swarmRegistry.checkMembership(swarmId, keccak256("test2"));
+ }
+
+ function test_checkMembership_tinyFilter_returnsFalse() public {
+ uint256 fleetId = _registerFleet(fleetOwner, "f1");
+ uint256 providerId = _registerProvider(providerOwner, "u1");
+
+ // 1-byte filter with 16-bit fingerprint: m = 1/2 = 0, returns false immediately
+ bytes memory filter = new bytes(1);
+ uint256 swarmId = _registerSwarm(fleetOwner, fleetId, providerId, filter, BITS_16, TagType.EDDYSTONE_UID);
+
+ // Should return false (not revert) because m == 0
+ assertFalse(swarmRegistry.checkMembership(swarmId, keccak256("test")), "m=0 should return false");
+ }
+
+ // ==============================
+ // Multiple swarms per fleet
+ // ==============================
+
+ function test_multipleSwarms_sameFleet() public {
+ uint256 fleetId = _registerFleet(fleetOwner, "f1");
+ uint256 providerId1 = _registerProvider(providerOwner, "url1");
+ uint256 providerId2 = _registerProvider(providerOwner, "url2");
+ uint256 providerId3 = _registerProvider(providerOwner, "url3");
+
+ uint256 s1 = _registerSwarm(fleetOwner, fleetId, providerId1, new bytes(32), BITS_8, TagType.EDDYSTONE_UID);
+ uint256 s2 = _registerSwarm(fleetOwner, fleetId, providerId2, new bytes(64), BITS_16, TagType.VENDOR_ID);
+ uint256 s3 =
+ _registerSwarm(fleetOwner, fleetId, providerId3, new bytes(50), BITS_8, TagType.IBEACON_PAYLOAD_ONLY);
+
+ // IDs are distinct hashes
+ assertTrue(s1 != s2 && s2 != s3 && s1 != s3);
+
+ assertEq(swarmRegistry.uuidSwarms(_getFleetUuid(fleetId), 0), s1);
+ assertEq(swarmRegistry.uuidSwarms(_getFleetUuid(fleetId), 1), s2);
+ assertEq(swarmRegistry.uuidSwarms(_getFleetUuid(fleetId), 2), s3);
+ }
+
+ // ==============================
+ // Fuzz
+ // ==============================
+
+ function testFuzz_registerSwarm_filterSizeRange(uint256 size) public {
+ size = bound(size, 1, 24576);
+
+ uint256 fleetId = _registerFleet(fleetOwner, abi.encodePacked("f-", size));
+ uint256 providerId = _registerProvider(providerOwner, string(abi.encodePacked("url-", size)));
+
+ uint256 swarmId =
+ _registerSwarm(fleetOwner, fleetId, providerId, new bytes(size), BITS_8, TagType.EDDYSTONE_UID);
+
+ (,, address pointer,,,) = swarmRegistry.swarms(swarmId);
+ assertTrue(pointer != address(0));
+ }
+
+ // ==============================
+ // updateSwarmProvider
+ // ==============================
+
+ function test_updateSwarmProvider_updatesProviderAndResetsStatus() public {
+ uint256 fleetId = _registerFleet(fleetOwner, "f1");
+ uint256 providerId1 = _registerProvider(providerOwner, "url1");
+ uint256 providerId2 = _registerProvider(providerOwner, "url2");
+
+ uint256 swarmId = _registerSwarm(fleetOwner, fleetId, providerId1, new bytes(50), BITS_8, TagType.EDDYSTONE_UID);
+
+ // Provider accepts
+ vm.prank(providerOwner);
+ swarmRegistry.acceptSwarm(swarmId);
+
+ // Fleet owner updates provider
+ vm.expectEmit(true, true, true, true);
+ emit SwarmProviderUpdated(swarmId, providerId1, providerId2);
+
+ vm.prank(fleetOwner);
+ swarmRegistry.updateSwarmProvider(swarmId, providerId2);
+
+ // Check new provider and status reset
+ (, uint256 newProviderId,,,, SwarmStatus status) = swarmRegistry.swarms(swarmId);
+ assertEq(newProviderId, providerId2);
+ assertEq(uint8(status), uint8(SwarmStatus.REGISTERED));
+ }
+
+ function test_RevertIf_updateSwarmProvider_swarmNotFound() public {
+ uint256 providerId = _registerProvider(providerOwner, "url1");
+
+ vm.prank(fleetOwner);
+ vm.expectRevert(SwarmRegistryL1Upgradeable.SwarmNotFound.selector);
+ swarmRegistry.updateSwarmProvider(999, providerId);
+ }
+
+ function test_RevertIf_updateSwarmProvider_notFleetOwner() public {
+ uint256 fleetId = _registerFleet(fleetOwner, "f1");
+ uint256 providerId1 = _registerProvider(providerOwner, "url1");
+ uint256 providerId2 = _registerProvider(providerOwner, "url2");
+
+ uint256 swarmId = _registerSwarm(fleetOwner, fleetId, providerId1, new bytes(50), BITS_8, TagType.EDDYSTONE_UID);
+
+ vm.prank(caller);
+ vm.expectRevert(SwarmRegistryL1Upgradeable.NotUuidOwner.selector);
+ swarmRegistry.updateSwarmProvider(swarmId, providerId2);
+ }
+
+ function test_RevertIf_updateSwarmProvider_providerDoesNotExist() public {
+ uint256 fleetId = _registerFleet(fleetOwner, "f1");
+ uint256 providerId = _registerProvider(providerOwner, "url1");
+
+ uint256 swarmId = _registerSwarm(fleetOwner, fleetId, providerId, new bytes(50), BITS_8, TagType.EDDYSTONE_UID);
+
+ vm.prank(fleetOwner);
+ vm.expectRevert(SwarmRegistryL1Upgradeable.ProviderDoesNotExist.selector);
+ swarmRegistry.updateSwarmProvider(swarmId, 99999);
+ }
+
+ // ==============================
+ // deleteSwarm
+ // ==============================
+
+ function test_deleteSwarm_removesSwarmAndEmits() public {
+ uint256 fleetId = _registerFleet(fleetOwner, "f1");
+ uint256 providerId = _registerProvider(providerOwner, "url1");
+ uint256 swarmId = _registerSwarm(fleetOwner, fleetId, providerId, new bytes(50), BITS_8, TagType.EDDYSTONE_UID);
+
+ vm.expectEmit(true, true, true, true);
+ emit SwarmDeleted(swarmId, _getFleetUuid(fleetId), fleetOwner);
+
+ vm.prank(fleetOwner);
+ swarmRegistry.deleteSwarm(swarmId);
+
+ // Swarm should be zeroed
+ (,, address pointer,,,) = swarmRegistry.swarms(swarmId);
+ assertEq(pointer, address(0));
+ }
+
+ function test_deleteSwarm_removesFromUuidSwarms() public {
+ uint256 fleetId = _registerFleet(fleetOwner, "f1");
+ uint256 providerId1 = _registerProvider(providerOwner, "url1");
+ uint256 providerId2 = _registerProvider(providerOwner, "url2");
+
+ // Use different filters to create distinct swarms
+ bytes memory filter1 = new bytes(50);
+ filter1[0] = 0x01;
+ bytes memory filter2 = new bytes(50);
+ filter2[0] = 0x02;
+
+ uint256 swarm1 = _registerSwarm(fleetOwner, fleetId, providerId1, filter1, BITS_8, TagType.EDDYSTONE_UID);
+ uint256 swarm2 = _registerSwarm(fleetOwner, fleetId, providerId2, filter2, BITS_8, TagType.EDDYSTONE_UID);
+
+ // Delete first swarm
+ vm.prank(fleetOwner);
+ swarmRegistry.deleteSwarm(swarm1);
+
+ // Only swarm2 should remain in fleetSwarms
+ assertEq(swarmRegistry.uuidSwarms(_getFleetUuid(fleetId), 0), swarm2);
+ vm.expectRevert();
+ swarmRegistry.uuidSwarms(_getFleetUuid(fleetId), 1); // Should be out of bounds
+ }
+
+ function test_deleteSwarm_swapAndPop() public {
+ uint256 fleetId = _registerFleet(fleetOwner, "f1");
+ uint256 providerId1 = _registerProvider(providerOwner, "url1");
+ uint256 providerId2 = _registerProvider(providerOwner, "url2");
+ uint256 providerId3 = _registerProvider(providerOwner, "url3");
+
+ // Use different filters to create distinct swarms
+ bytes memory filter1 = new bytes(50);
+ filter1[0] = 0x01;
+ bytes memory filter2 = new bytes(50);
+ filter2[0] = 0x02;
+ bytes memory filter3 = new bytes(50);
+ filter3[0] = 0x03;
+
+ uint256 swarm1 = _registerSwarm(fleetOwner, fleetId, providerId1, filter1, BITS_8, TagType.EDDYSTONE_UID);
+ uint256 swarm2 = _registerSwarm(fleetOwner, fleetId, providerId2, filter2, BITS_8, TagType.EDDYSTONE_UID);
+ uint256 swarm3 = _registerSwarm(fleetOwner, fleetId, providerId3, filter3, BITS_8, TagType.EDDYSTONE_UID);
+
+ // Delete middle swarm
+ vm.prank(fleetOwner);
+ swarmRegistry.deleteSwarm(swarm2);
+
+ // swarm3 should be swapped to index 1
+ assertEq(swarmRegistry.uuidSwarms(_getFleetUuid(fleetId), 0), swarm1);
+ assertEq(swarmRegistry.uuidSwarms(_getFleetUuid(fleetId), 1), swarm3);
+ vm.expectRevert();
+ swarmRegistry.uuidSwarms(_getFleetUuid(fleetId), 2); // Should be out of bounds
+ }
+
+ function test_RevertIf_deleteSwarm_swarmNotFound() public {
+ vm.prank(fleetOwner);
+ vm.expectRevert(SwarmRegistryL1Upgradeable.SwarmNotFound.selector);
+ swarmRegistry.deleteSwarm(999);
+ }
+
+ function test_RevertIf_deleteSwarm_notFleetOwner() public {
+ uint256 fleetId = _registerFleet(fleetOwner, "f1");
+ uint256 providerId = _registerProvider(providerOwner, "url1");
+ uint256 swarmId = _registerSwarm(fleetOwner, fleetId, providerId, new bytes(50), BITS_8, TagType.EDDYSTONE_UID);
+
+ vm.prank(caller);
+ vm.expectRevert(SwarmRegistryL1Upgradeable.NotUuidOwner.selector);
+ swarmRegistry.deleteSwarm(swarmId);
+ }
+
+ function test_deleteSwarm_afterProviderUpdate() public {
+ uint256 fleetId = _registerFleet(fleetOwner, "f1");
+ uint256 providerId1 = _registerProvider(providerOwner, "url1");
+ uint256 providerId2 = _registerProvider(providerOwner, "url2");
+ uint256 swarmId = _registerSwarm(fleetOwner, fleetId, providerId1, new bytes(50), BITS_8, TagType.EDDYSTONE_UID);
+
+ // Update provider then delete
+ vm.prank(fleetOwner);
+ swarmRegistry.updateSwarmProvider(swarmId, providerId2);
+
+ vm.prank(fleetOwner);
+ swarmRegistry.deleteSwarm(swarmId);
+
+ (,, address pointer,,,) = swarmRegistry.swarms(swarmId);
+ assertEq(pointer, address(0));
+ }
+
+ function test_deleteSwarm_updatesSwarmIndexInUuid() public {
+ uint256 fleetId = _registerFleet(fleetOwner, "f1");
+ uint256 p1 = _registerProvider(providerOwner, "url1");
+ uint256 p2 = _registerProvider(providerOwner, "url2");
+ uint256 p3 = _registerProvider(providerOwner, "url3");
+
+ // Use different filters to create distinct swarms
+ bytes memory filter1 = new bytes(50);
+ filter1[0] = 0x01;
+ bytes memory filter2 = new bytes(50);
+ filter2[0] = 0x02;
+ bytes memory filter3 = new bytes(50);
+ filter3[0] = 0x03;
+
+ uint256 s1 = _registerSwarm(fleetOwner, fleetId, p1, filter1, BITS_8, TagType.EDDYSTONE_UID);
+ uint256 s2 = _registerSwarm(fleetOwner, fleetId, p2, filter2, BITS_8, TagType.EDDYSTONE_UID);
+ uint256 s3 = _registerSwarm(fleetOwner, fleetId, p3, filter3, BITS_8, TagType.EDDYSTONE_UID);
+
+ // Verify initial indices
+ assertEq(swarmRegistry.swarmIndexInUuid(s1), 0);
+ assertEq(swarmRegistry.swarmIndexInUuid(s2), 1);
+ assertEq(swarmRegistry.swarmIndexInUuid(s3), 2);
+
+ // Delete s1 — s3 should be swapped to index 0
+ vm.prank(fleetOwner);
+ swarmRegistry.deleteSwarm(s1);
+
+ assertEq(swarmRegistry.swarmIndexInUuid(s3), 0);
+ assertEq(swarmRegistry.swarmIndexInUuid(s2), 1);
+ assertEq(swarmRegistry.swarmIndexInUuid(s1), 0); // deleted, reset to 0
+ }
+
+ // ==============================
+ // isSwarmValid
+ // ==============================
+
+ function test_isSwarmValid_bothValid() public {
+ uint256 fleetId = _registerFleet(fleetOwner, "f1");
+ uint256 providerId = _registerProvider(providerOwner, "url1");
+ uint256 swarmId = _registerSwarm(fleetOwner, fleetId, providerId, new bytes(50), BITS_8, TagType.EDDYSTONE_UID);
+
+ (bool fleetValid, bool providerValid) = swarmRegistry.isSwarmValid(swarmId);
+ assertTrue(fleetValid);
+ assertTrue(providerValid);
+ }
+
+ function test_isSwarmValid_providerBurned() public {
+ uint256 fleetId = _registerFleet(fleetOwner, "f1");
+ uint256 providerId = _registerProvider(providerOwner, "url1");
+ uint256 swarmId = _registerSwarm(fleetOwner, fleetId, providerId, new bytes(50), BITS_8, TagType.EDDYSTONE_UID);
+
+ // Burn provider
+ vm.prank(providerOwner);
+ providerContract.burn(providerId);
+
+ (bool fleetValid, bool providerValid) = swarmRegistry.isSwarmValid(swarmId);
+ assertTrue(fleetValid);
+ assertFalse(providerValid);
+ }
+
+ function test_isSwarmValid_fleetBurned() public {
+ uint256 fleetId = _registerFleet(fleetOwner, "f1");
+ uint256 providerId = _registerProvider(providerOwner, "url1");
+ uint256 swarmId = _registerSwarm(fleetOwner, fleetId, providerId, new bytes(50), BITS_8, TagType.EDDYSTONE_UID);
+
+ // Burn registered fleet token (operator = owner for fresh registration)
+ // This mints an owned-only token back to the owner
+ vm.prank(fleetOwner);
+ fleetContract.burn(fleetId);
+
+ // After burning registered token, UUID transitions to Owned state
+ // Need to burn the owned-only token to fully release
+ bytes16 uuid = _getFleetUuid(fleetId);
+ uint256 ownedTokenId = uint256(uint128(uuid)); // owned token has regionKey=0
+ vm.prank(fleetOwner);
+ fleetContract.burn(ownedTokenId);
+
+ (bool fleetValid, bool providerValid) = swarmRegistry.isSwarmValid(swarmId);
+ assertFalse(fleetValid);
+ assertTrue(providerValid);
+ }
+
+ function test_isSwarmValid_bothBurned() public {
+ uint256 fleetId = _registerFleet(fleetOwner, "f1");
+ uint256 providerId = _registerProvider(providerOwner, "url1");
+ uint256 swarmId = _registerSwarm(fleetOwner, fleetId, providerId, new bytes(50), BITS_8, TagType.EDDYSTONE_UID);
+
+ // Burn registered fleet token → mints owned-only token
+ vm.prank(fleetOwner);
+ fleetContract.burn(fleetId);
+
+ // Burn owned-only token to fully release UUID
+ bytes16 uuid = _getFleetUuid(fleetId);
+ uint256 ownedTokenId = uint256(uint128(uuid));
+ vm.prank(fleetOwner);
+ fleetContract.burn(ownedTokenId);
+
+ vm.prank(providerOwner);
+ providerContract.burn(providerId);
+
+ (bool fleetValid, bool providerValid) = swarmRegistry.isSwarmValid(swarmId);
+ assertFalse(fleetValid);
+ assertFalse(providerValid);
+ }
+
+ function test_RevertIf_isSwarmValid_swarmNotFound() public {
+ vm.expectRevert(SwarmRegistryL1Upgradeable.SwarmNotFound.selector);
+ swarmRegistry.isSwarmValid(999);
+ }
+
+ // ==============================
+ // purgeOrphanedSwarm
+ // ==============================
+
+ function test_purgeOrphanedSwarm_providerBurned() public {
+ uint256 fleetId = _registerFleet(fleetOwner, "f1");
+ uint256 providerId = _registerProvider(providerOwner, "url1");
+ uint256 swarmId = _registerSwarm(fleetOwner, fleetId, providerId, new bytes(50), BITS_8, TagType.EDDYSTONE_UID);
+
+ // Burn provider
+ vm.prank(providerOwner);
+ providerContract.burn(providerId);
+
+ // Anyone can purge
+ vm.expectEmit(true, true, true, true);
+ emit SwarmPurged(swarmId, _getFleetUuid(fleetId), caller);
+
+ vm.prank(caller);
+ swarmRegistry.purgeOrphanedSwarm(swarmId);
+
+ // Swarm should be zeroed
+ (,, address pointer,,,) = swarmRegistry.swarms(swarmId);
+ assertEq(pointer, address(0));
+ }
+
+ function test_purgeOrphanedSwarm_fleetBurned() public {
+ uint256 fleetId = _registerFleet(fleetOwner, "f1");
+ uint256 providerId = _registerProvider(providerOwner, "url1");
+ uint256 swarmId = _registerSwarm(fleetOwner, fleetId, providerId, new bytes(50), BITS_8, TagType.EDDYSTONE_UID);
+
+ // Burn registered fleet token → mints owned-only token
+ vm.prank(fleetOwner);
+ fleetContract.burn(fleetId);
+
+ // Burn owned-only token to fully release UUID
+ bytes16 uuid = _getFleetUuid(fleetId);
+ uint256 ownedTokenId = uint256(uint128(uuid));
+ vm.prank(fleetOwner);
+ fleetContract.burn(ownedTokenId);
+
+ vm.prank(caller);
+ swarmRegistry.purgeOrphanedSwarm(swarmId);
+
+ (,, address pointer,,,) = swarmRegistry.swarms(swarmId);
+ assertEq(pointer, address(0));
+ }
+
+ function test_purgeOrphanedSwarm_removesFromUuidSwarms() public {
+ uint256 fleetId = _registerFleet(fleetOwner, "f1");
+ uint256 p1 = _registerProvider(providerOwner, "url1");
+ uint256 p2 = _registerProvider(providerOwner, "url2");
+
+ // Use different filters to create distinct swarms
+ bytes memory filter1 = new bytes(50);
+ filter1[0] = 0x01;
+ bytes memory filter2 = new bytes(50);
+ filter2[0] = 0x02;
+
+ uint256 s1 = _registerSwarm(fleetOwner, fleetId, p1, filter1, BITS_8, TagType.EDDYSTONE_UID);
+ uint256 s2 = _registerSwarm(fleetOwner, fleetId, p2, filter2, BITS_8, TagType.EDDYSTONE_UID);
+
+ // Burn provider of s1
+ vm.prank(providerOwner);
+ providerContract.burn(p1);
+
+ vm.prank(caller);
+ swarmRegistry.purgeOrphanedSwarm(s1);
+
+ // s2 should be swapped to index 0
+ assertEq(swarmRegistry.uuidSwarms(_getFleetUuid(fleetId), 0), s2);
+ vm.expectRevert();
+ swarmRegistry.uuidSwarms(_getFleetUuid(fleetId), 1);
+ }
+
+ function test_RevertIf_purgeOrphanedSwarm_swarmNotFound() public {
+ vm.expectRevert(SwarmRegistryL1Upgradeable.SwarmNotFound.selector);
+ swarmRegistry.purgeOrphanedSwarm(999);
+ }
+
+ function test_RevertIf_purgeOrphanedSwarm_swarmNotOrphaned() public {
+ uint256 fleetId = _registerFleet(fleetOwner, "f1");
+ uint256 providerId = _registerProvider(providerOwner, "url1");
+ uint256 swarmId = _registerSwarm(fleetOwner, fleetId, providerId, new bytes(50), BITS_8, TagType.EDDYSTONE_UID);
+
+ vm.expectRevert(SwarmRegistryL1Upgradeable.SwarmNotOrphaned.selector);
+ swarmRegistry.purgeOrphanedSwarm(swarmId);
+ }
+
+ // ==============================
+ // Orphan guards on accept/reject/checkMembership
+ // ==============================
+
+ function test_RevertIf_acceptSwarm_orphaned() public {
+ uint256 fleetId = _registerFleet(fleetOwner, "f1");
+ uint256 providerId = _registerProvider(providerOwner, "url1");
+ uint256 swarmId = _registerSwarm(fleetOwner, fleetId, providerId, new bytes(50), BITS_8, TagType.EDDYSTONE_UID);
+
+ // Burn provider
+ vm.prank(providerOwner);
+ providerContract.burn(providerId);
+
+ vm.prank(providerOwner);
+ vm.expectRevert(SwarmRegistryL1Upgradeable.SwarmOrphaned.selector);
+ swarmRegistry.acceptSwarm(swarmId);
+ }
+
+ function test_RevertIf_rejectSwarm_orphaned() public {
+ uint256 fleetId = _registerFleet(fleetOwner, "f1");
+ uint256 providerId = _registerProvider(providerOwner, "url1");
+ uint256 swarmId = _registerSwarm(fleetOwner, fleetId, providerId, new bytes(50), BITS_8, TagType.EDDYSTONE_UID);
+
+ // Burn registered fleet token → mints owned-only token
+ vm.prank(fleetOwner);
+ fleetContract.burn(fleetId);
+
+ // Burn owned-only token to fully release UUID
+ bytes16 uuid = _getFleetUuid(fleetId);
+ uint256 ownedTokenId = uint256(uint128(uuid));
+ vm.prank(fleetOwner);
+ fleetContract.burn(ownedTokenId);
+
+ vm.prank(providerOwner);
+ vm.expectRevert(SwarmRegistryL1Upgradeable.SwarmOrphaned.selector);
+ swarmRegistry.rejectSwarm(swarmId);
+ }
+
+ function test_RevertIf_checkMembership_orphaned() public {
+ uint256 fleetId = _registerFleet(fleetOwner, "f1");
+ uint256 providerId = _registerProvider(providerOwner, "url1");
+ uint256 swarmId = _registerSwarm(fleetOwner, fleetId, providerId, new bytes(50), BITS_8, TagType.EDDYSTONE_UID);
+
+ // Burn provider
+ vm.prank(providerOwner);
+ providerContract.burn(providerId);
+
+ vm.expectRevert(SwarmRegistryL1Upgradeable.SwarmOrphaned.selector);
+ swarmRegistry.checkMembership(swarmId, keccak256("test"));
+ }
+
+ function test_RevertIf_acceptSwarm_fleetBurned() public {
+ uint256 fleetId = _registerFleet(fleetOwner, "f1");
+ uint256 providerId = _registerProvider(providerOwner, "url1");
+ uint256 swarmId = _registerSwarm(fleetOwner, fleetId, providerId, new bytes(50), BITS_8, TagType.EDDYSTONE_UID);
+
+ // Burn registered fleet token → mints owned-only token
+ vm.prank(fleetOwner);
+ fleetContract.burn(fleetId);
+
+ // Burn owned-only token to fully release UUID
+ bytes16 uuid = _getFleetUuid(fleetId);
+ uint256 ownedTokenId = uint256(uint128(uuid));
+ vm.prank(fleetOwner);
+ fleetContract.burn(ownedTokenId);
+
+ vm.prank(providerOwner);
+ vm.expectRevert(SwarmRegistryL1Upgradeable.SwarmOrphaned.selector);
+ swarmRegistry.acceptSwarm(swarmId);
+ }
+
+ function test_purge_thenAcceptReverts() public {
+ uint256 fleetId = _registerFleet(fleetOwner, "f1");
+ uint256 providerId = _registerProvider(providerOwner, "url1");
+ uint256 swarmId = _registerSwarm(fleetOwner, fleetId, providerId, new bytes(50), BITS_8, TagType.EDDYSTONE_UID);
+
+ vm.prank(providerOwner);
+ providerContract.burn(providerId);
+
+ vm.prank(caller);
+ swarmRegistry.purgeOrphanedSwarm(swarmId);
+
+ // After purge, swarm no longer exists
+ vm.prank(providerOwner);
+ vm.expectRevert(SwarmRegistryL1Upgradeable.SwarmNotFound.selector);
+ swarmRegistry.acceptSwarm(swarmId);
+ }
+
+ // ==============================
+ // Additional Coverage Tests
+ // ==============================
+
+ function test_checkMembership_8bit_forcedPath() public {
+ // This test ensures the 8-bit _readFingerprint path is exercised
+ uint256 fleetId = _registerFleet(fleetOwner, "f8bit");
+ uint256 providerId = _registerProvider(providerOwner, "url8bit");
+
+ // Use 100 bytes for filter
+ uint256 filterLen = 100;
+ bytes memory filter = new bytes(filterLen);
+
+ // For 8-bit, m = filterLen = 100 slots
+ bytes memory tagId = abi.encodePacked(uint256(0x12345678));
+ bytes32 tagHash = keccak256(tagId);
+ uint32 m32 = uint32(filterLen);
+
+ uint32 h1 = uint32(uint256(tagHash)) % m32;
+ uint32 h2 = uint32(uint256(tagHash) >> 32) % m32;
+ uint32 h3 = uint32(uint256(tagHash) >> 64) % m32;
+
+ // If there are collisions, try different tag
+ if (h1 == h2 || h1 == h3 || h2 == h3) {
+ tagId = abi.encodePacked(uint256(0xABCDEF01));
+ tagHash = keccak256(tagId);
+ h1 = uint32(uint256(tagHash)) % m32;
+ h2 = uint32(uint256(tagHash) >> 32) % m32;
+ h3 = uint32(uint256(tagHash) >> 64) % m32;
+ }
+
+ // Calculate expected fingerprint (8-bit)
+ uint256 expectedFp = (uint256(tagHash) >> 96) & 0xFF;
+
+ // Write fingerprint to h1 slot (f1 ^ 0 ^ 0 = expectedFp)
+ filter[h1] = bytes1(uint8(expectedFp));
+
+ uint256 swarmId = _registerSwarm(fleetOwner, fleetId, providerId, filter, BITS_8, TagType.EDDYSTONE_UID);
+
+ // This should exercise the 8-bit path in _readFingerprint
+ bool result = swarmRegistry.checkMembership(swarmId, tagHash);
+ assertTrue(result, "8-bit membership check should pass");
+ }
+
+ function test_upgrade_ownerCanUpgrade() public {
+ // Deploy a new implementation
+ SwarmRegistryL1Upgradeable newImpl = new SwarmRegistryL1Upgradeable();
+
+ // Owner should be able to upgrade (tests _authorizeUpgrade)
+ vm.prank(contractOwner);
+ swarmRegistry.upgradeToAndCall(address(newImpl), "");
+
+ // Verify upgrade succeeded - contract still works
+ assertEq(address(swarmRegistry.FLEET_CONTRACT()), address(fleetContract));
+ }
+
+ function test_RevertIf_upgrade_notOwner() public {
+ SwarmRegistryL1Upgradeable newImpl = new SwarmRegistryL1Upgradeable();
+
+ vm.prank(caller);
+ vm.expectRevert();
+ swarmRegistry.upgradeToAndCall(address(newImpl), "");
+ }
+
+ function test_checkMembership_mZero_16bit_returnsFalse() public {
+ // Edge case: filter too short for 16-bit -> m = 0 -> return false
+ uint256 fleetId = _registerFleet(fleetOwner, "f0");
+ uint256 providerId = _registerProvider(providerOwner, "url0");
+
+ // 1 byte filter with 16-bit: m = 1/2 = 0
+ bytes memory filter = new bytes(1);
+ uint256 swarmId = _registerSwarm(fleetOwner, fleetId, providerId, filter, BITS_16, TagType.EDDYSTONE_UID);
+
+ // Should return false without reverting
+ assertFalse(swarmRegistry.checkMembership(swarmId, keccak256("anyTag")));
+ }
+
+ function test_RevertIf_acceptSwarm_fleetOwnerNotProvider() public {
+ uint256 fleetId = _registerFleet(fleetOwner, "f1");
+ uint256 providerId = _registerProvider(providerOwner, "url1");
+ uint256 swarmId = _registerSwarm(fleetOwner, fleetId, providerId, new bytes(50), BITS_8, TagType.EDDYSTONE_UID);
+
+ // Fleet owner is not provider owner
+ vm.prank(fleetOwner);
+ vm.expectRevert(SwarmRegistryL1Upgradeable.NotProviderOwner.selector);
+ swarmRegistry.acceptSwarm(swarmId);
+ }
+
+ function test_registerSwarm_zeroProviderId() public {
+ uint256 fleetId = _registerFleet(fleetOwner, "f1");
+
+ vm.prank(fleetOwner);
+ vm.expectRevert(SwarmRegistryL1Upgradeable.ProviderDoesNotExist.selector);
+ swarmRegistry.registerSwarm(_getFleetUuid(fleetId), 0, new bytes(32), BITS_8, TagType.EDDYSTONE_UID);
+ }
+
+ // ==============================
+ // getFilterData
+ // ==============================
+
+ function test_getFilterData() public {
+ uint256 fleetId = _registerFleet(fleetOwner, "f1");
+ uint256 providerId = _registerProvider(providerOwner, "url1");
+
+ bytes memory filter = new bytes(100);
+ filter[0] = 0xFF;
+ filter[99] = 0x01;
+
+ uint256 swarmId = _registerSwarm(fleetOwner, fleetId, providerId, filter, BITS_16, TagType.EDDYSTONE_UID);
+
+ bytes memory stored = swarmRegistry.getFilterData(swarmId);
+ assertEq(stored.length, 100);
+ assertEq(uint8(stored[0]), 0xFF);
+ assertEq(uint8(stored[99]), 0x01);
+ }
+
+ function test_RevertIf_getFilterData_swarmNotFound() public {
+ vm.expectRevert(SwarmRegistryL1Upgradeable.SwarmNotFound.selector);
+ swarmRegistry.getFilterData(999);
+ }
+
+ // ==============================
+ // Invalid Enum Values
+ // ==============================
+
+ function test_RevertIf_registerSwarm_invalidFingerprintSize() public {
+ // Solidity 0.8+ reverts with Panic(0x21) for invalid enum conversion
+ uint256 fleetId = _registerFleet(fleetOwner, "f1");
+ uint256 providerId = _registerProvider(providerOwner, "url1");
+ bytes16 uuid = _getFleetUuid(fleetId);
+
+ // Encode call with invalid fpSize (2, but enum only has 0 and 1)
+ bytes memory callData = abi.encodeWithSelector(
+ SwarmRegistryL1Upgradeable.registerSwarm.selector,
+ uuid,
+ providerId,
+ new bytes(32),
+ uint8(2), // Invalid FingerprintSize
+ uint8(0) // Valid TagType
+ );
+
+ vm.prank(fleetOwner);
+ (bool success,) = address(swarmRegistry).call(callData);
+ assertFalse(success, "Should revert on invalid FingerprintSize");
+ }
+
+ function test_RevertIf_registerSwarm_invalidTagType() public {
+ // Also verify TagType enum validation
+ uint256 fleetId = _registerFleet(fleetOwner, "f1");
+ uint256 providerId = _registerProvider(providerOwner, "url1");
+ bytes16 uuid = _getFleetUuid(fleetId);
+
+ // Encode call with invalid tagType (5, but enum only has 0-4)
+ bytes memory callData = abi.encodeWithSelector(
+ SwarmRegistryL1Upgradeable.registerSwarm.selector,
+ uuid,
+ providerId,
+ new bytes(32),
+ uint8(0), // Valid FingerprintSize
+ uint8(5) // Invalid TagType
+ );
+
+ vm.prank(fleetOwner);
+ (bool success,) = address(swarmRegistry).call(callData);
+ assertFalse(success, "Should revert on invalid TagType");
+ }
+}
diff --git a/test/SwarmRegistryUniversal.t.sol b/test/SwarmRegistryUniversal.t.sol
new file mode 100644
index 00000000..272aae89
--- /dev/null
+++ b/test/SwarmRegistryUniversal.t.sol
@@ -0,0 +1,1272 @@
+// SPDX-License-Identifier: MIT
+pragma solidity ^0.8.24;
+
+import {Test} from "forge-std/Test.sol";
+import {ERC1967Proxy} from "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol";
+import "../src/swarms/SwarmRegistryUniversalUpgradeable.sol";
+import {FleetIdentityUpgradeable} from "../src/swarms/FleetIdentityUpgradeable.sol";
+import {ServiceProviderUpgradeable} from "../src/swarms/ServiceProviderUpgradeable.sol";
+import {SwarmStatus, TagType, FingerprintSize} from "../src/swarms/interfaces/SwarmTypes.sol";
+import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol";
+
+contract MockBondTokenUniv is ERC20 {
+ constructor() ERC20("Mock Bond", "MBOND") {}
+
+ function mint(address to, uint256 amount) external {
+ _mint(to, amount);
+ }
+}
+
+contract SwarmRegistryUniversalTest is Test {
+ SwarmRegistryUniversalUpgradeable swarmRegistry;
+ FleetIdentityUpgradeable fleetContract;
+ ServiceProviderUpgradeable providerContract;
+ MockBondTokenUniv bondToken;
+
+ address contractOwner = address(0x1111);
+ address fleetOwner = address(0x1);
+ address providerOwner = address(0x2);
+ address caller = address(0x3);
+
+ uint256 constant FLEET_BOND = 100 ether;
+
+ // Region constants for fleet registration
+ uint16 constant US = 840;
+ uint16 constant ADMIN_CA = 6; // California
+
+ // Alias for FingerprintSize enum
+ FingerprintSize constant BITS_8 = FingerprintSize.BITS_8;
+ FingerprintSize constant BITS_16 = FingerprintSize.BITS_16;
+
+ event SwarmRegistered(
+ uint256 indexed swarmId, bytes16 indexed fleetUuid, uint256 indexed providerId, address owner, uint32 filterSize
+ );
+ event SwarmStatusChanged(uint256 indexed swarmId, SwarmStatus status);
+ event SwarmProviderUpdated(uint256 indexed swarmId, uint256 indexed oldProviderId, uint256 indexed newProviderId);
+ event SwarmDeleted(uint256 indexed swarmId, bytes16 indexed fleetUuid, address indexed owner);
+ event SwarmPurged(uint256 indexed swarmId, bytes16 indexed fleetUuid, address indexed purgedBy);
+
+ function setUp() public {
+ bondToken = new MockBondTokenUniv();
+
+ // Deploy FleetIdentity via proxy
+ FleetIdentityUpgradeable fleetImpl = new FleetIdentityUpgradeable();
+ ERC1967Proxy fleetProxy = new ERC1967Proxy(
+ address(fleetImpl),
+ abi.encodeCall(FleetIdentityUpgradeable.initialize, (contractOwner, address(bondToken), FLEET_BOND, 0))
+ );
+ fleetContract = FleetIdentityUpgradeable(address(fleetProxy));
+
+ // Deploy ServiceProvider via proxy
+ ServiceProviderUpgradeable providerImpl = new ServiceProviderUpgradeable();
+ ERC1967Proxy providerProxy = new ERC1967Proxy(
+ address(providerImpl), abi.encodeCall(ServiceProviderUpgradeable.initialize, (contractOwner))
+ );
+ providerContract = ServiceProviderUpgradeable(address(providerProxy));
+
+ // Deploy SwarmRegistry via proxy
+ SwarmRegistryUniversalUpgradeable registryImpl = new SwarmRegistryUniversalUpgradeable();
+ ERC1967Proxy registryProxy = new ERC1967Proxy(
+ address(registryImpl),
+ abi.encodeCall(
+ SwarmRegistryUniversalUpgradeable.initialize,
+ (address(fleetContract), address(providerContract), contractOwner)
+ )
+ );
+ swarmRegistry = SwarmRegistryUniversalUpgradeable(address(registryProxy));
+
+ // Fund fleet owner and approve
+ bondToken.mint(fleetOwner, 1_000_000 ether);
+ vm.prank(fleetOwner);
+ bondToken.approve(address(fleetContract), type(uint256).max);
+ }
+
+ // ==============================
+ // Helpers
+ // ==============================
+
+ function _registerFleet(address owner, bytes memory seed) internal returns (uint256) {
+ vm.prank(owner);
+ return fleetContract.registerFleetLocal(bytes16(keccak256(seed)), US, ADMIN_CA, 0);
+ }
+
+ function _getFleetUuid(uint256 fleetId) internal pure returns (bytes16) {
+ return bytes16(uint128(fleetId));
+ }
+
+ function _registerProvider(address owner, string memory url) internal returns (uint256) {
+ vm.prank(owner);
+ return providerContract.registerProvider(url);
+ }
+
+ function _registerSwarm(
+ address owner,
+ uint256 fleetId,
+ uint256 providerId,
+ bytes memory filter,
+ FingerprintSize fpSize,
+ TagType tagType
+ ) internal returns (uint256) {
+ bytes16 fleetUuid = _getFleetUuid(fleetId);
+ vm.prank(owner);
+ return swarmRegistry.registerSwarm(fleetUuid, providerId, filter, fpSize, tagType);
+ }
+
+ /// @dev Get expected hash indices and fingerprint for XOR filter verification
+ function getExpectedValues(bytes memory tagId, uint256 m, FingerprintSize fpSize)
+ public
+ pure
+ returns (uint32 h1, uint32 h2, uint32 h3, uint256 fp)
+ {
+ bytes32 h = keccak256(tagId);
+ h1 = uint32(uint256(h)) % uint32(m);
+ h2 = uint32(uint256(h) >> 32) % uint32(m);
+ h3 = uint32(uint256(h) >> 64) % uint32(m);
+ uint256 fpMask = fpSize == FingerprintSize.BITS_8 ? 0xFF : 0xFFFF;
+ fp = (uint256(h) >> 96) & fpMask;
+ }
+
+ function _write16Bit(bytes memory data, uint256 slotIndex, uint16 value) internal pure {
+ uint256 byteOffset = slotIndex * 2;
+ data[byteOffset] = bytes1(uint8(value >> 8));
+ data[byteOffset + 1] = bytes1(uint8(value));
+ }
+
+ function _write8Bit(bytes memory data, uint256 slotIndex, uint8 value) internal pure {
+ data[slotIndex] = bytes1(value);
+ }
+
+ // ==============================
+ // Constructor
+ // ==============================
+
+ function test_initialize_setsContracts() public view {
+ assertEq(address(swarmRegistry.FLEET_CONTRACT()), address(fleetContract));
+ assertEq(address(swarmRegistry.PROVIDER_CONTRACT()), address(providerContract));
+ }
+
+ function test_RevertIf_initialize_zeroFleetAddress() public {
+ SwarmRegistryUniversalUpgradeable impl = new SwarmRegistryUniversalUpgradeable();
+ vm.expectRevert(SwarmRegistryUniversalUpgradeable.InvalidSwarmData.selector);
+ new ERC1967Proxy(
+ address(impl),
+ abi.encodeCall(
+ SwarmRegistryUniversalUpgradeable.initialize, (address(0), address(providerContract), contractOwner)
+ )
+ );
+ }
+
+ function test_RevertIf_initialize_zeroProviderAddress() public {
+ SwarmRegistryUniversalUpgradeable impl = new SwarmRegistryUniversalUpgradeable();
+ vm.expectRevert(SwarmRegistryUniversalUpgradeable.InvalidSwarmData.selector);
+ new ERC1967Proxy(
+ address(impl),
+ abi.encodeCall(
+ SwarmRegistryUniversalUpgradeable.initialize, (address(fleetContract), address(0), contractOwner)
+ )
+ );
+ }
+
+ function test_RevertIf_initialize_bothZero() public {
+ SwarmRegistryUniversalUpgradeable impl = new SwarmRegistryUniversalUpgradeable();
+ vm.expectRevert(SwarmRegistryUniversalUpgradeable.InvalidSwarmData.selector);
+ new ERC1967Proxy(
+ address(impl),
+ abi.encodeCall(SwarmRegistryUniversalUpgradeable.initialize, (address(0), address(0), contractOwner))
+ );
+ }
+
+ // ==============================
+ // registerSwarm — happy path
+ // ==============================
+
+ function test_registerSwarm_basicFlow() public {
+ uint256 fleetId = _registerFleet(fleetOwner, "my-fleet");
+ uint256 providerId = _registerProvider(providerOwner, "https://api.example.com");
+
+ uint256 swarmId =
+ _registerSwarm(fleetOwner, fleetId, providerId, new bytes(100), BITS_16, TagType.IBEACON_INCLUDES_MAC);
+
+ // Swarm ID is deterministic hash of (fleetUuid, filter, fpSize, tagType)
+ uint256 expectedId =
+ swarmRegistry.computeSwarmId(_getFleetUuid(fleetId), new bytes(100), BITS_16, TagType.IBEACON_INCLUDES_MAC);
+ assertEq(swarmId, expectedId);
+ }
+
+ function test_registerSwarm_storesMetadataCorrectly() public {
+ uint256 fleetId = _registerFleet(fleetOwner, "f1");
+ uint256 providerId = _registerProvider(providerOwner, "url1");
+
+ uint256 swarmId = _registerSwarm(fleetOwner, fleetId, providerId, new bytes(50), BITS_16, TagType.VENDOR_ID);
+
+ (
+ bytes16 storedFleetUuid,
+ uint256 storedProviderId,
+ uint32 storedFilterLen,
+ FingerprintSize storedFpSize,
+ TagType storedTagType,
+ SwarmStatus storedStatus
+ ) = swarmRegistry.swarms(swarmId);
+
+ assertEq(storedFleetUuid, _getFleetUuid(fleetId));
+ assertEq(storedProviderId, providerId);
+ assertEq(storedFilterLen, 50);
+ assertEq(uint8(storedFpSize), uint8(BITS_16));
+ assertEq(uint8(storedTagType), uint8(TagType.VENDOR_ID));
+ assertEq(uint8(storedStatus), uint8(SwarmStatus.REGISTERED));
+ }
+
+ function test_registerSwarm_storesFilterData() public {
+ uint256 fleetId = _registerFleet(fleetOwner, "f1");
+ uint256 providerId = _registerProvider(providerOwner, "url1");
+
+ bytes memory filter = new bytes(100);
+ // Write some non-zero data
+ filter[0] = 0xAB;
+ filter[50] = 0xCD;
+ filter[99] = 0xEF;
+
+ uint256 swarmId = _registerSwarm(fleetOwner, fleetId, providerId, filter, BITS_16, TagType.EDDYSTONE_UID);
+
+ bytes memory storedFilter = swarmRegistry.getFilterData(swarmId);
+ assertEq(storedFilter.length, 100);
+ assertEq(uint8(storedFilter[0]), 0xAB);
+ assertEq(uint8(storedFilter[50]), 0xCD);
+ assertEq(uint8(storedFilter[99]), 0xEF);
+ }
+
+ function test_registerSwarm_deterministicId() public {
+ uint256 fleetId = _registerFleet(fleetOwner, "f1");
+ uint256 providerId = _registerProvider(providerOwner, "url1");
+
+ bytes memory filter = new bytes(32);
+
+ uint256 expectedId = swarmRegistry.computeSwarmId(_getFleetUuid(fleetId), filter, BITS_8, TagType.EDDYSTONE_UID);
+
+ uint256 swarmId = _registerSwarm(fleetOwner, fleetId, providerId, filter, BITS_8, TagType.EDDYSTONE_UID);
+ assertEq(swarmId, expectedId);
+ }
+
+ function test_RevertIf_registerSwarm_duplicateSwarm() public {
+ uint256 fleetId = _registerFleet(fleetOwner, "f1");
+ uint256 providerId = _registerProvider(providerOwner, "url1");
+
+ _registerSwarm(fleetOwner, fleetId, providerId, new bytes(32), BITS_8, TagType.EDDYSTONE_UID);
+
+ vm.prank(fleetOwner);
+ vm.expectRevert(SwarmRegistryUniversalUpgradeable.SwarmAlreadyExists.selector);
+ swarmRegistry.registerSwarm(_getFleetUuid(fleetId), providerId, new bytes(32), BITS_8, TagType.EDDYSTONE_UID);
+ }
+
+ function test_registerSwarm_emitsSwarmRegistered() public {
+ uint256 fleetId = _registerFleet(fleetOwner, "f1");
+ uint256 providerId = _registerProvider(providerOwner, "url1");
+
+ bytes memory filter = new bytes(50);
+ uint256 expectedId =
+ swarmRegistry.computeSwarmId(_getFleetUuid(fleetId), filter, BITS_16, TagType.EDDYSTONE_UID);
+
+ vm.expectEmit(true, true, true, true);
+ emit SwarmRegistered(expectedId, _getFleetUuid(fleetId), providerId, fleetOwner, 50);
+
+ _registerSwarm(fleetOwner, fleetId, providerId, filter, BITS_16, TagType.EDDYSTONE_UID);
+ }
+
+ function test_registerSwarm_linksUuidSwarms() public {
+ uint256 fleetId = _registerFleet(fleetOwner, "f1");
+ uint256 providerId1 = _registerProvider(providerOwner, "url1");
+ uint256 providerId2 = _registerProvider(providerOwner, "url2");
+
+ // Use different filters to create distinct swarms
+ bytes memory filter1 = new bytes(50);
+ filter1[0] = 0x01;
+ bytes memory filter2 = new bytes(50);
+ filter2[0] = 0x02;
+
+ uint256 s1 = _registerSwarm(fleetOwner, fleetId, providerId1, filter1, BITS_8, TagType.EDDYSTONE_UID);
+ uint256 s2 = _registerSwarm(fleetOwner, fleetId, providerId2, filter2, BITS_8, TagType.EDDYSTONE_UID);
+
+ assertEq(swarmRegistry.uuidSwarms(_getFleetUuid(fleetId), 0), s1);
+ assertEq(swarmRegistry.uuidSwarms(_getFleetUuid(fleetId), 1), s2);
+ }
+
+ function test_registerSwarm_allTagTypes() public {
+ uint256 fleetId1 = _registerFleet(fleetOwner, "f1");
+ uint256 fleetId2 = _registerFleet(fleetOwner, "f2");
+ uint256 fleetId3 = _registerFleet(fleetOwner, "f3");
+ uint256 fleetId4 = _registerFleet(fleetOwner, "f4");
+ uint256 providerId = _registerProvider(providerOwner, "url");
+
+ uint256 s1 =
+ _registerSwarm(fleetOwner, fleetId1, providerId, new bytes(32), BITS_8, TagType.IBEACON_PAYLOAD_ONLY);
+ uint256 s2 =
+ _registerSwarm(fleetOwner, fleetId2, providerId, new bytes(32), BITS_8, TagType.IBEACON_INCLUDES_MAC);
+ uint256 s3 = _registerSwarm(fleetOwner, fleetId3, providerId, new bytes(32), BITS_8, TagType.VENDOR_ID);
+ uint256 s4 = _registerSwarm(fleetOwner, fleetId4, providerId, new bytes(32), BITS_8, TagType.EDDYSTONE_UID);
+
+ (,,,, TagType t1,) = swarmRegistry.swarms(s1);
+ (,,,, TagType t2,) = swarmRegistry.swarms(s2);
+ (,,,, TagType t3,) = swarmRegistry.swarms(s3);
+ (,,,, TagType t4,) = swarmRegistry.swarms(s4);
+
+ assertEq(uint8(t1), uint8(TagType.IBEACON_PAYLOAD_ONLY));
+ assertEq(uint8(t2), uint8(TagType.IBEACON_INCLUDES_MAC));
+ assertEq(uint8(t3), uint8(TagType.VENDOR_ID));
+ assertEq(uint8(t4), uint8(TagType.EDDYSTONE_UID));
+ }
+
+ function test_registerSwarm_bothFingerprintSizes() public {
+ uint256 fleetId1 = _registerFleet(fleetOwner, "f1");
+ uint256 fleetId2 = _registerFleet(fleetOwner, "f2");
+ uint256 providerId = _registerProvider(providerOwner, "url");
+
+ uint256 s1 = _registerSwarm(fleetOwner, fleetId1, providerId, new bytes(32), BITS_8, TagType.EDDYSTONE_UID);
+ uint256 s2 = _registerSwarm(fleetOwner, fleetId2, providerId, new bytes(64), BITS_16, TagType.EDDYSTONE_UID);
+
+ (,,, FingerprintSize fp1,,) = swarmRegistry.swarms(s1);
+ (,,, FingerprintSize fp2,,) = swarmRegistry.swarms(s2);
+
+ assertEq(uint8(fp1), uint8(BITS_8));
+ assertEq(uint8(fp2), uint8(BITS_16));
+ }
+
+ // ==============================
+ // registerSwarm — reverts
+ // ==============================
+
+ function test_RevertIf_registerSwarm_zeroUuid() public {
+ uint256 providerId = _registerProvider(providerOwner, "url1");
+
+ vm.prank(fleetOwner);
+ vm.expectRevert(SwarmRegistryUniversalUpgradeable.InvalidUuid.selector);
+ swarmRegistry.registerSwarm(bytes16(0), providerId, new bytes(32), BITS_8, TagType.EDDYSTONE_UID);
+ }
+
+ function test_RevertIf_registerSwarm_providerDoesNotExist() public {
+ uint256 fleetId = _registerFleet(fleetOwner, "f1");
+ uint256 nonExistentProvider = 12345;
+
+ vm.prank(fleetOwner);
+ vm.expectRevert(SwarmRegistryUniversalUpgradeable.ProviderDoesNotExist.selector);
+ swarmRegistry.registerSwarm(
+ _getFleetUuid(fleetId), nonExistentProvider, new bytes(32), BITS_8, TagType.EDDYSTONE_UID
+ );
+ }
+
+ function test_RevertIf_registerSwarm_notFleetOwner() public {
+ uint256 fleetId = _registerFleet(fleetOwner, "my-fleet");
+
+ vm.prank(caller);
+ vm.expectRevert(SwarmRegistryUniversalUpgradeable.NotUuidOwner.selector);
+ swarmRegistry.registerSwarm(_getFleetUuid(fleetId), 1, new bytes(10), BITS_16, TagType.EDDYSTONE_UID);
+ }
+
+ function test_RevertIf_registerSwarm_emptyFilter() public {
+ uint256 fleetId = _registerFleet(fleetOwner, "f1");
+ uint256 providerId = _registerProvider(providerOwner, "url1");
+
+ vm.prank(fleetOwner);
+ vm.expectRevert(SwarmRegistryUniversalUpgradeable.InvalidFilterSize.selector);
+ swarmRegistry.registerSwarm(_getFleetUuid(fleetId), providerId, new bytes(0), BITS_8, TagType.EDDYSTONE_UID);
+ }
+
+ function test_RevertIf_registerSwarm_filterTooLarge() public {
+ uint256 fleetId = _registerFleet(fleetOwner, "f1");
+ uint256 providerId = _registerProvider(providerOwner, "url1");
+
+ vm.prank(fleetOwner);
+ vm.expectRevert(SwarmRegistryUniversalUpgradeable.FilterTooLarge.selector);
+ swarmRegistry.registerSwarm(_getFleetUuid(fleetId), providerId, new bytes(24577), BITS_8, TagType.EDDYSTONE_UID);
+ }
+
+ function test_registerSwarm_maxFilterSize() public {
+ uint256 fleetId = _registerFleet(fleetOwner, "f1");
+ uint256 providerId = _registerProvider(providerOwner, "url1");
+
+ // Exactly MAX_FILTER_SIZE (24576) should succeed
+ uint256 swarmId =
+ _registerSwarm(fleetOwner, fleetId, providerId, new bytes(24576), BITS_8, TagType.EDDYSTONE_UID);
+ assertTrue(swarmId != 0);
+ }
+
+ function test_registerSwarm_minFilterSize() public {
+ uint256 fleetId = _registerFleet(fleetOwner, "f1");
+ uint256 providerId = _registerProvider(providerOwner, "url1");
+
+ // 1 byte filter
+ uint256 swarmId = _registerSwarm(fleetOwner, fleetId, providerId, new bytes(1), BITS_8, TagType.EDDYSTONE_UID);
+ assertTrue(swarmId != 0);
+ }
+
+ // ==============================
+ // acceptSwarm / rejectSwarm
+ // ==============================
+
+ function test_acceptSwarm_setsStatusAndEmits() public {
+ uint256 fleetId = _registerFleet(fleetOwner, "f1");
+ uint256 providerId = _registerProvider(providerOwner, "url1");
+ uint256 swarmId = _registerSwarm(fleetOwner, fleetId, providerId, new bytes(50), BITS_8, TagType.EDDYSTONE_UID);
+
+ vm.expectEmit(true, true, true, true);
+ emit SwarmStatusChanged(swarmId, SwarmStatus.ACCEPTED);
+
+ vm.prank(providerOwner);
+ swarmRegistry.acceptSwarm(swarmId);
+
+ (,,,,, SwarmStatus status) = swarmRegistry.swarms(swarmId);
+ assertEq(uint8(status), uint8(SwarmStatus.ACCEPTED));
+ }
+
+ function test_rejectSwarm_setsStatusAndEmits() public {
+ uint256 fleetId = _registerFleet(fleetOwner, "f1");
+ uint256 providerId = _registerProvider(providerOwner, "url1");
+ uint256 swarmId = _registerSwarm(fleetOwner, fleetId, providerId, new bytes(50), BITS_8, TagType.EDDYSTONE_UID);
+
+ vm.expectEmit(true, true, true, true);
+ emit SwarmStatusChanged(swarmId, SwarmStatus.REJECTED);
+
+ vm.prank(providerOwner);
+ swarmRegistry.rejectSwarm(swarmId);
+
+ (,,,,, SwarmStatus status) = swarmRegistry.swarms(swarmId);
+ assertEq(uint8(status), uint8(SwarmStatus.REJECTED));
+ }
+
+ function test_RevertIf_acceptSwarm_notProviderOwner() public {
+ uint256 fleetId = _registerFleet(fleetOwner, "f1");
+ uint256 providerId = _registerProvider(providerOwner, "url1");
+ uint256 swarmId = _registerSwarm(fleetOwner, fleetId, providerId, new bytes(50), BITS_8, TagType.EDDYSTONE_UID);
+
+ vm.prank(caller);
+ vm.expectRevert(SwarmRegistryUniversalUpgradeable.NotProviderOwner.selector);
+ swarmRegistry.acceptSwarm(swarmId);
+ }
+
+ function test_RevertIf_rejectSwarm_notProviderOwner() public {
+ uint256 fleetId = _registerFleet(fleetOwner, "f1");
+ uint256 providerId = _registerProvider(providerOwner, "url1");
+ uint256 swarmId = _registerSwarm(fleetOwner, fleetId, providerId, new bytes(50), BITS_8, TagType.EDDYSTONE_UID);
+
+ vm.prank(fleetOwner); // fleet owner != provider owner
+ vm.expectRevert(SwarmRegistryUniversalUpgradeable.NotProviderOwner.selector);
+ swarmRegistry.rejectSwarm(swarmId);
+ }
+
+ function test_RevertIf_acceptSwarm_fleetOwnerNotProvider() public {
+ uint256 fleetId = _registerFleet(fleetOwner, "f1");
+ uint256 providerId = _registerProvider(providerOwner, "url1");
+ uint256 swarmId = _registerSwarm(fleetOwner, fleetId, providerId, new bytes(50), BITS_8, TagType.EDDYSTONE_UID);
+
+ vm.prank(fleetOwner);
+ vm.expectRevert(SwarmRegistryUniversalUpgradeable.NotProviderOwner.selector);
+ swarmRegistry.acceptSwarm(swarmId);
+ }
+
+ function test_acceptSwarm_afterReject() public {
+ uint256 fleetId = _registerFleet(fleetOwner, "f1");
+ uint256 providerId = _registerProvider(providerOwner, "url1");
+ uint256 swarmId = _registerSwarm(fleetOwner, fleetId, providerId, new bytes(50), BITS_8, TagType.EDDYSTONE_UID);
+
+ vm.prank(providerOwner);
+ swarmRegistry.rejectSwarm(swarmId);
+
+ vm.prank(providerOwner);
+ swarmRegistry.acceptSwarm(swarmId);
+
+ (,,,,, SwarmStatus status) = swarmRegistry.swarms(swarmId);
+ assertEq(uint8(status), uint8(SwarmStatus.ACCEPTED));
+ }
+
+ function test_rejectSwarm_afterAccept() public {
+ uint256 fleetId = _registerFleet(fleetOwner, "f1");
+ uint256 providerId = _registerProvider(providerOwner, "url1");
+ uint256 swarmId = _registerSwarm(fleetOwner, fleetId, providerId, new bytes(50), BITS_8, TagType.EDDYSTONE_UID);
+
+ vm.prank(providerOwner);
+ swarmRegistry.acceptSwarm(swarmId);
+
+ vm.prank(providerOwner);
+ swarmRegistry.rejectSwarm(swarmId);
+
+ (,,,,, SwarmStatus status) = swarmRegistry.swarms(swarmId);
+ assertEq(uint8(status), uint8(SwarmStatus.REJECTED));
+ }
+
+ // ==============================
+ // checkMembership — XOR logic
+ // ==============================
+
+ function test_checkMembership_XORLogic16Bit() public {
+ uint256 fleetId = _registerFleet(fleetOwner, "f1");
+ uint256 providerId = _registerProvider(providerOwner, "u1");
+
+ bytes memory tagId = hex"1122334455";
+ uint256 dataLen = 100;
+ uint256 m = dataLen / 2; // 50 slots for 16-bit
+
+ (uint32 h1, uint32 h2, uint32 h3, uint256 expectedFp) = getExpectedValues(tagId, m, BITS_16);
+
+ if (h1 == h2 || h1 == h3 || h2 == h3) {
+ return;
+ }
+
+ bytes memory filter = new bytes(dataLen);
+ _write16Bit(filter, h1, uint16(expectedFp));
+
+ uint256 swarmId = _registerSwarm(fleetOwner, fleetId, providerId, filter, BITS_16, TagType.EDDYSTONE_UID);
+
+ bytes32 tagHash = keccak256(tagId);
+ assertTrue(swarmRegistry.checkMembership(swarmId, tagHash), "Tag should be member");
+
+ bytes32 fakeHash = keccak256("not-a-tag");
+ assertFalse(swarmRegistry.checkMembership(swarmId, fakeHash), "Fake tag should not be member");
+ }
+
+ function test_checkMembership_XORLogic8Bit() public {
+ uint256 fleetId = _registerFleet(fleetOwner, "f1");
+ uint256 providerId = _registerProvider(providerOwner, "u1");
+
+ bytes memory tagId = hex"AABBCCDD";
+ uint256 dataLen = 80;
+ uint256 m = dataLen; // 80 slots for 8-bit
+
+ (uint32 h1, uint32 h2, uint32 h3, uint256 expectedFp) = getExpectedValues(tagId, m, BITS_8);
+
+ if (h1 == h2 || h1 == h3 || h2 == h3) {
+ return;
+ }
+
+ bytes memory filter = new bytes(dataLen);
+ _write8Bit(filter, h1, uint8(expectedFp));
+
+ uint256 swarmId = _registerSwarm(fleetOwner, fleetId, providerId, filter, BITS_8, TagType.EDDYSTONE_UID);
+
+ assertTrue(swarmRegistry.checkMembership(swarmId, keccak256(tagId)), "8-bit valid tag should pass");
+ assertFalse(swarmRegistry.checkMembership(swarmId, keccak256(hex"FFFFFF")), "8-bit invalid tag should fail");
+ }
+
+ function test_RevertIf_checkMembership_swarmNotFound() public {
+ vm.expectRevert(SwarmRegistryUniversalUpgradeable.SwarmNotFound.selector);
+ swarmRegistry.checkMembership(999, keccak256("anything"));
+ }
+
+ function test_checkMembership_allZeroFilter_returnsConsistent() public {
+ uint256 fleetId = _registerFleet(fleetOwner, "f1");
+ uint256 providerId = _registerProvider(providerOwner, "u1");
+
+ // All-zero filter: f1^f2^f3 = 0^0^0 = 0
+ bytes memory filter = new bytes(64);
+ uint256 swarmId = _registerSwarm(fleetOwner, fleetId, providerId, filter, BITS_16, TagType.EDDYSTONE_UID);
+
+ // Should not revert regardless of result
+ swarmRegistry.checkMembership(swarmId, keccak256("test1"));
+ swarmRegistry.checkMembership(swarmId, keccak256("test2"));
+ }
+
+ function test_checkMembership_tinyFilter_returnsFalse() public {
+ uint256 fleetId = _registerFleet(fleetOwner, "f1");
+ uint256 providerId = _registerProvider(providerOwner, "u1");
+
+ // 1-byte filter with 16-bit fingerprint: m = 1/2 = 0, returns false immediately
+ bytes memory filter = new bytes(1);
+ uint256 swarmId = _registerSwarm(fleetOwner, fleetId, providerId, filter, BITS_16, TagType.EDDYSTONE_UID);
+
+ // Should return false (not revert) because m == 0
+ assertFalse(swarmRegistry.checkMembership(swarmId, keccak256("test")), "m=0 should return false");
+ }
+
+ // ==============================
+ // getFilterData
+ // ==============================
+
+ function test_getFilterData_returnsCorrectData() public {
+ uint256 fleetId = _registerFleet(fleetOwner, "f1");
+ uint256 providerId = _registerProvider(providerOwner, "url1");
+
+ bytes memory filter = new bytes(100);
+ filter[0] = 0xFF;
+ filter[99] = 0x01;
+
+ uint256 swarmId = _registerSwarm(fleetOwner, fleetId, providerId, filter, BITS_16, TagType.EDDYSTONE_UID);
+
+ bytes memory stored = swarmRegistry.getFilterData(swarmId);
+ assertEq(stored.length, 100);
+ assertEq(uint8(stored[0]), 0xFF);
+ assertEq(uint8(stored[99]), 0x01);
+ }
+
+ function test_RevertIf_getFilterData_swarmNotFound() public {
+ vm.expectRevert(SwarmRegistryUniversalUpgradeable.SwarmNotFound.selector);
+ swarmRegistry.getFilterData(999);
+ }
+
+ // ==============================
+ // Multiple swarms per fleet
+ // ==============================
+
+ function test_multipleSwarms_sameFleet() public {
+ uint256 fleetId = _registerFleet(fleetOwner, "f1");
+ uint256 providerId1 = _registerProvider(providerOwner, "url1");
+ uint256 providerId2 = _registerProvider(providerOwner, "url2");
+ uint256 providerId3 = _registerProvider(providerOwner, "url3");
+
+ uint256 s1 = _registerSwarm(fleetOwner, fleetId, providerId1, new bytes(32), BITS_8, TagType.EDDYSTONE_UID);
+ uint256 s2 = _registerSwarm(fleetOwner, fleetId, providerId2, new bytes(64), BITS_16, TagType.VENDOR_ID);
+ uint256 s3 =
+ _registerSwarm(fleetOwner, fleetId, providerId3, new bytes(50), BITS_8, TagType.IBEACON_PAYLOAD_ONLY);
+
+ // IDs are distinct hashes
+ assertTrue(s1 != s2 && s2 != s3 && s1 != s3);
+
+ assertEq(swarmRegistry.uuidSwarms(_getFleetUuid(fleetId), 0), s1);
+ assertEq(swarmRegistry.uuidSwarms(_getFleetUuid(fleetId), 1), s2);
+ assertEq(swarmRegistry.uuidSwarms(_getFleetUuid(fleetId), 2), s3);
+ }
+
+ // ==============================
+ // Constants
+ // ==============================
+
+ function test_constants() public view {
+ assertEq(swarmRegistry.MAX_FILTER_SIZE(), 24576);
+ }
+
+ // ==============================
+ // Fuzz
+ // ==============================
+
+ function testFuzz_registerSwarm_filterSizeRange(uint256 size) public {
+ size = bound(size, 1, 24576);
+
+ uint256 fleetId = _registerFleet(fleetOwner, abi.encodePacked("f-", size));
+ uint256 providerId = _registerProvider(providerOwner, string(abi.encodePacked("url-", size)));
+
+ uint256 swarmId =
+ _registerSwarm(fleetOwner, fleetId, providerId, new bytes(size), BITS_8, TagType.EDDYSTONE_UID);
+
+ (,, uint32 storedLen,,,) = swarmRegistry.swarms(swarmId);
+ assertEq(storedLen, uint32(size));
+ }
+
+ // ==============================
+ // updateSwarmProvider
+ // ==============================
+
+ function test_updateSwarmProvider_updatesProviderAndResetsStatus() public {
+ uint256 fleetId = _registerFleet(fleetOwner, "f1");
+ uint256 providerId1 = _registerProvider(providerOwner, "url1");
+ uint256 providerId2 = _registerProvider(providerOwner, "url2");
+
+ uint256 swarmId = _registerSwarm(fleetOwner, fleetId, providerId1, new bytes(50), BITS_8, TagType.EDDYSTONE_UID);
+
+ // Provider accepts
+ vm.prank(providerOwner);
+ swarmRegistry.acceptSwarm(swarmId);
+
+ // Fleet owner updates provider
+ vm.expectEmit(true, true, true, true);
+ emit SwarmProviderUpdated(swarmId, providerId1, providerId2);
+
+ vm.prank(fleetOwner);
+ swarmRegistry.updateSwarmProvider(swarmId, providerId2);
+
+ // Check new provider and status reset
+ (, uint256 newProviderId,,,, SwarmStatus status) = swarmRegistry.swarms(swarmId);
+ assertEq(newProviderId, providerId2);
+ assertEq(uint8(status), uint8(SwarmStatus.REGISTERED));
+ }
+
+ function test_RevertIf_updateSwarmProvider_swarmNotFound() public {
+ uint256 providerId = _registerProvider(providerOwner, "url1");
+
+ vm.prank(fleetOwner);
+ vm.expectRevert(SwarmRegistryUniversalUpgradeable.SwarmNotFound.selector);
+ swarmRegistry.updateSwarmProvider(999, providerId);
+ }
+
+ function test_RevertIf_updateSwarmProvider_notFleetOwner() public {
+ uint256 fleetId = _registerFleet(fleetOwner, "f1");
+ uint256 providerId1 = _registerProvider(providerOwner, "url1");
+ uint256 providerId2 = _registerProvider(providerOwner, "url2");
+
+ uint256 swarmId = _registerSwarm(fleetOwner, fleetId, providerId1, new bytes(50), BITS_8, TagType.EDDYSTONE_UID);
+
+ vm.prank(caller);
+ vm.expectRevert(SwarmRegistryUniversalUpgradeable.NotUuidOwner.selector);
+ swarmRegistry.updateSwarmProvider(swarmId, providerId2);
+ }
+
+ function test_RevertIf_updateSwarmProvider_providerDoesNotExist() public {
+ uint256 fleetId = _registerFleet(fleetOwner, "f1");
+ uint256 providerId = _registerProvider(providerOwner, "url1");
+
+ uint256 swarmId = _registerSwarm(fleetOwner, fleetId, providerId, new bytes(50), BITS_8, TagType.EDDYSTONE_UID);
+
+ vm.prank(fleetOwner);
+ vm.expectRevert(SwarmRegistryUniversalUpgradeable.ProviderDoesNotExist.selector);
+ swarmRegistry.updateSwarmProvider(swarmId, 99999);
+ }
+
+ // ==============================
+ // deleteSwarm
+ // ==============================
+
+ function test_deleteSwarm_removesSwarmAndEmits() public {
+ uint256 fleetId = _registerFleet(fleetOwner, "f1");
+ uint256 providerId = _registerProvider(providerOwner, "url1");
+ uint256 swarmId = _registerSwarm(fleetOwner, fleetId, providerId, new bytes(50), BITS_8, TagType.EDDYSTONE_UID);
+
+ vm.expectEmit(true, true, true, true);
+ emit SwarmDeleted(swarmId, _getFleetUuid(fleetId), fleetOwner);
+
+ vm.prank(fleetOwner);
+ swarmRegistry.deleteSwarm(swarmId);
+
+ // Swarm should be zeroed
+ (bytes16 fleetUuidAfter,, uint32 filterLength,,,) = swarmRegistry.swarms(swarmId);
+ assertEq(fleetUuidAfter, bytes16(0));
+ assertEq(filterLength, 0);
+ }
+
+ function test_deleteSwarm_removesFromUuidSwarms() public {
+ uint256 fleetId = _registerFleet(fleetOwner, "f1");
+ uint256 providerId1 = _registerProvider(providerOwner, "url1");
+ uint256 providerId2 = _registerProvider(providerOwner, "url2");
+
+ // Use different filters to create distinct swarms
+ bytes memory filter1 = new bytes(50);
+ filter1[0] = 0x01;
+ bytes memory filter2 = new bytes(50);
+ filter2[0] = 0x02;
+
+ uint256 swarm1 = _registerSwarm(fleetOwner, fleetId, providerId1, filter1, BITS_8, TagType.EDDYSTONE_UID);
+ uint256 swarm2 = _registerSwarm(fleetOwner, fleetId, providerId2, filter2, BITS_8, TagType.EDDYSTONE_UID);
+
+ // Delete first swarm
+ vm.prank(fleetOwner);
+ swarmRegistry.deleteSwarm(swarm1);
+
+ // Only swarm2 should remain in fleetSwarms
+ assertEq(swarmRegistry.uuidSwarms(_getFleetUuid(fleetId), 0), swarm2);
+ vm.expectRevert();
+ swarmRegistry.uuidSwarms(_getFleetUuid(fleetId), 1); // Should be out of bounds
+ }
+
+ function test_deleteSwarm_swapAndPop() public {
+ uint256 fleetId = _registerFleet(fleetOwner, "f1");
+ uint256 providerId1 = _registerProvider(providerOwner, "url1");
+ uint256 providerId2 = _registerProvider(providerOwner, "url2");
+ uint256 providerId3 = _registerProvider(providerOwner, "url3");
+
+ // Use different filters to create distinct swarms
+ bytes memory filter1 = new bytes(50);
+ filter1[0] = 0x01;
+ bytes memory filter2 = new bytes(50);
+ filter2[0] = 0x02;
+ bytes memory filter3 = new bytes(50);
+ filter3[0] = 0x03;
+
+ uint256 swarm1 = _registerSwarm(fleetOwner, fleetId, providerId1, filter1, BITS_8, TagType.EDDYSTONE_UID);
+ uint256 swarm2 = _registerSwarm(fleetOwner, fleetId, providerId2, filter2, BITS_8, TagType.EDDYSTONE_UID);
+ uint256 swarm3 = _registerSwarm(fleetOwner, fleetId, providerId3, filter3, BITS_8, TagType.EDDYSTONE_UID);
+
+ // Delete middle swarm
+ vm.prank(fleetOwner);
+ swarmRegistry.deleteSwarm(swarm2);
+
+ // swarm3 should be swapped to index 1
+ assertEq(swarmRegistry.uuidSwarms(_getFleetUuid(fleetId), 0), swarm1);
+ assertEq(swarmRegistry.uuidSwarms(_getFleetUuid(fleetId), 1), swarm3);
+ vm.expectRevert();
+ swarmRegistry.uuidSwarms(_getFleetUuid(fleetId), 2); // Should be out of bounds
+ }
+
+ function test_deleteSwarm_clearsFilterData() public {
+ uint256 fleetId = _registerFleet(fleetOwner, "f1");
+ uint256 providerId = _registerProvider(providerOwner, "url1");
+
+ bytes memory filterData = new bytes(50);
+ for (uint256 i = 0; i < 50; i++) {
+ filterData[i] = bytes1(uint8(i));
+ }
+
+ uint256 swarmId = _registerSwarm(fleetOwner, fleetId, providerId, filterData, BITS_8, TagType.EDDYSTONE_UID);
+
+ // Delete swarm
+ vm.prank(fleetOwner);
+ swarmRegistry.deleteSwarm(swarmId);
+
+ // filterLength should be cleared
+ (,, uint32 filterLength,,,) = swarmRegistry.swarms(swarmId);
+ assertEq(filterLength, 0);
+ }
+
+ function test_RevertIf_deleteSwarm_swarmNotFound() public {
+ vm.prank(fleetOwner);
+ vm.expectRevert(SwarmRegistryUniversalUpgradeable.SwarmNotFound.selector);
+ swarmRegistry.deleteSwarm(999);
+ }
+
+ function test_RevertIf_deleteSwarm_notFleetOwner() public {
+ uint256 fleetId = _registerFleet(fleetOwner, "f1");
+ uint256 providerId = _registerProvider(providerOwner, "url1");
+ uint256 swarmId = _registerSwarm(fleetOwner, fleetId, providerId, new bytes(50), BITS_8, TagType.EDDYSTONE_UID);
+
+ vm.prank(caller);
+ vm.expectRevert(SwarmRegistryUniversalUpgradeable.NotUuidOwner.selector);
+ swarmRegistry.deleteSwarm(swarmId);
+ }
+
+ function test_deleteSwarm_afterProviderUpdate() public {
+ uint256 fleetId = _registerFleet(fleetOwner, "f1");
+ uint256 providerId1 = _registerProvider(providerOwner, "url1");
+ uint256 providerId2 = _registerProvider(providerOwner, "url2");
+ uint256 swarmId = _registerSwarm(fleetOwner, fleetId, providerId1, new bytes(50), BITS_8, TagType.EDDYSTONE_UID);
+
+ // Update provider then delete
+ vm.prank(fleetOwner);
+ swarmRegistry.updateSwarmProvider(swarmId, providerId2);
+
+ vm.prank(fleetOwner);
+ swarmRegistry.deleteSwarm(swarmId);
+
+ (bytes16 fleetUuidAfter,,,,,) = swarmRegistry.swarms(swarmId);
+ assertEq(fleetUuidAfter, bytes16(0));
+ }
+
+ function test_deleteSwarm_updatesSwarmIndexInUuid() public {
+ uint256 fleetId = _registerFleet(fleetOwner, "f1");
+ uint256 p1 = _registerProvider(providerOwner, "url1");
+ uint256 p2 = _registerProvider(providerOwner, "url2");
+ uint256 p3 = _registerProvider(providerOwner, "url3");
+
+ // Use different filters to create distinct swarms
+ bytes memory filter1 = new bytes(50);
+ filter1[0] = 0x01;
+ bytes memory filter2 = new bytes(50);
+ filter2[0] = 0x02;
+ bytes memory filter3 = new bytes(50);
+ filter3[0] = 0x03;
+
+ uint256 s1 = _registerSwarm(fleetOwner, fleetId, p1, filter1, BITS_8, TagType.EDDYSTONE_UID);
+ uint256 s2 = _registerSwarm(fleetOwner, fleetId, p2, filter2, BITS_8, TagType.EDDYSTONE_UID);
+ uint256 s3 = _registerSwarm(fleetOwner, fleetId, p3, filter3, BITS_8, TagType.EDDYSTONE_UID);
+
+ // Verify initial indices
+ assertEq(swarmRegistry.swarmIndexInUuid(s1), 0);
+ assertEq(swarmRegistry.swarmIndexInUuid(s2), 1);
+ assertEq(swarmRegistry.swarmIndexInUuid(s3), 2);
+
+ // Delete s1 — s3 should be swapped to index 0
+ vm.prank(fleetOwner);
+ swarmRegistry.deleteSwarm(s1);
+
+ assertEq(swarmRegistry.swarmIndexInUuid(s3), 0);
+ assertEq(swarmRegistry.swarmIndexInUuid(s2), 1);
+ assertEq(swarmRegistry.swarmIndexInUuid(s1), 0); // deleted, reset to 0
+ }
+
+ // ==============================
+ // isSwarmValid
+ // ==============================
+
+ function test_isSwarmValid_bothValid() public {
+ uint256 fleetId = _registerFleet(fleetOwner, "f1");
+ uint256 providerId = _registerProvider(providerOwner, "url1");
+ uint256 swarmId = _registerSwarm(fleetOwner, fleetId, providerId, new bytes(50), BITS_8, TagType.EDDYSTONE_UID);
+
+ (bool fleetValid, bool providerValid) = swarmRegistry.isSwarmValid(swarmId);
+ assertTrue(fleetValid);
+ assertTrue(providerValid);
+ }
+
+ function test_isSwarmValid_providerBurned() public {
+ uint256 fleetId = _registerFleet(fleetOwner, "f1");
+ uint256 providerId = _registerProvider(providerOwner, "url1");
+ uint256 swarmId = _registerSwarm(fleetOwner, fleetId, providerId, new bytes(50), BITS_8, TagType.EDDYSTONE_UID);
+
+ vm.prank(providerOwner);
+ providerContract.burn(providerId);
+
+ (bool fleetValid, bool providerValid) = swarmRegistry.isSwarmValid(swarmId);
+ assertTrue(fleetValid);
+ assertFalse(providerValid);
+ }
+
+ function test_isSwarmValid_fleetBurned() public {
+ uint256 fleetId = _registerFleet(fleetOwner, "f1");
+ uint256 providerId = _registerProvider(providerOwner, "url1");
+ uint256 swarmId = _registerSwarm(fleetOwner, fleetId, providerId, new bytes(50), BITS_8, TagType.EDDYSTONE_UID);
+
+ // Burn registered fleet token (operator = owner for fresh registration)
+ // This mints an owned-only token back to the owner
+ vm.prank(fleetOwner);
+ fleetContract.burn(fleetId);
+
+ // After burning registered token, UUID transitions to Owned state
+ // Need to burn the owned-only token to fully release
+ bytes16 uuid = _getFleetUuid(fleetId);
+ uint256 ownedTokenId = uint256(uint128(uuid)); // owned token has regionKey=0
+ vm.prank(fleetOwner);
+ fleetContract.burn(ownedTokenId);
+
+ (bool fleetValid, bool providerValid) = swarmRegistry.isSwarmValid(swarmId);
+ assertFalse(fleetValid);
+ assertTrue(providerValid);
+ }
+
+ function test_isSwarmValid_bothBurned() public {
+ uint256 fleetId = _registerFleet(fleetOwner, "f1");
+ uint256 providerId = _registerProvider(providerOwner, "url1");
+ uint256 swarmId = _registerSwarm(fleetOwner, fleetId, providerId, new bytes(50), BITS_8, TagType.EDDYSTONE_UID);
+
+ // Burn registered fleet token → mints owned-only token
+ vm.prank(fleetOwner);
+ fleetContract.burn(fleetId);
+
+ // Burn owned-only token to fully release UUID
+ bytes16 uuid = _getFleetUuid(fleetId);
+ uint256 ownedTokenId = uint256(uint128(uuid));
+ vm.prank(fleetOwner);
+ fleetContract.burn(ownedTokenId);
+
+ vm.prank(providerOwner);
+ providerContract.burn(providerId);
+
+ (bool fleetValid, bool providerValid) = swarmRegistry.isSwarmValid(swarmId);
+ assertFalse(fleetValid);
+ assertFalse(providerValid);
+ }
+
+ function test_RevertIf_isSwarmValid_swarmNotFound() public {
+ vm.expectRevert(SwarmRegistryUniversalUpgradeable.SwarmNotFound.selector);
+ swarmRegistry.isSwarmValid(999);
+ }
+
+ // ==============================
+ // purgeOrphanedSwarm
+ // ==============================
+
+ function test_purgeOrphanedSwarm_providerBurned() public {
+ uint256 fleetId = _registerFleet(fleetOwner, "f1");
+ uint256 providerId = _registerProvider(providerOwner, "url1");
+ uint256 swarmId = _registerSwarm(fleetOwner, fleetId, providerId, new bytes(50), BITS_8, TagType.EDDYSTONE_UID);
+
+ vm.prank(providerOwner);
+ providerContract.burn(providerId);
+
+ vm.expectEmit(true, true, true, true);
+ emit SwarmPurged(swarmId, _getFleetUuid(fleetId), caller);
+
+ vm.prank(caller);
+ swarmRegistry.purgeOrphanedSwarm(swarmId);
+
+ (,, uint32 filterLength,,,) = swarmRegistry.swarms(swarmId);
+ assertEq(filterLength, 0);
+ }
+
+ function test_purgeOrphanedSwarm_fleetBurned() public {
+ uint256 fleetId = _registerFleet(fleetOwner, "f1");
+ uint256 providerId = _registerProvider(providerOwner, "url1");
+ uint256 swarmId = _registerSwarm(fleetOwner, fleetId, providerId, new bytes(50), BITS_8, TagType.EDDYSTONE_UID);
+
+ // Burn registered fleet token → mints owned-only token
+ vm.prank(fleetOwner);
+ fleetContract.burn(fleetId);
+
+ // Burn owned-only token to fully release UUID
+ bytes16 uuid = _getFleetUuid(fleetId);
+ uint256 ownedTokenId = uint256(uint128(uuid));
+ vm.prank(fleetOwner);
+ fleetContract.burn(ownedTokenId);
+
+ vm.prank(caller);
+ swarmRegistry.purgeOrphanedSwarm(swarmId);
+
+ (bytes16 fUuid,, uint32 filterLength,,,) = swarmRegistry.swarms(swarmId);
+ assertEq(fUuid, bytes16(0));
+ assertEq(filterLength, 0);
+ }
+
+ function test_purgeOrphanedSwarm_removesFromUuidSwarms() public {
+ uint256 fleetId = _registerFleet(fleetOwner, "f1");
+ uint256 p1 = _registerProvider(providerOwner, "url1");
+ uint256 p2 = _registerProvider(providerOwner, "url2");
+
+ // Use different filters to create distinct swarms
+ bytes memory filter1 = new bytes(50);
+ filter1[0] = 0x01;
+ bytes memory filter2 = new bytes(50);
+ filter2[0] = 0x02;
+
+ uint256 s1 = _registerSwarm(fleetOwner, fleetId, p1, filter1, BITS_8, TagType.EDDYSTONE_UID);
+ uint256 s2 = _registerSwarm(fleetOwner, fleetId, p2, filter2, BITS_8, TagType.EDDYSTONE_UID);
+
+ // Burn provider of s1
+ vm.prank(providerOwner);
+ providerContract.burn(p1);
+
+ vm.prank(caller);
+ swarmRegistry.purgeOrphanedSwarm(s1);
+
+ // s2 should be swapped to index 0
+ assertEq(swarmRegistry.uuidSwarms(_getFleetUuid(fleetId), 0), s2);
+ vm.expectRevert();
+ swarmRegistry.uuidSwarms(_getFleetUuid(fleetId), 1);
+ }
+
+ function test_purgeOrphanedSwarm_clearsFilterData() public {
+ uint256 fleetId = _registerFleet(fleetOwner, "f1");
+ uint256 providerId = _registerProvider(providerOwner, "url1");
+
+ bytes memory filter = new bytes(50);
+ for (uint256 i = 0; i < 50; i++) {
+ filter[i] = bytes1(uint8(i));
+ }
+
+ uint256 swarmId = _registerSwarm(fleetOwner, fleetId, providerId, filter, BITS_8, TagType.EDDYSTONE_UID);
+
+ vm.prank(providerOwner);
+ providerContract.burn(providerId);
+
+ vm.prank(caller);
+ swarmRegistry.purgeOrphanedSwarm(swarmId);
+
+ // filterLength should be cleared
+ (,, uint32 filterLength,,,) = swarmRegistry.swarms(swarmId);
+ assertEq(filterLength, 0);
+ }
+
+ function test_RevertIf_purgeOrphanedSwarm_swarmNotFound() public {
+ vm.expectRevert(SwarmRegistryUniversalUpgradeable.SwarmNotFound.selector);
+ swarmRegistry.purgeOrphanedSwarm(999);
+ }
+
+ function test_RevertIf_purgeOrphanedSwarm_swarmNotOrphaned() public {
+ uint256 fleetId = _registerFleet(fleetOwner, "f1");
+ uint256 providerId = _registerProvider(providerOwner, "url1");
+ uint256 swarmId = _registerSwarm(fleetOwner, fleetId, providerId, new bytes(50), BITS_8, TagType.EDDYSTONE_UID);
+
+ vm.expectRevert(SwarmRegistryUniversalUpgradeable.SwarmNotOrphaned.selector);
+ swarmRegistry.purgeOrphanedSwarm(swarmId);
+ }
+
+ // ==============================
+ // Orphan guards on accept/reject/checkMembership
+ // ==============================
+
+ function test_RevertIf_acceptSwarm_orphaned() public {
+ uint256 fleetId = _registerFleet(fleetOwner, "f1");
+ uint256 providerId = _registerProvider(providerOwner, "url1");
+ uint256 swarmId = _registerSwarm(fleetOwner, fleetId, providerId, new bytes(50), BITS_8, TagType.EDDYSTONE_UID);
+
+ vm.prank(providerOwner);
+ providerContract.burn(providerId);
+
+ vm.prank(providerOwner);
+ vm.expectRevert(SwarmRegistryUniversalUpgradeable.SwarmOrphaned.selector);
+ swarmRegistry.acceptSwarm(swarmId);
+ }
+
+ function test_RevertIf_rejectSwarm_orphaned() public {
+ uint256 fleetId = _registerFleet(fleetOwner, "f1");
+ uint256 providerId = _registerProvider(providerOwner, "url1");
+ uint256 swarmId = _registerSwarm(fleetOwner, fleetId, providerId, new bytes(50), BITS_8, TagType.EDDYSTONE_UID);
+
+ // Burn registered fleet token → mints owned-only token
+ vm.prank(fleetOwner);
+ fleetContract.burn(fleetId);
+
+ // Burn owned-only token to fully release UUID
+ bytes16 uuid = _getFleetUuid(fleetId);
+ uint256 ownedTokenId = uint256(uint128(uuid));
+ vm.prank(fleetOwner);
+ fleetContract.burn(ownedTokenId);
+
+ vm.prank(providerOwner);
+ vm.expectRevert(SwarmRegistryUniversalUpgradeable.SwarmOrphaned.selector);
+ swarmRegistry.rejectSwarm(swarmId);
+ }
+
+ function test_RevertIf_checkMembership_orphaned() public {
+ uint256 fleetId = _registerFleet(fleetOwner, "f1");
+ uint256 providerId = _registerProvider(providerOwner, "url1");
+ uint256 swarmId = _registerSwarm(fleetOwner, fleetId, providerId, new bytes(50), BITS_8, TagType.EDDYSTONE_UID);
+
+ vm.prank(providerOwner);
+ providerContract.burn(providerId);
+
+ vm.expectRevert(SwarmRegistryUniversalUpgradeable.SwarmOrphaned.selector);
+ swarmRegistry.checkMembership(swarmId, keccak256("test"));
+ }
+
+ function test_RevertIf_acceptSwarm_fleetBurned() public {
+ uint256 fleetId = _registerFleet(fleetOwner, "f1");
+ uint256 providerId = _registerProvider(providerOwner, "url1");
+ uint256 swarmId = _registerSwarm(fleetOwner, fleetId, providerId, new bytes(50), BITS_8, TagType.EDDYSTONE_UID);
+
+ // Burn registered fleet token → mints owned-only token
+ vm.prank(fleetOwner);
+ fleetContract.burn(fleetId);
+
+ // Burn owned-only token to fully release UUID
+ bytes16 uuid = _getFleetUuid(fleetId);
+ uint256 ownedTokenId = uint256(uint128(uuid));
+ vm.prank(fleetOwner);
+ fleetContract.burn(ownedTokenId);
+
+ vm.prank(providerOwner);
+ vm.expectRevert(SwarmRegistryUniversalUpgradeable.SwarmOrphaned.selector);
+ swarmRegistry.acceptSwarm(swarmId);
+ }
+
+ function test_purge_thenAcceptReverts() public {
+ uint256 fleetId = _registerFleet(fleetOwner, "f1");
+ uint256 providerId = _registerProvider(providerOwner, "url1");
+ uint256 swarmId = _registerSwarm(fleetOwner, fleetId, providerId, new bytes(50), BITS_8, TagType.EDDYSTONE_UID);
+
+ vm.prank(providerOwner);
+ providerContract.burn(providerId);
+
+ vm.prank(caller);
+ swarmRegistry.purgeOrphanedSwarm(swarmId);
+
+ // After purge, swarm no longer exists
+ vm.prank(providerOwner);
+ vm.expectRevert(SwarmRegistryUniversalUpgradeable.SwarmNotFound.selector);
+ swarmRegistry.acceptSwarm(swarmId);
+ }
+
+ // ==============================
+ // Additional Coverage Tests
+ // ==============================
+
+ function test_checkMembership_8bit_forcedPath() public {
+ // This test ensures the 8-bit _readFingerprint path is exercised
+ // Uses a tagId known to have non-colliding h1, h2, h3 for m=100
+ uint256 fleetId = _registerFleet(fleetOwner, "f8bit");
+ uint256 providerId = _registerProvider(providerOwner, "url8bit");
+
+ // Use 100 bytes for filter
+ uint256 filterLen = 100;
+ bytes memory filter = new bytes(filterLen);
+
+ // For 8-bit, m = filterLen = 100 slots
+ // Pick a tag that's known to have distinct h1, h2, h3
+ bytes memory tagId = abi.encodePacked(uint256(0x12345678));
+ bytes32 tagHash = keccak256(tagId);
+ uint32 m32 = uint32(filterLen);
+
+ uint32 h1 = uint32(uint256(tagHash)) % m32;
+ uint32 h2 = uint32(uint256(tagHash) >> 32) % m32;
+ uint32 h3 = uint32(uint256(tagHash) >> 64) % m32;
+
+ // If there are collisions, try different tag
+ if (h1 == h2 || h1 == h3 || h2 == h3) {
+ tagId = abi.encodePacked(uint256(0xABCDEF01));
+ tagHash = keccak256(tagId);
+ h1 = uint32(uint256(tagHash)) % m32;
+ h2 = uint32(uint256(tagHash) >> 32) % m32;
+ h3 = uint32(uint256(tagHash) >> 64) % m32;
+ }
+
+ // Calculate expected fingerprint (8-bit)
+ uint256 expectedFp = (uint256(tagHash) >> 96) & 0xFF;
+
+ // Write fingerprint to h1 slot (f1 ^ 0 ^ 0 = expectedFp)
+ filter[h1] = bytes1(uint8(expectedFp));
+
+ uint256 swarmId = _registerSwarm(fleetOwner, fleetId, providerId, filter, BITS_8, TagType.EDDYSTONE_UID);
+
+ // This should exercise the 8-bit path in _readFingerprint
+ bool result = swarmRegistry.checkMembership(swarmId, tagHash);
+ assertTrue(result, "8-bit membership check should pass");
+ }
+
+ function test_upgrade_ownerCanUpgrade() public {
+ // Deploy a new implementation
+ SwarmRegistryUniversalUpgradeable newImpl = new SwarmRegistryUniversalUpgradeable();
+
+ // Owner should be able to upgrade (tests _authorizeUpgrade)
+ vm.prank(contractOwner);
+ swarmRegistry.upgradeToAndCall(address(newImpl), "");
+
+ // Verify upgrade succeeded - contract still works
+ assertEq(address(swarmRegistry.FLEET_CONTRACT()), address(fleetContract));
+ }
+
+ function test_RevertIf_upgrade_notOwner() public {
+ SwarmRegistryUniversalUpgradeable newImpl = new SwarmRegistryUniversalUpgradeable();
+
+ vm.prank(caller);
+ vm.expectRevert();
+ swarmRegistry.upgradeToAndCall(address(newImpl), "");
+ }
+
+ function test_checkMembership_mZero_16bit_returnsFalse() public {
+ // Edge case: filter too short for 16-bit -> m = 0 -> return false
+ uint256 fleetId = _registerFleet(fleetOwner, "f0");
+ uint256 providerId = _registerProvider(providerOwner, "url0");
+
+ // 1 byte filter with 16-bit: m = 1/2 = 0
+ bytes memory filter = new bytes(1);
+ uint256 swarmId = _registerSwarm(fleetOwner, fleetId, providerId, filter, BITS_16, TagType.EDDYSTONE_UID);
+
+ // Should return false without reverting
+ assertFalse(swarmRegistry.checkMembership(swarmId, keccak256("anyTag")));
+ }
+
+ function test_registerSwarm_zeroProviderId() public {
+ uint256 fleetId = _registerFleet(fleetOwner, "f1");
+
+ vm.prank(fleetOwner);
+ vm.expectRevert(SwarmRegistryUniversalUpgradeable.ProviderDoesNotExist.selector);
+ swarmRegistry.registerSwarm(_getFleetUuid(fleetId), 0, new bytes(32), BITS_8, TagType.EDDYSTONE_UID);
+ }
+
+ // ==============================
+ // Invalid Enum Values
+ // ==============================
+
+ function test_RevertIf_registerSwarm_invalidFingerprintSize() public {
+ // Solidity 0.8+ reverts with Panic(0x21) for invalid enum conversion
+ uint256 fleetId = _registerFleet(fleetOwner, "f1");
+ uint256 providerId = _registerProvider(providerOwner, "url1");
+ bytes16 uuid = _getFleetUuid(fleetId);
+
+ // Encode call with invalid fpSize (2, but enum only has 0 and 1)
+ bytes memory callData = abi.encodeWithSelector(
+ SwarmRegistryUniversalUpgradeable.registerSwarm.selector,
+ uuid,
+ providerId,
+ new bytes(32),
+ uint8(2), // Invalid FingerprintSize
+ uint8(0) // Valid TagType
+ );
+
+ vm.prank(fleetOwner);
+ (bool success,) = address(swarmRegistry).call(callData);
+ assertFalse(success, "Should revert on invalid FingerprintSize");
+ }
+
+ function test_RevertIf_registerSwarm_invalidTagType() public {
+ // Also verify TagType enum validation
+ uint256 fleetId = _registerFleet(fleetOwner, "f1");
+ uint256 providerId = _registerProvider(providerOwner, "url1");
+ bytes16 uuid = _getFleetUuid(fleetId);
+
+ // Encode call with invalid tagType (5, but enum only has 0-4)
+ bytes memory callData = abi.encodeWithSelector(
+ SwarmRegistryUniversalUpgradeable.registerSwarm.selector,
+ uuid,
+ providerId,
+ new bytes(32),
+ uint8(0), // Valid FingerprintSize
+ uint8(5) // Invalid TagType
+ );
+
+ vm.prank(fleetOwner);
+ (bool success,) = address(swarmRegistry).call(callData);
+ assertFalse(success, "Should revert on invalid TagType");
+ }
+}
diff --git a/test/__helpers__/MockERC20.sol b/test/__helpers__/MockERC20.sol
new file mode 100644
index 00000000..15f8604d
--- /dev/null
+++ b/test/__helpers__/MockERC20.sol
@@ -0,0 +1,22 @@
+// SPDX-License-Identifier: BSD-3-Clause-Clear
+
+pragma solidity ^0.8.24;
+
+import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol";
+
+/// @dev Minimal ERC-20 mock with public mint for testing.
+contract MockERC20 is ERC20 {
+ uint8 private immutable _decimals;
+
+ constructor(string memory name_, string memory symbol_, uint8 decimals_) ERC20(name_, symbol_) {
+ _decimals = decimals_;
+ }
+
+ function decimals() public view override returns (uint8) {
+ return _decimals;
+ }
+
+ function mint(address to, uint256 amount) external {
+ _mint(to, amount);
+ }
+}
diff --git a/test/contentsign/BaseContentSign.t.sol b/test/contentsign/BaseContentSign.t.sol
index b52438b3..ef5a5389 100644
--- a/test/contentsign/BaseContentSign.t.sol
+++ b/test/contentsign/BaseContentSign.t.sol
@@ -2,7 +2,7 @@
pragma solidity ^0.8.20;
-import {Test, console} from "forge-std/Test.sol";
+import {Test} from "forge-std/Test.sol";
import {BaseContentSign} from "../../src/contentsign/BaseContentSign.sol";
contract MockContentSign is BaseContentSign {
diff --git a/test/contentsign/PaymentMiddleware.t.sol b/test/contentsign/PaymentMiddleware.t.sol
index 3d5c24b0..5c7bb6e6 100644
--- a/test/contentsign/PaymentMiddleware.t.sol
+++ b/test/contentsign/PaymentMiddleware.t.sol
@@ -2,14 +2,13 @@
pragma solidity ^0.8.20;
-import {Test, console} from "forge-std/Test.sol";
+import {Test} from "forge-std/Test.sol";
import {BaseContentSign} from "../../src/contentsign/BaseContentSign.sol";
import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import {IERC20Errors} from "@openzeppelin/contracts/interfaces/draft-IERC6093.sol";
import {ERC721} from "@openzeppelin/contracts/token/ERC721/ERC721.sol";
import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol";
import {PaymentMiddleware} from "../../src/contentsign/PaymentMiddleware.sol";
-import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
contract MockToken is ERC20 {
constructor() ERC20("Mock Token", "MTK") {}
diff --git a/test/upgrade-demo/README.md b/test/upgrade-demo/README.md
new file mode 100644
index 00000000..128fbea0
--- /dev/null
+++ b/test/upgrade-demo/README.md
@@ -0,0 +1,213 @@
+# Upgrade Demo
+
+This folder contains a self-contained script demonstrating the UUPS upgrade process for the Swarm contracts on a local chain.
+
+## Contents
+
+| File | Description |
+| -------------------------- | ---------------------------------------------------------------------------------------------- |
+| `TestUpgradeOnAnvil.s.sol` | Forge script with inline V2 mocks that deploys V1, creates state, upgrades to V2, and verifies |
+
+The V2 contracts (`FleetIdentityUpgradeableV2`, `ServiceProviderUpgradeableV2`, `SwarmRegistryL1UpgradeableV2`) are defined **inline** in the script file for simplicity. They inherit from their V1 counterparts and add a `version()` function.
+
+## Prerequisites
+
+1. **anvil** installed (comes with foundry) for L1 mode
+2. **anvil-zksync** installed (comes with foundry-zksync) for ZkSync mode
+3. Contracts compiled with optimizer enabled (see `foundry.toml`)
+
+---
+
+## Managing Anvil Instances
+
+### Check if Anvil is Running
+
+```bash
+# Quick health check - returns chain ID if running
+cast chain-id --rpc-url http://127.0.0.1:8545
+
+# Check what process is using port 8545
+lsof -i :8545
+```
+
+### Stop Existing Anvil
+
+```bash
+# Kill any process on port 8545
+lsof -ti:8545 | xargs kill -9
+
+# Or find and kill by process name
+pkill -f anvil-zksync
+pkill -f anvil
+```
+
+### Verify Node is Healthy
+
+```bash
+# Get current block number
+cast block-number --rpc-url http://127.0.0.1:8545
+
+# Get gas price (confirms RPC responding)
+cast gas-price --rpc-url http://127.0.0.1:8545
+
+# Check test account balance (should be 10000 ETH)
+cast balance 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266 --rpc-url http://127.0.0.1:8545
+```
+
+---
+
+## Running the Upgrade Test
+
+### Option A: L1 Mode (Ethereum Mainnet Simulation) — Recommended
+
+Use this for testing `SwarmRegistryL1Upgradeable` which uses SSTORE2.
+
+> **Important:** SSTORE2 relies on `EXTCODECOPY` which is not supported on ZkSync Era.
+> You must use regular `anvil`, not `anvil-zksync --zksync-os`.
+
+```bash
+# 1. Stop any existing anvil
+lsof -ti:8545 | xargs kill -9 2>/dev/null || true
+
+# 2. Start regular anvil (NOT anvil-zksync with --zksync-os)
+anvil --host 127.0.0.1 --port 8545
+
+# 3. Verify it's running
+cast chain-id --rpc-url http://127.0.0.1:8545
+
+# 4. Run the test (in another terminal)
+forge script test/upgrade-demo/TestUpgradeOnAnvil.s.sol:TestUpgradeOnAnvil \
+ --rpc-url http://127.0.0.1:8545 \
+ --broadcast
+
+# 5. Stop anvil when done
+lsof -ti:8545 | xargs kill -9
+```
+
+### Option B: ZkSync Mode (ZkSync Era Simulation)
+
+Use this for testing with full ZkSync system contracts.
+
+> **Note:** This test script uses `SwarmRegistryL1Upgradeable` which is incompatible with ZkSync
+> due to SSTORE2's reliance on `EXTCODECOPY`. For ZkSync, use `SwarmRegistryUniversalUpgradeable` instead.
+
+```bash
+# 1. Stop any existing anvil
+lsof -ti:8545 | xargs kill -9 2>/dev/null || true
+
+# 2. Start anvil-zksync with ZkSync OS enabled
+~/.foundry/bin/anvil-zksync --host 127.0.0.1 --port 8545 --zksync-os
+
+# 3. Verify it's running (chain ID will be 260 for ZkSync)
+cast chain-id --rpc-url http://127.0.0.1:8545
+
+# 4. Run the test with --zksync flag (in another terminal)
+# NOTE: This will FAIL with SwarmRegistryL1 - use SwarmRegistryUniversal for ZkSync
+forge script test/upgrade-demo/TestUpgradeOnAnvil.s.sol:TestUpgradeOnAnvil \
+ --rpc-url http://127.0.0.1:8545 \
+ --broadcast \
+ --zksync
+
+# 5. Stop anvil when done
+lsof -ti:8545 | xargs kill -9
+```
+
+### Option C: Full L1 + L2 Mode (Bridge Testing)
+
+Use this for testing L1↔L2 interactions (not needed for basic upgrade demo).
+
+```bash
+# Start with both L1 and ZkSync
+~/.foundry/bin/anvil-zksync --host 127.0.0.1 --port 8545 --l1 --zksync-os
+```
+
+---
+
+## Quick Reference
+
+| Task | Command |
+| ----------------- | ---------------------------------------------------------------------- |
+| Check if running | `cast chain-id --rpc-url http://127.0.0.1:8545` |
+| Check port usage | `lsof -i :8545` |
+| Kill on port 8545 | `lsof -ti:8545 \| xargs kill -9` |
+| Kill all anvil | `pkill -f anvil` |
+| Start L1 mode | `anvil --host 127.0.0.1 --port 8545` |
+| Start ZkSync mode | `~/.foundry/bin/anvil-zksync --host 127.0.0.1 --port 8545 --zksync-os` |
+| Health check | `cast block-number --rpc-url http://127.0.0.1:8545` |
+
+---
+
+## Expected Output
+
+The script will:
+
+1. **Deploy V1 contracts** - ServiceProvider, FleetIdentity, SwarmRegistryL1 (all via ERC1967 proxies)
+2. **Verify V1 initializers** - Check owner, ERC721 metadata, bond parameters, and contract references
+3. **Create state** - Register a provider URL and a fleet with bond
+4. **Upgrade to V2** - Deploy V2 implementations and call `upgradeToAndCall()`
+5. **Verify success** - Check `version()` returns "2.0.0" and all state is preserved
+
+```
+=== PHASE 1: Deploy V1 Contracts ===
+ Bond Token: 0x...
+ ServiceProvider Proxy: 0x...
+ FleetIdentity Proxy: 0x...
+ SwarmRegistry Proxy: 0x...
+
+=== PHASE 1B: Verify V1 Initializers ===
+ ServiceProvider V1 Initialization:
+ owner: 0x... [OK]
+ name: Swarm Service Provider [OK]
+ symbol: SSV [OK]
+ FleetIdentity V1 Initialization:
+ owner: 0x... [OK]
+ BOND_TOKEN: 0x... [OK]
+ BASE_BOND: 100000000000000000000 [OK]
+ countryBondMultiplier: 16 [OK]
+ SwarmRegistry V1 Initialization:
+ owner: 0x... [OK]
+ FLEET_CONTRACT: 0x... [OK]
+ PROVIDER_CONTRACT: 0x... [OK]
+
+=== PHASE 2: Create State ===
+ Registered Provider: Token ID: ...
+ Registered Fleet: Token ID: ...
+
+=== PHASE 3: Upgrade to V2 ===
+ Upgraded! (x3)
+
+=== PHASE 4: Verify Upgrade Success ===
+ ServiceProvider version: 2.0.0
+ FleetIdentity version: 2.0.0
+ SwarmRegistry version: 2.0.0
+ Provider URL still valid: true
+ Fleet bond preserved: true
+
+ UPGRADE TEST COMPLETED SUCCESSFULLY
+```
+
+---
+
+## Contract Size Consideration
+
+`FleetIdentityUpgradeable` is a large contract. Ensure optimizer is enabled in `foundry.toml`:
+
+```toml
+via_ir = true
+optimizer = true
+optimizer_runs = 200
+```
+
+Without the optimizer, the contract exceeds the EIP-170 size limit (24,576 bytes) and cannot deploy to L1 Ethereum.
+
+---
+
+## Troubleshooting
+
+| Issue | Cause | Solution |
+| ------------------------------ | --------------------------- | ------------------------------------- |
+| `failed to connect to network` | Anvil not running | Start anvil first |
+| `address already in use` | Port 8545 occupied | `lsof -ti:8545 \| xargs kill -9` |
+| `contract size limit exceeded` | Optimizer disabled | Enable in `foundry.toml` |
+| `EXTCODECOPY not supported` | Using L1 contract on ZkSync | Use L1 mode or SwarmRegistryUniversal |
+| Script hangs | Previous anvil state | Restart anvil fresh |
diff --git a/test/upgrade-demo/TestUpgradeOnAnvil.s.sol b/test/upgrade-demo/TestUpgradeOnAnvil.s.sol
new file mode 100644
index 00000000..51212a01
--- /dev/null
+++ b/test/upgrade-demo/TestUpgradeOnAnvil.s.sol
@@ -0,0 +1,293 @@
+// SPDX-License-Identifier: BSD-3-Clause-Clear
+
+pragma solidity ^0.8.24;
+
+import {Script, console} from "forge-std/Script.sol";
+import {ERC1967Proxy} from "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol";
+import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol";
+
+// V1 contracts
+import {ServiceProviderUpgradeable} from "../../src/swarms/ServiceProviderUpgradeable.sol";
+import {FleetIdentityUpgradeable} from "../../src/swarms/FleetIdentityUpgradeable.sol";
+import {SwarmRegistryL1Upgradeable} from "../../src/swarms/SwarmRegistryL1Upgradeable.sol";
+
+// ═══════════════════════════════════════════════════════════════════════════════
+// Mock V2 Contracts (inline for testing purposes only)
+// ═══════════════════════════════════════════════════════════════════════════════
+
+/// @dev Mock V2 that adds version() - inherits from V1 to preserve storage layout
+contract FleetIdentityUpgradeableV2 is FleetIdentityUpgradeable {
+ function version() external pure returns (string memory) {
+ return "2.0.0";
+ }
+}
+
+/// @dev Mock V2 that adds version() - inherits from V1 to preserve storage layout
+contract ServiceProviderUpgradeableV2 is ServiceProviderUpgradeable {
+ function version() external pure returns (string memory) {
+ return "2.0.0";
+ }
+}
+
+/// @dev Mock V2 that adds version() - inherits from V1 to preserve storage layout
+contract SwarmRegistryL1UpgradeableV2 is SwarmRegistryL1Upgradeable {
+ function version() external pure returns (string memory) {
+ return "2.0.0";
+ }
+}
+
+/// @dev Simple ERC20 for testing bond deposits
+contract MockBondToken is ERC20 {
+ constructor() ERC20("Mock Bond", "MBOND") {
+ _mint(msg.sender, 1_000_000 ether);
+ }
+
+ function mint(address to, uint256 amount) external {
+ _mint(to, amount);
+ }
+}
+
+// ═══════════════════════════════════════════════════════════════════════════════
+// Main Test Script
+// ═══════════════════════════════════════════════════════════════════════════════
+
+/**
+ * @title TestUpgradeOnAnvil
+ * @notice End-to-end script to deploy, use, upgrade, and verify swarm contracts on anvil.
+ *
+ * @dev NOTE: This script uses SwarmRegistryL1Upgradeable which relies on SSTORE2 (EXTCODECOPY).
+ * SSTORE2 is NOT compatible with ZkSync Era. Use regular anvil, not anvil-zksync.
+ *
+ * Usage:
+ * 1. Start regular anvil in a separate terminal:
+ * anvil --host 127.0.0.1 --port 8545
+ *
+ * 2. Run this script:
+ * forge script test/upgrade-demo/TestUpgradeOnAnvil.s.sol:TestUpgradeOnAnvil \
+ * --rpc-url http://127.0.0.1:8545 \
+ * --broadcast
+ */
+contract TestUpgradeOnAnvil is Script {
+ // Use anvil's first default account
+ uint256 constant ANVIL_PRIVATE_KEY = 0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80;
+
+ // Proxy addresses
+ address public serviceProviderProxy;
+ address public fleetIdentityProxy;
+ address public swarmRegistryProxy;
+
+ // Test state
+ uint256 public providerTokenId;
+ uint256 public fleetTokenId;
+
+ function run() external {
+ address deployer = vm.addr(ANVIL_PRIVATE_KEY);
+ console.log("Deployer:", deployer);
+ console.log("Balance:", deployer.balance);
+
+ vm.startBroadcast(ANVIL_PRIVATE_KEY);
+
+ // ═══════════════════════════════════════════
+ // PHASE 1: Deploy Mock Token & V1 Contracts
+ // ═══════════════════════════════════════════
+ console.log("\n=== PHASE 1: Deploy V1 Contracts ===\n");
+
+ // Deploy mock bond token
+ MockBondToken bondToken = new MockBondToken();
+ console.log("Bond Token:", address(bondToken));
+
+ uint256 baseBond = 100 ether;
+
+ // Deploy ServiceProvider V1
+ console.log("\nDeploying ServiceProviderUpgradeable V1...");
+ ServiceProviderUpgradeable spImpl = new ServiceProviderUpgradeable();
+ ERC1967Proxy spProxy = new ERC1967Proxy(
+ address(spImpl),
+ abi.encodeCall(ServiceProviderUpgradeable.initialize, (deployer))
+ );
+ serviceProviderProxy = address(spProxy);
+ console.log(" ServiceProvider Proxy:", serviceProviderProxy);
+ console.log(" ServiceProvider Impl V1:", address(spImpl));
+
+ // Deploy FleetIdentity V1
+ console.log("\nDeploying FleetIdentityUpgradeable V1...");
+ FleetIdentityUpgradeable fiImpl = new FleetIdentityUpgradeable();
+ ERC1967Proxy fiProxy = new ERC1967Proxy(
+ address(fiImpl),
+ abi.encodeCall(FleetIdentityUpgradeable.initialize, (deployer, address(bondToken), baseBond, 0))
+ );
+ fleetIdentityProxy = address(fiProxy);
+ console.log(" FleetIdentity Proxy:", fleetIdentityProxy);
+ console.log(" FleetIdentity Impl V1:", address(fiImpl));
+
+ // Deploy SwarmRegistryL1 V1
+ console.log("\nDeploying SwarmRegistryL1Upgradeable V1...");
+ SwarmRegistryL1Upgradeable srImpl = new SwarmRegistryL1Upgradeable();
+ ERC1967Proxy srProxy = new ERC1967Proxy(
+ address(srImpl),
+ abi.encodeCall(SwarmRegistryL1Upgradeable.initialize, (fleetIdentityProxy, serviceProviderProxy, deployer))
+ );
+ swarmRegistryProxy = address(srProxy);
+ console.log(" SwarmRegistry Proxy:", swarmRegistryProxy);
+ console.log(" SwarmRegistry Impl V1:", address(srImpl));
+
+ // ═══════════════════════════════════════════
+ // PHASE 1B: Verify V1 Initializers
+ // ═══════════════════════════════════════════
+ console.log("\n=== PHASE 1B: Verify V1 Initializers ===\n");
+
+ // Verify ServiceProvider initialization
+ ServiceProviderUpgradeable sp = ServiceProviderUpgradeable(serviceProviderProxy);
+ console.log("ServiceProvider V1 Initialization:");
+ require(sp.owner() == deployer, "SP: owner not initialized correctly");
+ console.log(" owner:", sp.owner(), "[OK]");
+ require(keccak256(bytes(sp.name())) == keccak256(bytes("Swarm Service Provider")), "SP: name mismatch");
+ console.log(" name:", sp.name(), "[OK]");
+ require(keccak256(bytes(sp.symbol())) == keccak256(bytes("SSV")), "SP: symbol mismatch");
+ console.log(" symbol:", sp.symbol(), "[OK]");
+
+ // Verify FleetIdentity initialization
+ FleetIdentityUpgradeable fi = FleetIdentityUpgradeable(fleetIdentityProxy);
+ console.log("\nFleetIdentity V1 Initialization:");
+ require(fi.owner() == deployer, "FI: owner not initialized correctly");
+ console.log(" owner:", fi.owner(), "[OK]");
+ require(address(fi.BOND_TOKEN()) == address(bondToken), "FI: BOND_TOKEN mismatch");
+ console.log(" BOND_TOKEN:", address(fi.BOND_TOKEN()), "[OK]");
+ require(fi.BASE_BOND() == baseBond, "FI: BASE_BOND mismatch");
+ console.log(" BASE_BOND:", fi.BASE_BOND(), "[OK]");
+ require(fi.countryBondMultiplier() == 16, "FI: countryBondMultiplier mismatch");
+ console.log(" countryBondMultiplier:", fi.countryBondMultiplier(), "[OK]");
+ require(keccak256(bytes(fi.name())) == keccak256(bytes("Swarm Fleet Identity")), "FI: name mismatch");
+ console.log(" name:", fi.name(), "[OK]");
+ require(keccak256(bytes(fi.symbol())) == keccak256(bytes("SFID")), "FI: symbol mismatch");
+ console.log(" symbol:", fi.symbol(), "[OK]");
+
+ // Verify SwarmRegistry initialization
+ SwarmRegistryL1Upgradeable sr = SwarmRegistryL1Upgradeable(swarmRegistryProxy);
+ console.log("\nSwarmRegistry V1 Initialization:");
+ require(sr.owner() == deployer, "SR: owner not initialized correctly");
+ console.log(" owner:", sr.owner(), "[OK]");
+ require(address(sr.FLEET_CONTRACT()) == fleetIdentityProxy, "SR: FLEET_CONTRACT mismatch");
+ console.log(" FLEET_CONTRACT:", address(sr.FLEET_CONTRACT()), "[OK]");
+ require(address(sr.PROVIDER_CONTRACT()) == serviceProviderProxy, "SR: PROVIDER_CONTRACT mismatch");
+ console.log(" PROVIDER_CONTRACT:", address(sr.PROVIDER_CONTRACT()), "[OK]");
+
+ console.log("\nAll V1 initializers verified successfully!");
+
+ // ═══════════════════════════════════════════
+ // PHASE 2: Create State (register provider & fleet)
+ // ═══════════════════════════════════════════
+ console.log("\n=== PHASE 2: Create State ===\n");
+
+ // Register a provider
+ string memory providerUrl = "https://api.example.com";
+ providerTokenId = sp.registerProvider(providerUrl);
+ console.log("Registered Provider:");
+ console.log(" Token ID:", providerTokenId);
+ console.log(" URL:", sp.providerUrls(providerTokenId));
+ console.log(" Owner:", sp.ownerOf(providerTokenId));
+
+ // Approve bond token for fleet
+ bondToken.approve(fleetIdentityProxy, type(uint256).max);
+ console.log("\nBond token approved for FleetIdentity");
+
+ // Register a fleet
+ bytes16 fleetUuid = bytes16(keccak256("test-fleet-uuid"));
+ uint16 countryCode = 840; // US
+ uint16 adminCode = 6; // CA
+ fleetTokenId = fi.registerFleetLocal(fleetUuid, countryCode, adminCode, 0);
+ console.log("\nRegistered Fleet:");
+ console.log(" Token ID:", fleetTokenId);
+ console.log(" Bond deposited:", fi.bonds(fleetTokenId));
+
+ // Verify state before upgrade
+ console.log("\n--- Pre-Upgrade State Verification ---");
+ console.log("ServiceProvider owner:", sp.owner());
+ console.log("FleetIdentity BASE_BOND:", fi.BASE_BOND());
+ console.log("Provider token exists:", sp.ownerOf(providerTokenId) == deployer);
+ console.log("Fleet token exists:", fi.ownerOf(fleetTokenId) == deployer);
+
+ // ═══════════════════════════════════════════
+ // PHASE 3: Upgrade to V2
+ // ═══════════════════════════════════════════
+ console.log("\n=== PHASE 3: Upgrade to V2 ===\n");
+
+ // Deploy V2 implementations (defined inline above)
+ console.log("Deploying V2 implementations...");
+ ServiceProviderUpgradeableV2 spImplV2 = new ServiceProviderUpgradeableV2();
+ FleetIdentityUpgradeableV2 fiImplV2 = new FleetIdentityUpgradeableV2();
+ SwarmRegistryL1UpgradeableV2 srImplV2 = new SwarmRegistryL1UpgradeableV2();
+
+ console.log(" ServiceProvider Impl V2:", address(spImplV2));
+ console.log(" FleetIdentity Impl V2:", address(fiImplV2));
+ console.log(" SwarmRegistry Impl V2:", address(srImplV2));
+
+ // Upgrade ServiceProvider
+ console.log("\nUpgrading ServiceProvider to V2...");
+ ServiceProviderUpgradeable(serviceProviderProxy).upgradeToAndCall(address(spImplV2), "");
+ console.log(" Upgraded!");
+
+ // Upgrade FleetIdentity
+ console.log("Upgrading FleetIdentity to V2...");
+ FleetIdentityUpgradeable(fleetIdentityProxy).upgradeToAndCall(address(fiImplV2), "");
+ console.log(" Upgraded!");
+
+ // Upgrade SwarmRegistry
+ console.log("Upgrading SwarmRegistry to V2...");
+ SwarmRegistryL1Upgradeable(swarmRegistryProxy).upgradeToAndCall(address(srImplV2), "");
+ console.log(" Upgraded!");
+
+ // ═══════════════════════════════════════════
+ // PHASE 4: Verify Upgrade Success
+ // ═══════════════════════════════════════════
+ console.log("\n=== PHASE 4: Verify Upgrade Success ===\n");
+
+ // Cast proxies to V2 interfaces
+ ServiceProviderUpgradeableV2 spV2 = ServiceProviderUpgradeableV2(serviceProviderProxy);
+ FleetIdentityUpgradeableV2 fiV2 = FleetIdentityUpgradeableV2(fleetIdentityProxy);
+ SwarmRegistryL1UpgradeableV2 srV2 = SwarmRegistryL1UpgradeableV2(swarmRegistryProxy);
+
+ // Check versions
+ console.log("--- Version Check ---");
+ console.log("ServiceProvider version:", spV2.version());
+ console.log("FleetIdentity version:", fiV2.version());
+ console.log("SwarmRegistry version:", srV2.version());
+
+ // Verify state preserved
+ console.log("\n--- State Preservation Check ---");
+ console.log("Provider URL still valid:", keccak256(bytes(spV2.providerUrls(providerTokenId))) == keccak256(bytes(providerUrl)));
+ console.log("Provider owner unchanged:", spV2.ownerOf(providerTokenId) == deployer);
+ console.log("Fleet bond preserved:", fiV2.bonds(fleetTokenId) == baseBond);
+ console.log("Fleet owner unchanged:", fiV2.ownerOf(fleetTokenId) == deployer);
+ console.log("Contract owner unchanged:", spV2.owner() == deployer);
+
+ // Test that contracts still work after upgrade
+ console.log("\n--- Post-Upgrade Functionality Test ---");
+
+ // Register another provider
+ uint256 newProviderId = spV2.registerProvider("https://api2.example.com");
+ console.log("New provider registered after upgrade, ID:", newProviderId);
+
+ // Register another fleet
+ bytes16 fleetUuid2 = bytes16(keccak256("test-fleet-uuid-2"));
+ uint256 newFleetId = fiV2.registerFleetLocal(fleetUuid2, countryCode, adminCode, 0);
+ console.log("New fleet registered after upgrade, ID:", newFleetId);
+
+ vm.stopBroadcast();
+
+ // ═══════════════════════════════════════════
+ // Final Summary
+ // ═══════════════════════════════════════════
+ console.log("\n========================================");
+ console.log(" UPGRADE TEST COMPLETED SUCCESSFULLY");
+ console.log("========================================");
+ console.log("- All V1 contracts deployed");
+ console.log("- V1 initializers verified (owner, params, ERC721)");
+ console.log("- State created (provider + fleet)");
+ console.log("- Upgraded to V2 implementations");
+ console.log("- Version functions return '2.0.0'");
+ console.log("- All state preserved after upgrade");
+ console.log("- Post-upgrade operations work");
+ console.log("========================================\n");
+ }
+}
diff --git a/test/upgradeable/UpgradeableContracts.t.sol b/test/upgradeable/UpgradeableContracts.t.sol
new file mode 100644
index 00000000..9b4b23d6
--- /dev/null
+++ b/test/upgradeable/UpgradeableContracts.t.sol
@@ -0,0 +1,441 @@
+// SPDX-License-Identifier: BSD-3-Clause-Clear
+
+pragma solidity ^0.8.24;
+
+import {Test, console} from "forge-std/Test.sol";
+import {ERC1967Proxy} from "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol";
+import {Initializable} from "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
+import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
+
+import {ServiceProviderUpgradeable} from "../../src/swarms/ServiceProviderUpgradeable.sol";
+import {FleetIdentityUpgradeable} from "../../src/swarms/FleetIdentityUpgradeable.sol";
+import {SwarmRegistryUniversalUpgradeable} from "../../src/swarms/SwarmRegistryUniversalUpgradeable.sol";
+import {SwarmStatus, TagType, FingerprintSize} from "../../src/swarms/interfaces/SwarmTypes.sol";
+
+import {MockERC20} from "../__helpers__/MockERC20.sol";
+
+/**
+ * @title ServiceProviderUpgradeableV2Mock
+ * @notice Mock V2 implementation for testing upgrades
+ */
+contract ServiceProviderUpgradeableV2Mock is ServiceProviderUpgradeable {
+ // New V2 storage
+ mapping(uint256 => uint256) public providerScores;
+ uint256 public v2InitializedAt;
+
+ // Reduce gap from 49 to 47 (added 2 slots)
+ uint256[47] private __gap_v2;
+
+ function initializeV2() external reinitializer(2) {
+ v2InitializedAt = block.timestamp;
+ }
+
+ function setProviderScore(uint256 tokenId, uint256 score) external {
+ if (ownerOf(tokenId) != msg.sender) revert("Not owner");
+ providerScores[tokenId] = score;
+ }
+
+ function version() external pure returns (string memory) {
+ return "V2";
+ }
+}
+
+/**
+ * @title FleetIdentityUpgradeableV2Mock
+ * @notice Mock V2 implementation for testing upgrades
+ */
+contract FleetIdentityUpgradeableV2Mock is FleetIdentityUpgradeable {
+ // New V2 storage
+ mapping(uint256 => string) public fleetMetadata;
+ uint256 public v2InitializedAt;
+
+ // Reduce gap from 40 to 38 (added 2 slots)
+ uint256[38] private __gap_v2;
+
+ function initializeV2() external reinitializer(2) {
+ v2InitializedAt = block.timestamp;
+ }
+
+ function setFleetMetadata(uint256 tokenId, string calldata metadata) external {
+ if (ownerOf(tokenId) != msg.sender) revert("Not owner");
+ fleetMetadata[tokenId] = metadata;
+ }
+
+ function version() external pure returns (string memory) {
+ return "V2";
+ }
+}
+
+/**
+ * @title SwarmRegistryUniversalUpgradeableV2Mock
+ * @notice Mock V2 implementation for testing upgrades
+ */
+contract SwarmRegistryUniversalUpgradeableV2Mock is SwarmRegistryUniversalUpgradeable {
+ // New V2 storage
+ mapping(bytes32 => bool) public swarmPaused;
+ uint256 public v2InitializedAt;
+
+ // Reduce gap from 44 to 42 (added 2 slots)
+ uint256[42] private __gap_v2;
+
+ function initializeV2() external reinitializer(2) {
+ v2InitializedAt = block.timestamp;
+ }
+
+ function pauseSwarm(bytes32 swarmId) external {
+ swarmPaused[swarmId] = true;
+ }
+
+ function version() external pure returns (string memory) {
+ return "V2";
+ }
+}
+
+/**
+ * @title UpgradeableContractsTest
+ * @notice Tests for UUPS upgradeable swarm contracts
+ */
+contract UpgradeableContractsTest is Test {
+ // Contracts
+ ServiceProviderUpgradeable public serviceProviderImpl;
+ ServiceProviderUpgradeable public serviceProvider;
+ address public serviceProviderProxy;
+
+ FleetIdentityUpgradeable public fleetIdentityImpl;
+ FleetIdentityUpgradeable public fleetIdentity;
+ address public fleetIdentityProxy;
+
+ SwarmRegistryUniversalUpgradeable public swarmRegistryImpl;
+ SwarmRegistryUniversalUpgradeable public swarmRegistry;
+ address public swarmRegistryProxy;
+
+ // Mock token
+ MockERC20 public bondToken;
+
+ // Actors
+ address public owner = address(0x1111);
+ address public alice = address(0xA11CE);
+ address public bob = address(0xB0B);
+ address public attacker = address(0xBAD);
+
+ // Constants
+ uint256 constant BASE_BOND = 1000e18;
+
+ function setUp() public {
+ // Deploy mock token
+ bondToken = new MockERC20("Mock Token", "MOCK", 18);
+ bondToken.mint(alice, 1_000_000e18);
+ bondToken.mint(bob, 1_000_000e18);
+
+ // Deploy ServiceProvider
+ serviceProviderImpl = new ServiceProviderUpgradeable();
+ serviceProviderProxy = address(
+ new ERC1967Proxy(
+ address(serviceProviderImpl),
+ abi.encodeCall(ServiceProviderUpgradeable.initialize, (owner))
+ )
+ );
+ serviceProvider = ServiceProviderUpgradeable(serviceProviderProxy);
+
+ // Deploy FleetIdentity
+ fleetIdentityImpl = new FleetIdentityUpgradeable();
+ fleetIdentityProxy = address(
+ new ERC1967Proxy(
+ address(fleetIdentityImpl),
+ abi.encodeCall(FleetIdentityUpgradeable.initialize, (owner, address(bondToken), BASE_BOND, 0))
+ )
+ );
+ fleetIdentity = FleetIdentityUpgradeable(fleetIdentityProxy);
+
+ // Deploy SwarmRegistry
+ swarmRegistryImpl = new SwarmRegistryUniversalUpgradeable();
+ swarmRegistryProxy = address(
+ new ERC1967Proxy(
+ address(swarmRegistryImpl),
+ abi.encodeCall(
+ SwarmRegistryUniversalUpgradeable.initialize,
+ (fleetIdentityProxy, serviceProviderProxy, owner)
+ )
+ )
+ );
+ swarmRegistry = SwarmRegistryUniversalUpgradeable(swarmRegistryProxy);
+ }
+
+ // =========================================================================
+ // ServiceProvider Initialization Tests
+ // =========================================================================
+
+ function test_ServiceProvider_InitializesCorrectly() public view {
+ assertEq(serviceProvider.owner(), owner);
+ assertEq(serviceProvider.name(), "Swarm Service Provider");
+ assertEq(serviceProvider.symbol(), "SSV");
+ }
+
+ function test_ServiceProvider_CannotReinitialize() public {
+ vm.expectRevert(Initializable.InvalidInitialization.selector);
+ serviceProvider.initialize(attacker);
+ }
+
+ function test_ServiceProvider_ImplementationCannotBeInitialized() public {
+ vm.expectRevert(Initializable.InvalidInitialization.selector);
+ serviceProviderImpl.initialize(attacker);
+ }
+
+ // =========================================================================
+ // ServiceProvider Upgrade Tests
+ // =========================================================================
+
+ function test_ServiceProvider_OwnerCanUpgrade() public {
+ // Create some state before upgrade
+ vm.startPrank(alice);
+ uint256 tokenId = serviceProvider.registerProvider("https://alice.example.com/api");
+ vm.stopPrank();
+
+ // Deploy V2 and upgrade
+ ServiceProviderUpgradeableV2Mock v2Impl = new ServiceProviderUpgradeableV2Mock();
+
+ vm.prank(owner);
+ serviceProvider.upgradeToAndCall(
+ address(v2Impl), abi.encodeCall(ServiceProviderUpgradeableV2Mock.initializeV2, ())
+ );
+
+ // Verify upgrade
+ ServiceProviderUpgradeableV2Mock v2 = ServiceProviderUpgradeableV2Mock(serviceProviderProxy);
+ assertEq(v2.version(), "V2");
+ assertGt(v2.v2InitializedAt(), 0);
+
+ // Verify old state preserved
+ assertEq(v2.ownerOf(tokenId), alice);
+ assertEq(v2.providerUrls(tokenId), "https://alice.example.com/api");
+
+ // New V2 functionality works
+ vm.prank(alice);
+ v2.setProviderScore(tokenId, 100);
+ assertEq(v2.providerScores(tokenId), 100);
+ }
+
+ function test_ServiceProvider_NonOwnerCannotUpgrade() public {
+ ServiceProviderUpgradeableV2Mock v2Impl = new ServiceProviderUpgradeableV2Mock();
+
+ vm.prank(attacker);
+ vm.expectRevert();
+ serviceProvider.upgradeToAndCall(address(v2Impl), "");
+ }
+
+ function test_ServiceProvider_StoragePersistsAfterUpgrade() public {
+ // Create multiple tokens
+ vm.startPrank(alice);
+ uint256 aliceToken = serviceProvider.registerProvider("https://alice.example.com");
+ vm.stopPrank();
+
+ vm.startPrank(bob);
+ uint256 bobToken = serviceProvider.registerProvider("https://bob.example.com");
+ vm.stopPrank();
+
+ // Upgrade
+ ServiceProviderUpgradeableV2Mock v2Impl = new ServiceProviderUpgradeableV2Mock();
+ vm.prank(owner);
+ serviceProvider.upgradeToAndCall(address(v2Impl), "");
+
+ ServiceProviderUpgradeableV2Mock v2 = ServiceProviderUpgradeableV2Mock(serviceProviderProxy);
+
+ // Verify all state
+ assertEq(v2.ownerOf(aliceToken), alice);
+ assertEq(v2.ownerOf(bobToken), bob);
+ assertEq(v2.providerUrls(aliceToken), "https://alice.example.com");
+ assertEq(v2.providerUrls(bobToken), "https://bob.example.com");
+ assertEq(v2.owner(), owner);
+ }
+
+ // =========================================================================
+ // FleetIdentity Initialization Tests
+ // =========================================================================
+
+ function test_FleetIdentity_InitializesCorrectly() public view {
+ assertEq(fleetIdentity.owner(), owner);
+ assertEq(address(fleetIdentity.BOND_TOKEN()), address(bondToken));
+ assertEq(fleetIdentity.BASE_BOND(), BASE_BOND);
+ assertEq(fleetIdentity.name(), "Swarm Fleet Identity");
+ assertEq(fleetIdentity.symbol(), "SFID");
+ }
+
+ function test_FleetIdentity_CannotReinitialize() public {
+ vm.expectRevert(Initializable.InvalidInitialization.selector);
+ fleetIdentity.initialize(attacker, address(bondToken), BASE_BOND, 0);
+ }
+
+ function test_FleetIdentity_ImplementationCannotBeInitialized() public {
+ vm.expectRevert(Initializable.InvalidInitialization.selector);
+ fleetIdentityImpl.initialize(attacker, address(bondToken), BASE_BOND, 0);
+ }
+
+ // =========================================================================
+ // FleetIdentity Upgrade Tests
+ // =========================================================================
+
+ function test_FleetIdentity_OwnerCanUpgrade() public {
+ // Approve and claim a UUID (simplest operation)
+ vm.startPrank(alice);
+ bondToken.approve(address(fleetIdentity), BASE_BOND);
+ bytes16 uuid1 = bytes16(keccak256("test-fleet-1"));
+ uint256 tokenId = fleetIdentity.claimUuid(uuid1, address(0));
+ vm.stopPrank();
+
+ // Deploy V2 and upgrade
+ FleetIdentityUpgradeableV2Mock v2Impl = new FleetIdentityUpgradeableV2Mock();
+
+ vm.prank(owner);
+ fleetIdentity.upgradeToAndCall(address(v2Impl), abi.encodeCall(FleetIdentityUpgradeableV2Mock.initializeV2, ()));
+
+ // Verify upgrade
+ FleetIdentityUpgradeableV2Mock v2 = FleetIdentityUpgradeableV2Mock(fleetIdentityProxy);
+ assertEq(v2.version(), "V2");
+ assertGt(v2.v2InitializedAt(), 0);
+
+ // Verify old state preserved
+ assertEq(v2.ownerOf(tokenId), alice);
+
+ // Both old and new functionality work
+ vm.prank(alice);
+ v2.setFleetMetadata(tokenId, "metadata://test");
+ assertEq(v2.fleetMetadata(tokenId), "metadata://test");
+ }
+
+ function test_FleetIdentity_NonOwnerCannotUpgrade() public {
+ FleetIdentityUpgradeableV2Mock v2Impl = new FleetIdentityUpgradeableV2Mock();
+
+ vm.prank(attacker);
+ vm.expectRevert();
+ fleetIdentity.upgradeToAndCall(address(v2Impl), "");
+ }
+
+ function test_FleetIdentity_BondStatePersistsAfterUpgrade() public {
+ // Claim UUIDs (each costs BASE_BOND)
+ vm.startPrank(alice);
+ bondToken.approve(address(fleetIdentity), BASE_BOND * 2);
+ bytes16 uuid1 = bytes16(keccak256("fleet-1"));
+ bytes16 uuid2 = bytes16(keccak256("fleet-2"));
+ uint256 token1 = fleetIdentity.claimUuid(uuid1, address(0));
+ uint256 token2 = fleetIdentity.claimUuid(uuid2, address(0));
+ vm.stopPrank();
+
+ uint256 bond1 = fleetIdentity.bonds(token1);
+ uint256 bond2 = fleetIdentity.bonds(token2);
+
+ // Upgrade
+ FleetIdentityUpgradeableV2Mock v2Impl = new FleetIdentityUpgradeableV2Mock();
+ vm.prank(owner);
+ fleetIdentity.upgradeToAndCall(address(v2Impl), "");
+
+ FleetIdentityUpgradeableV2Mock v2 = FleetIdentityUpgradeableV2Mock(fleetIdentityProxy);
+
+ // Verify bonds match
+ assertEq(v2.bonds(token1), bond1);
+ assertEq(v2.bonds(token2), bond2);
+ assertEq(address(v2.BOND_TOKEN()), address(bondToken));
+ assertEq(v2.BASE_BOND(), BASE_BOND);
+ }
+
+ // =========================================================================
+ // SwarmRegistry Initialization Tests
+ // =========================================================================
+
+ function test_SwarmRegistry_InitializesCorrectly() public view {
+ assertEq(swarmRegistry.owner(), owner);
+ assertEq(address(swarmRegistry.FLEET_CONTRACT()), fleetIdentityProxy);
+ assertEq(address(swarmRegistry.PROVIDER_CONTRACT()), serviceProviderProxy);
+ }
+
+ function test_SwarmRegistry_CannotReinitialize() public {
+ vm.expectRevert(Initializable.InvalidInitialization.selector);
+ swarmRegistry.initialize(address(fleetIdentity), address(serviceProvider), attacker);
+ }
+
+ function test_SwarmRegistry_ImplementationCannotBeInitialized() public {
+ vm.expectRevert(Initializable.InvalidInitialization.selector);
+ swarmRegistryImpl.initialize(address(fleetIdentity), address(serviceProvider), attacker);
+ }
+
+ // =========================================================================
+ // SwarmRegistry Upgrade Tests
+ // =========================================================================
+
+ function test_SwarmRegistry_OwnerCanUpgrade() public {
+ // Claim UUID first to get a fleet ID
+ vm.startPrank(alice);
+ bondToken.approve(address(fleetIdentity), BASE_BOND);
+ bytes16 uuid = bytes16(keccak256("swarm-test-fleet"));
+ uint256 fleetId = fleetIdentity.claimUuid(uuid, address(0));
+ vm.stopPrank();
+
+ vm.startPrank(bob);
+ uint256 providerId = serviceProvider.registerProvider("https://bob.example.com");
+ vm.stopPrank();
+
+ // Register swarm with correct parameters
+ bytes memory filterData = hex"0102030405";
+ FingerprintSize fpSize = FingerprintSize.BITS_16;
+ TagType tagType = TagType.IBEACON_PAYLOAD_ONLY;
+
+ vm.prank(alice);
+ uint256 swarmId = swarmRegistry.registerSwarm(uuid, providerId, filterData, fpSize, tagType);
+
+ // Deploy V2 and upgrade
+ SwarmRegistryUniversalUpgradeableV2Mock v2Impl = new SwarmRegistryUniversalUpgradeableV2Mock();
+
+ vm.prank(owner);
+ swarmRegistry.upgradeToAndCall(
+ address(v2Impl), abi.encodeCall(SwarmRegistryUniversalUpgradeableV2Mock.initializeV2, ())
+ );
+
+ // Verify upgrade
+ SwarmRegistryUniversalUpgradeableV2Mock v2 = SwarmRegistryUniversalUpgradeableV2Mock(swarmRegistryProxy);
+ assertEq(v2.version(), "V2");
+ assertGt(v2.v2InitializedAt(), 0);
+
+ // Verify old state preserved - swarm still exists via public mapping
+ (bytes16 storedUuid, uint256 storedProviderId,,,,) = v2.swarms(swarmId);
+ assertEq(storedUuid, uuid);
+ assertEq(storedProviderId, providerId);
+
+ // New V2 functionality works
+ v2.pauseSwarm(bytes32(swarmId));
+ assertTrue(v2.swarmPaused(bytes32(swarmId)));
+ }
+
+ function test_SwarmRegistry_NonOwnerCannotUpgrade() public {
+ SwarmRegistryUniversalUpgradeableV2Mock v2Impl = new SwarmRegistryUniversalUpgradeableV2Mock();
+
+ vm.prank(attacker);
+ vm.expectRevert();
+ swarmRegistry.upgradeToAndCall(address(v2Impl), "");
+ }
+
+ // =========================================================================
+ // Fuzz Tests
+ // =========================================================================
+
+ function testFuzz_ServiceProvider_MultipleUpgradesPreserveState(uint8 registrations) public {
+ vm.assume(registrations > 0 && registrations < 10);
+
+ // Register multiple providers
+ uint256[] memory tokenIds = new uint256[](registrations);
+ for (uint256 i = 0; i < registrations; i++) {
+ address user = address(uint160(0x1000 + i));
+ vm.prank(user);
+ tokenIds[i] = serviceProvider.registerProvider(string(abi.encodePacked("https://", i, ".example.com")));
+ }
+
+ // Upgrade to V2
+ ServiceProviderUpgradeableV2Mock v2Impl = new ServiceProviderUpgradeableV2Mock();
+ vm.prank(owner);
+ serviceProvider.upgradeToAndCall(address(v2Impl), "");
+
+ // Verify all tokens still exist with correct owners
+ ServiceProviderUpgradeableV2Mock v2 = ServiceProviderUpgradeableV2Mock(serviceProviderProxy);
+ for (uint256 i = 0; i < registrations; i++) {
+ address expectedOwner = address(uint160(0x1000 + i));
+ assertEq(v2.ownerOf(tokenIds[i]), expectedOwner);
+ }
+ }
+}
diff --git a/yarn.lock b/yarn.lock
index afcdcbdb..e5bdbfd8 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -7,6 +7,19 @@
resolved "https://registry.npmjs.org/@adraffy/ens-normalize/-/ens-normalize-1.10.1.tgz"
integrity sha512-96Z2IP3mYmF1Xg2cDm8f1gWGf/HUVedQ3FMifV4kG/PQ4yEP51xDtRAEfhVNt5f/uzpNkZHwWQuUcu6D6K+Ekw==
+"@alloc/quick-lru@^5.2.0":
+ version "5.2.0"
+ resolved "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz"
+ integrity sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==
+
+"@antfu/install-pkg@^1.1.0":
+ version "1.1.0"
+ resolved "https://registry.npmjs.org/@antfu/install-pkg/-/install-pkg-1.1.0.tgz"
+ integrity sha512-MGQsmw10ZyI+EJo45CdSER4zEb+p31LpDAFp2Z3gkSd1yqVZGi0Ebx++YTEMonJy4oChEMLsxZ64j8FH6sSqtQ==
+ dependencies:
+ package-manager-detector "^1.3.0"
+ tinyexec "^1.0.1"
+
"@aws-crypto/crc32@5.2.0":
version "5.2.0"
resolved "https://registry.npmjs.org/@aws-crypto/crc32/-/crc32-5.2.0.tgz"
@@ -29,6 +42,15 @@
"@smithy/util-utf8" "^2.0.0"
tslib "^2.6.2"
+"@aws-crypto/sha256-js@^5.2.0", "@aws-crypto/sha256-js@5.2.0":
+ version "5.2.0"
+ resolved "https://registry.npmjs.org/@aws-crypto/sha256-js/-/sha256-js-5.2.0.tgz"
+ integrity sha512-FFQQyu7edu4ufvIZ+OadFpHHOt+eSTBaYaki44c+akjg7qZg9oOQeLlk77F6tSYqjDAFClrHJk9tMf0HdVyOvA==
+ dependencies:
+ "@aws-crypto/util" "^5.2.0"
+ "@aws-sdk/types" "^3.222.0"
+ tslib "^2.6.2"
+
"@aws-crypto/sha256-js@1.2.2":
version "1.2.2"
resolved "https://registry.npmjs.org/@aws-crypto/sha256-js/-/sha256-js-1.2.2.tgz"
@@ -38,15 +60,6 @@
"@aws-sdk/types" "^3.1.0"
tslib "^1.11.1"
-"@aws-crypto/sha256-js@5.2.0", "@aws-crypto/sha256-js@^5.2.0":
- version "5.2.0"
- resolved "https://registry.npmjs.org/@aws-crypto/sha256-js/-/sha256-js-5.2.0.tgz"
- integrity sha512-FFQQyu7edu4ufvIZ+OadFpHHOt+eSTBaYaki44c+akjg7qZg9oOQeLlk77F6tSYqjDAFClrHJk9tMf0HdVyOvA==
- dependencies:
- "@aws-crypto/util" "^5.2.0"
- "@aws-sdk/types" "^3.222.0"
- tslib "^2.6.2"
-
"@aws-crypto/supports-web-crypto@^5.2.0":
version "5.2.0"
resolved "https://registry.npmjs.org/@aws-crypto/supports-web-crypto/-/supports-web-crypto-5.2.0.tgz"
@@ -400,7 +413,7 @@
"@smithy/types" "^4.3.2"
tslib "^2.6.2"
-"@aws-sdk/types@3.862.0", "@aws-sdk/types@^3.1.0", "@aws-sdk/types@^3.222.0":
+"@aws-sdk/types@^3.1.0", "@aws-sdk/types@^3.222.0", "@aws-sdk/types@3.862.0":
version "3.862.0"
resolved "https://registry.npmjs.org/@aws-sdk/types/-/types-3.862.0.tgz"
integrity sha512-Bei+RL0cDxxV+lW2UezLbCYYNeJm6Nzee0TpW0FfyTRBhH9C1XQh4+x+IClriXvgBnRquTMMYsmJfvx8iyLKrg==
@@ -481,11 +494,48 @@
resolved "https://registry.npmjs.org/@balena/dockerignore/-/dockerignore-1.0.2.tgz"
integrity sha512-wMue2Sy4GAVTk6Ic4tJVcnfdau+gx2EnG7S+uAEe+TWJFqE4YoWN4/H8MSLj4eYJKxGg26lZwboEniNiNwZQ6Q==
+"@braintree/sanitize-url@^7.1.1":
+ version "7.1.2"
+ resolved "https://registry.npmjs.org/@braintree/sanitize-url/-/sanitize-url-7.1.2.tgz"
+ integrity sha512-jigsZK+sMF/cuiB7sERuo9V7N9jx+dhmHHnQyDSVdpZwVutaBu7WvNYqMDLSgFgfB30n452TP3vjDAvFC973mA==
+
"@bytecodealliance/preview2-shim@0.17.0":
version "0.17.0"
resolved "https://registry.npmjs.org/@bytecodealliance/preview2-shim/-/preview2-shim-0.17.0.tgz"
integrity sha512-JorcEwe4ud0x5BS/Ar2aQWOQoFzjq/7jcnxYXCvSMh0oRm0dQXzOA+hqLDBnOMks1LLBA7dmiLLsEBl09Yd6iQ==
+"@chevrotain/cst-dts-gen@11.1.2":
+ version "11.1.2"
+ resolved "https://registry.npmjs.org/@chevrotain/cst-dts-gen/-/cst-dts-gen-11.1.2.tgz"
+ integrity sha512-XTsjvDVB5nDZBQB8o0o/0ozNelQtn2KrUVteIHSlPd2VAV2utEb6JzyCJaJ8tGxACR4RiBNWy5uYUHX2eji88Q==
+ dependencies:
+ "@chevrotain/gast" "11.1.2"
+ "@chevrotain/types" "11.1.2"
+ lodash-es "4.17.23"
+
+"@chevrotain/gast@11.1.2":
+ version "11.1.2"
+ resolved "https://registry.npmjs.org/@chevrotain/gast/-/gast-11.1.2.tgz"
+ integrity sha512-Z9zfXR5jNZb1Hlsd/p+4XWeUFugrHirq36bKzPWDSIacV+GPSVXdk+ahVWZTwjhNwofAWg/sZg58fyucKSQx5g==
+ dependencies:
+ "@chevrotain/types" "11.1.2"
+ lodash-es "4.17.23"
+
+"@chevrotain/regexp-to-ast@11.1.2":
+ version "11.1.2"
+ resolved "https://registry.npmjs.org/@chevrotain/regexp-to-ast/-/regexp-to-ast-11.1.2.tgz"
+ integrity sha512-nMU3Uj8naWer7xpZTYJdxbAs6RIv/dxYzkYU8GSwgUtcAAlzjcPfX1w+RKRcYG8POlzMeayOQ/znfwxEGo5ulw==
+
+"@chevrotain/types@11.1.2":
+ version "11.1.2"
+ resolved "https://registry.npmjs.org/@chevrotain/types/-/types-11.1.2.tgz"
+ integrity sha512-U+HFai5+zmJCkK86QsaJtoITlboZHBqrVketcO2ROv865xfCMSFpELQoz1GkX5GzME8pTa+3kbKrZHQtI0gdbw==
+
+"@chevrotain/utils@11.1.2":
+ version "11.1.2"
+ resolved "https://registry.npmjs.org/@chevrotain/utils/-/utils-11.1.2.tgz"
+ integrity sha512-4mudFAQ6H+MqBTfqLmU7G1ZwRzCLfJEooL/fsF6rCX5eePMbGhoy5n4g+G4vlh2muDcsCTJtL+uKbOzWxs5LHA==
+
"@cspell/cspell-bundled-dicts@9.2.0":
version "9.2.0"
resolved "https://registry.npmjs.org/@cspell/cspell-bundled-dicts/-/cspell-bundled-dicts-9.2.0.tgz"
@@ -506,9 +556,9 @@
"@cspell/dict-docker" "^1.1.15"
"@cspell/dict-dotnet" "^5.0.10"
"@cspell/dict-elixir" "^4.0.8"
+ "@cspell/dict-en_us" "^4.4.15"
"@cspell/dict-en-common-misspellings" "^2.1.3"
"@cspell/dict-en-gb-mit" "^3.1.5"
- "@cspell/dict-en_us" "^4.4.15"
"@cspell/dict-filetypes" "^3.0.13"
"@cspell/dict-flutter" "^1.1.1"
"@cspell/dict-fonts" "^4.0.5"
@@ -656,6 +706,11 @@
resolved "https://registry.npmjs.org/@cspell/dict-elixir/-/dict-elixir-4.0.8.tgz"
integrity sha512-CyfphrbMyl4Ms55Vzuj+mNmd693HjBFr9hvU+B2YbFEZprE5AG+EXLYTMRWrXbpds4AuZcvN3deM2XVB80BN/Q==
+"@cspell/dict-en_us@^4.4.15":
+ version "4.4.16"
+ resolved "https://registry.npmjs.org/@cspell/dict-en_us/-/dict-en_us-4.4.16.tgz"
+ integrity sha512-/R47sUbUmba2dG/0LZyE6P6gX/DRF1sCcYNQNWyPk/KeidQRNZG+FH9U0KRvX42/2ZzMge6ebXH3WAJ52w0Vqw==
+
"@cspell/dict-en-common-misspellings@^2.1.3":
version "2.1.3"
resolved "https://registry.npmjs.org/@cspell/dict-en-common-misspellings/-/dict-en-common-misspellings-2.1.3.tgz"
@@ -666,11 +721,6 @@
resolved "https://registry.npmjs.org/@cspell/dict-en-gb-mit/-/dict-en-gb-mit-3.1.6.tgz"
integrity sha512-3JJGxuPhDK5rMDYPzJYAdjjsBddEyV54rXfUQpOCl7c7weMhNDWfC2q4h3cKNDj7Isud1q2RM+DlSxQWf40OTw==
-"@cspell/dict-en_us@^4.4.15":
- version "4.4.16"
- resolved "https://registry.npmjs.org/@cspell/dict-en_us/-/dict-en_us-4.4.16.tgz"
- integrity sha512-/R47sUbUmba2dG/0LZyE6P6gX/DRF1sCcYNQNWyPk/KeidQRNZG+FH9U0KRvX42/2ZzMge6ebXH3WAJ52w0Vqw==
-
"@cspell/dict-filetypes@^3.0.13":
version "3.0.13"
resolved "https://registry.npmjs.org/@cspell/dict-filetypes/-/dict-filetypes-3.0.13.tgz"
@@ -833,7 +883,7 @@
resolved "https://registry.npmjs.org/@cspell/dict-scala/-/dict-scala-5.0.8.tgz"
integrity sha512-YdftVmumv8IZq9zu1gn2U7A4bfM2yj9Vaupydotyjuc+EEZZSqAafTpvW/jKLWji2TgybM1L2IhmV0s/Iv9BTw==
-"@cspell/dict-shell@1.1.1", "@cspell/dict-shell@^1.1.1":
+"@cspell/dict-shell@^1.1.1", "@cspell/dict-shell@1.1.1":
version "1.1.1"
resolved "https://registry.npmjs.org/@cspell/dict-shell/-/dict-shell-1.1.1.tgz"
integrity sha512-T37oYxE7OV1x/1D4/13Y8JZGa1QgDCXV7AVt3HLXjn0Fe3TaNDvf5sU0fGnXKmBPqFFrHdpD3uutAQb1dlp15g==
@@ -905,12 +955,12 @@
"@ethereumjs/rlp@^5.0.2":
version "5.0.2"
- resolved "https://registry.yarnpkg.com/@ethereumjs/rlp/-/rlp-5.0.2.tgz#c89bd82f2f3bec248ab2d517ae25f5bbc4aac842"
+ resolved "https://registry.npmjs.org/@ethereumjs/rlp/-/rlp-5.0.2.tgz"
integrity sha512-DziebCdg4JpGlEqEdGgXmjqcFoJi+JGulUXwEjsZGAscAQ7MyD/7LE/GVCP29vEQxKc7AAwjT3A2ywHp2xfoCA==
"@ethereumjs/util@^9.1.0":
version "9.1.0"
- resolved "https://registry.yarnpkg.com/@ethereumjs/util/-/util-9.1.0.tgz#75e3898a3116d21c135fa9e29886565609129bce"
+ resolved "https://registry.npmjs.org/@ethereumjs/util/-/util-9.1.0.tgz"
integrity sha512-XBEKsYqLGXLah9PNJbgdkigthkG7TAGvlD/sH12beMXEyHDyigfcbdvHhmLyDWgDyOJn4QwiQUaF7yeuhnjdog==
dependencies:
"@ethereumjs/rlp" "^5.0.2"
@@ -955,7 +1005,7 @@
"@ethersproject/logger" "^5.7.0"
"@ethersproject/properties" "^5.7.0"
-"@ethersproject/address@5.7.0", "@ethersproject/address@^5.0.2", "@ethersproject/address@^5.7.0":
+"@ethersproject/address@^5.0.2", "@ethersproject/address@^5.7.0", "@ethersproject/address@5.7.0":
version "5.7.0"
resolved "https://registry.npmjs.org/@ethersproject/address/-/address-5.7.0.tgz"
integrity sha512-9wYhYt7aghVGo758POM5nqcOMaE168Q6aRLJZwUmiqSrAungkG74gSSeKEIR7ukixesdRZGPgVqme6vmxs1fkA==
@@ -1098,6 +1148,51 @@
resolved "https://registry.npmjs.org/@fastify/busboy/-/busboy-2.1.1.tgz"
integrity sha512-vBZP4NlzfOlerQTnba4aqZoMhE/a9HY7HRqoOPaETQcSQuWEIyZMHGfVu6w9wGtGK5fED5qRs2DteVCjOH60sA==
+"@floating-ui/core@^1.7.5":
+ version "1.7.5"
+ resolved "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.5.tgz"
+ integrity sha512-1Ih4WTWyw0+lKyFMcBHGbb5U5FtuHJuujoyyr5zTaWS5EYMeT6Jb2AuDeftsCsEuchO+mM2ij5+q9crhydzLhQ==
+ dependencies:
+ "@floating-ui/utils" "^0.2.11"
+
+"@floating-ui/dom@^1.7.6":
+ version "1.7.6"
+ resolved "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.6.tgz"
+ integrity sha512-9gZSAI5XM36880PPMm//9dfiEngYoC6Am2izES1FF406YFsjvyBMmeJ2g4SAju3xWwtuynNRFL2s9hgxpLI5SQ==
+ dependencies:
+ "@floating-ui/core" "^1.7.5"
+ "@floating-ui/utils" "^0.2.11"
+
+"@floating-ui/react-dom@^2.1.2", "@floating-ui/react-dom@^2.1.8":
+ version "2.1.8"
+ resolved "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.8.tgz"
+ integrity sha512-cC52bHwM/n/CxS87FH0yWdngEZrjdtLW/qVruo68qg+prK7ZQ4YGdut2GyDVpoGeAYe/h899rVeOVm6Oi40k2A==
+ dependencies:
+ "@floating-ui/dom" "^1.7.6"
+
+"@floating-ui/react@^0.26.16":
+ version "0.26.28"
+ resolved "https://registry.npmjs.org/@floating-ui/react/-/react-0.26.28.tgz"
+ integrity sha512-yORQuuAtVpiRjpMhdc0wJj06b9JFjrYF4qp96j++v2NBpbi6SEGF7donUJ3TMieerQ6qVkAv1tgr7L4r5roTqw==
+ dependencies:
+ "@floating-ui/react-dom" "^2.1.2"
+ "@floating-ui/utils" "^0.2.8"
+ tabbable "^6.0.0"
+
+"@floating-ui/react@^0.27.16":
+ version "0.27.19"
+ resolved "https://registry.npmjs.org/@floating-ui/react/-/react-0.27.19.tgz"
+ integrity sha512-31B8h5mm8YxotlE7/AU/PhNAl8eWxAmjL/v2QOxroDNkTFLk3Uu82u63N3b6TXa4EGJeeZLVcd/9AlNlVqzeog==
+ dependencies:
+ "@floating-ui/react-dom" "^2.1.8"
+ "@floating-ui/utils" "^0.2.11"
+ tabbable "^6.0.0"
+
+"@floating-ui/utils@^0.2.11", "@floating-ui/utils@^0.2.8":
+ version "0.2.11"
+ resolved "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.11.tgz"
+ integrity sha512-RiB/yIh78pcIxl6lLMG0CgBXAZ2Y0eVHqMPYugu+9U0AeT6YBeiJpf7lbdJNIugFP5SIjwNRgo4DhR1Qxi26Gg==
+
"@grpc/grpc-js@^1.11.1":
version "1.13.4"
resolved "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.13.4.tgz"
@@ -1116,11 +1211,41 @@
protobufjs "^7.2.5"
yargs "^17.7.2"
+"@headlessui/react@^2.2.9":
+ version "2.2.9"
+ resolved "https://registry.npmjs.org/@headlessui/react/-/react-2.2.9.tgz"
+ integrity sha512-Mb+Un58gwBn0/yWZfyrCh0TJyurtT+dETj7YHleylHk5od3dv2XqETPGWMyQ5/7sYN7oWdyM1u9MvC0OC8UmzQ==
+ dependencies:
+ "@floating-ui/react" "^0.26.16"
+ "@react-aria/focus" "^3.20.2"
+ "@react-aria/interactions" "^3.25.0"
+ "@tanstack/react-virtual" "^3.13.9"
+ use-sync-external-store "^1.5.0"
+
+"@headlessui/tailwindcss@^0.2.2":
+ version "0.2.2"
+ resolved "https://registry.npmjs.org/@headlessui/tailwindcss/-/tailwindcss-0.2.2.tgz"
+ integrity sha512-xNe42KjdyA4kfUKLLPGzME9zkH7Q3rOZ5huFihWNWOQFxnItxPB3/67yBI8/qBfY8nwBRx5GHn4VprsoluVMGw==
+
"@humanwhocodes/momoa@^2.0.4":
version "2.0.4"
resolved "https://registry.npmjs.org/@humanwhocodes/momoa/-/momoa-2.0.4.tgz"
integrity sha512-RE815I4arJFtt+FVeU1Tgp9/Xvecacji8w/V6XtXsWWH/wz/eNkNbhb+ny/+PlVZjV0rxQpRSQKNKE3lcktHEA==
+"@iconify/types@^2.0.0":
+ version "2.0.0"
+ resolved "https://registry.npmjs.org/@iconify/types/-/types-2.0.0.tgz"
+ integrity sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg==
+
+"@iconify/utils@^3.0.2":
+ version "3.1.0"
+ resolved "https://registry.npmjs.org/@iconify/utils/-/utils-3.1.0.tgz"
+ integrity sha512-Zlzem1ZXhI1iHeeERabLNzBHdOa4VhQbqAcOQaMKuTuyZCpwKbC2R4Dd0Zo3g9EAc+Y4fiarO8HIHRAth7+skw==
+ dependencies:
+ "@antfu/install-pkg" "^1.1.0"
+ "@iconify/types" "^2.0.0"
+ mlly "^1.8.0"
+
"@isaacs/cliui@^8.0.2":
version "8.0.2"
resolved "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz"
@@ -1133,16 +1258,32 @@
wrap-ansi "^8.1.0"
wrap-ansi-cjs "npm:wrap-ansi@^7.0.0"
-"@jridgewell/resolve-uri@^3.0.3":
+"@jridgewell/gen-mapping@^0.3.2":
+ version "0.3.13"
+ resolved "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz"
+ integrity sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==
+ dependencies:
+ "@jridgewell/sourcemap-codec" "^1.5.0"
+ "@jridgewell/trace-mapping" "^0.3.24"
+
+"@jridgewell/resolve-uri@^3.0.3", "@jridgewell/resolve-uri@^3.1.0":
version "3.1.2"
resolved "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz"
integrity sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==
-"@jridgewell/sourcemap-codec@^1.4.10":
+"@jridgewell/sourcemap-codec@^1.4.10", "@jridgewell/sourcemap-codec@^1.4.14", "@jridgewell/sourcemap-codec@^1.5.0":
version "1.5.0"
resolved "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz"
integrity sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==
+"@jridgewell/trace-mapping@^0.3.24":
+ version "0.3.31"
+ resolved "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz"
+ integrity sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==
+ dependencies:
+ "@jridgewell/resolve-uri" "^3.1.0"
+ "@jridgewell/sourcemap-codec" "^1.4.14"
+
"@jridgewell/trace-mapping@0.3.9":
version "0.3.9"
resolved "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz"
@@ -1307,14 +1448,32 @@
readline-sync "^1.4.10"
uuid "^11.0.3"
-"@noble/curves@1.2.0":
- version "1.2.0"
- resolved "https://registry.npmjs.org/@noble/curves/-/curves-1.2.0.tgz"
- integrity sha512-oYclrNgRaM9SsBUBVbb8M6DTV7ZHRTKugureoYEncY5c65HOmRzvSiTE3y5CYaPYJA/GVkrhXEoF0M3Ya9PMnw==
+"@mermaid-js/mermaid-cli@*":
+ version "11.12.0"
+ resolved "https://registry.npmjs.org/@mermaid-js/mermaid-cli/-/mermaid-cli-11.12.0.tgz"
+ integrity sha512-a0swOS6PByXKi0dZnLQQIhbtUEu7ubc6bojmIqXqvUPq7mIJukCNEvVBTv6IAbuEWqB3Ti8QntupoGdz3ej+kg==
dependencies:
- "@noble/hashes" "1.3.2"
+ "@mermaid-js/mermaid-zenuml" "^0.2.0"
+ chalk "^5.0.1"
+ commander "^14.0.0"
+ import-meta-resolve "^4.1.0"
+ mermaid "^11.0.2"
-"@noble/curves@1.4.2", "@noble/curves@~1.4.0":
+"@mermaid-js/mermaid-zenuml@^0.2.0":
+ version "0.2.2"
+ resolved "https://registry.npmjs.org/@mermaid-js/mermaid-zenuml/-/mermaid-zenuml-0.2.2.tgz"
+ integrity sha512-sUjwk4NWUpy9uaHypYSIGJDks10ZaZo5CHH9lx9xcmyqv9w7yvd4vecUmlUQxmlHStYO+aqSkYKX5/gFjDfypw==
+ dependencies:
+ "@zenuml/core" "^3.35.2"
+
+"@mermaid-js/parser@^1.0.1":
+ version "1.0.1"
+ resolved "https://registry.npmjs.org/@mermaid-js/parser/-/parser-1.0.1.tgz"
+ integrity sha512-opmV19kN1JsK0T6HhhokHpcVkqKpF+x2pPDKKM2ThHtZAB5F4PROopk0amuVYK5qMrIA4erzpNm8gmPNJgMDxQ==
+ dependencies:
+ langium "^4.0.0"
+
+"@noble/curves@~1.4.0", "@noble/curves@1.4.2":
version "1.4.2"
resolved "https://registry.npmjs.org/@noble/curves/-/curves-1.4.2.tgz"
integrity sha512-TavHr8qycMChk8UwMld0ZDRvatedkzWfH8IiaeGCfymOP5i0hSCozz9vHOL0nkwk7HRMlFnAiKpS2jrUmSybcw==
@@ -1328,27 +1487,39 @@
dependencies:
"@noble/hashes" "1.7.2"
-"@noble/hashes@1.2.0", "@noble/hashes@~1.2.0":
+"@noble/curves@1.2.0":
+ version "1.2.0"
+ resolved "https://registry.npmjs.org/@noble/curves/-/curves-1.2.0.tgz"
+ integrity sha512-oYclrNgRaM9SsBUBVbb8M6DTV7ZHRTKugureoYEncY5c65HOmRzvSiTE3y5CYaPYJA/GVkrhXEoF0M3Ya9PMnw==
+ dependencies:
+ "@noble/hashes" "1.3.2"
+
+"@noble/hashes@~1.2.0":
version "1.2.0"
resolved "https://registry.npmjs.org/@noble/hashes/-/hashes-1.2.0.tgz"
integrity sha512-FZfhjEDbT5GRswV3C6uvLPHMiVD6lQBmpoX5+eSiPaMTXte/IKqI5dykDxzZB/WBeK/CDuQRBWarPdi3FNY2zQ==
-"@noble/hashes@1.3.2":
- version "1.3.2"
- resolved "https://registry.npmjs.org/@noble/hashes/-/hashes-1.3.2.tgz"
- integrity sha512-MVC8EAQp7MvEcm30KWENFjgR+Mkmf+D189XJTkFIlwohU5hcBbn1ZkKq7KVTi2Hme3PMGF390DaL52beVrIihQ==
-
-"@noble/hashes@1.4.0", "@noble/hashes@~1.4.0":
+"@noble/hashes@~1.4.0", "@noble/hashes@1.4.0":
version "1.4.0"
resolved "https://registry.npmjs.org/@noble/hashes/-/hashes-1.4.0.tgz"
integrity sha512-V1JJ1WTRUqHHrOSh597hURcMqVKVGL/ea3kv0gSnEdsEZ0/+VyPghM1lMNGc00z7CIQorSvbKpuJkxvuHbvdbg==
-"@noble/hashes@1.7.2", "@noble/hashes@~1.7.1":
+"@noble/hashes@~1.7.1", "@noble/hashes@1.7.2":
version "1.7.2"
resolved "https://registry.npmjs.org/@noble/hashes/-/hashes-1.7.2.tgz"
integrity sha512-biZ0NUSxyjLLqo6KxEJ1b+C2NAx0wtDoFvCaXHGgUkeHzf3Xc1xKumFKREuT7f7DARNZ/slvYUwFG6B0f2b6hQ==
-"@noble/secp256k1@1.7.1", "@noble/secp256k1@~1.7.0":
+"@noble/hashes@1.2.0":
+ version "1.2.0"
+ resolved "https://registry.npmjs.org/@noble/hashes/-/hashes-1.2.0.tgz"
+ integrity sha512-FZfhjEDbT5GRswV3C6uvLPHMiVD6lQBmpoX5+eSiPaMTXte/IKqI5dykDxzZB/WBeK/CDuQRBWarPdi3FNY2zQ==
+
+"@noble/hashes@1.3.2":
+ version "1.3.2"
+ resolved "https://registry.npmjs.org/@noble/hashes/-/hashes-1.3.2.tgz"
+ integrity sha512-MVC8EAQp7MvEcm30KWENFjgR+Mkmf+D189XJTkFIlwohU5hcBbn1ZkKq7KVTi2Hme3PMGF390DaL52beVrIihQ==
+
+"@noble/secp256k1@~1.7.0", "@noble/secp256k1@1.7.1":
version "1.7.1"
resolved "https://registry.npmjs.org/@noble/secp256k1/-/secp256k1-1.7.1.tgz"
integrity sha512-hOUk6AyBFmqVrv7k5WAw/LpszxVbj9gGN4JRkIX52fdFAj1UA61KXmZDvqVEm+pOyec3+fIeZB02LYa/pWOArw==
@@ -1361,7 +1532,7 @@
"@nodelib/fs.stat" "2.0.5"
run-parallel "^1.1.9"
-"@nodelib/fs.stat@2.0.5", "@nodelib/fs.stat@^2.0.2":
+"@nodelib/fs.stat@^2.0.2", "@nodelib/fs.stat@2.0.5":
version "2.0.5"
resolved "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz"
integrity sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==
@@ -1376,42 +1547,42 @@
"@nomicfoundation/edr-darwin-arm64@0.11.3":
version "0.11.3"
- resolved "https://registry.yarnpkg.com/@nomicfoundation/edr-darwin-arm64/-/edr-darwin-arm64-0.11.3.tgz#d8e2609fc24cf20e75c3782e39cd5a95f7488075"
+ resolved "https://registry.npmjs.org/@nomicfoundation/edr-darwin-arm64/-/edr-darwin-arm64-0.11.3.tgz"
integrity sha512-w0tksbdtSxz9nuzHKsfx4c2mwaD0+l5qKL2R290QdnN9gi9AV62p9DHkOgfBdyg6/a6ZlnQqnISi7C9avk/6VA==
"@nomicfoundation/edr-darwin-x64@0.11.3":
version "0.11.3"
- resolved "https://registry.yarnpkg.com/@nomicfoundation/edr-darwin-x64/-/edr-darwin-x64-0.11.3.tgz#7a9e94cee330269a33c7f1dce267560c7e12dbd3"
+ resolved "https://registry.npmjs.org/@nomicfoundation/edr-darwin-x64/-/edr-darwin-x64-0.11.3.tgz"
integrity sha512-QR4jAFrPbOcrO7O2z2ESg+eUeIZPe2bPIlQYgiJ04ltbSGW27FblOzdd5+S3RoOD/dsZGKAvvy6dadBEl0NgoA==
"@nomicfoundation/edr-linux-arm64-gnu@0.11.3":
version "0.11.3"
- resolved "https://registry.yarnpkg.com/@nomicfoundation/edr-linux-arm64-gnu/-/edr-linux-arm64-gnu-0.11.3.tgz#cd5ec90c7263045c3dfd0b109c73206e488edc27"
+ resolved "https://registry.npmjs.org/@nomicfoundation/edr-linux-arm64-gnu/-/edr-linux-arm64-gnu-0.11.3.tgz"
integrity sha512-Ktjv89RZZiUmOFPspuSBVJ61mBZQ2+HuLmV67InNlh9TSUec/iDjGIwAn59dx0bF/LOSrM7qg5od3KKac4LJDQ==
"@nomicfoundation/edr-linux-arm64-musl@0.11.3":
version "0.11.3"
- resolved "https://registry.yarnpkg.com/@nomicfoundation/edr-linux-arm64-musl/-/edr-linux-arm64-musl-0.11.3.tgz#ed23df2d9844470f5661716da27d99a72a69e99e"
+ resolved "https://registry.npmjs.org/@nomicfoundation/edr-linux-arm64-musl/-/edr-linux-arm64-musl-0.11.3.tgz"
integrity sha512-B3sLJx1rL2E9pfdD4mApiwOZSrX0a/KQSBWdlq1uAhFKqkl00yZaY4LejgZndsJAa4iKGQJlGnw4HCGeVt0+jA==
"@nomicfoundation/edr-linux-x64-gnu@0.11.3":
version "0.11.3"
- resolved "https://registry.yarnpkg.com/@nomicfoundation/edr-linux-x64-gnu/-/edr-linux-x64-gnu-0.11.3.tgz#87a62496c2c4b808bc4a9ae96cca1642a21c2b51"
+ resolved "https://registry.npmjs.org/@nomicfoundation/edr-linux-x64-gnu/-/edr-linux-x64-gnu-0.11.3.tgz"
integrity sha512-D/4cFKDXH6UYyKPu6J3Y8TzW11UzeQI0+wS9QcJzjlrrfKj0ENW7g9VihD1O2FvXkdkTjcCZYb6ai8MMTCsaVw==
"@nomicfoundation/edr-linux-x64-musl@0.11.3":
version "0.11.3"
- resolved "https://registry.yarnpkg.com/@nomicfoundation/edr-linux-x64-musl/-/edr-linux-x64-musl-0.11.3.tgz#8cfe408c73bcb9ed5e263910c313866d442f4b48"
+ resolved "https://registry.npmjs.org/@nomicfoundation/edr-linux-x64-musl/-/edr-linux-x64-musl-0.11.3.tgz"
integrity sha512-ergXuIb4nIvmf+TqyiDX5tsE49311DrBky6+jNLgsGDTBaN1GS3OFwFS8I6Ri/GGn6xOaT8sKu3q7/m+WdlFzg==
"@nomicfoundation/edr-win32-x64-msvc@0.11.3":
version "0.11.3"
- resolved "https://registry.yarnpkg.com/@nomicfoundation/edr-win32-x64-msvc/-/edr-win32-x64-msvc-0.11.3.tgz#fb208b94553c7eb22246d73a1ac4de5bfdb97d01"
+ resolved "https://registry.npmjs.org/@nomicfoundation/edr-win32-x64-msvc/-/edr-win32-x64-msvc-0.11.3.tgz"
integrity sha512-snvEf+WB3OV0wj2A7kQ+ZQqBquMcrozSLXcdnMdEl7Tmn+KDCbmFKBt3Tk0X3qOU4RKQpLPnTxdM07TJNVtung==
"@nomicfoundation/edr@^0.11.3":
version "0.11.3"
- resolved "https://registry.yarnpkg.com/@nomicfoundation/edr/-/edr-0.11.3.tgz#e8b30b868788e45d7a2ee2359a021ef7dcb96952"
+ resolved "https://registry.npmjs.org/@nomicfoundation/edr/-/edr-0.11.3.tgz"
integrity sha512-kqILRkAd455Sd6v8mfP3C1/0tCOynJWY+Ir+k/9Boocu2kObCrsFgG+ZWB7fSBVdd9cPVSNrnhWS+V+PEo637g==
dependencies:
"@nomicfoundation/edr-darwin-arm64" "0.11.3"
@@ -1424,7 +1595,7 @@
"@nomicfoundation/hardhat-ethers@^3.0.6", "@nomicfoundation/hardhat-ethers@^3.0.8":
version "3.1.0"
- resolved "https://registry.yarnpkg.com/@nomicfoundation/hardhat-ethers/-/hardhat-ethers-3.1.0.tgz#d33595e85cdb53e802391de64ee35910c078dbb0"
+ resolved "https://registry.npmjs.org/@nomicfoundation/hardhat-ethers/-/hardhat-ethers-3.1.0.tgz"
integrity sha512-jx6fw3Ms7QBwFGT2MU6ICG292z0P81u6g54JjSV105+FbTZOF4FJqPksLfDybxkkOeq28eDxbqq7vpxRYyIlxA==
dependencies:
debug "^4.1.1"
@@ -1437,9 +1608,9 @@
dependencies:
picocolors "^1.1.0"
-"@nomicfoundation/hardhat-verify@^2.0.0", "@nomicfoundation/hardhat-verify@^2.0.12", "@nomicfoundation/hardhat-verify@^2.0.8":
+"@nomicfoundation/hardhat-verify@^2.0.0", "@nomicfoundation/hardhat-verify@^2.0.12", "@nomicfoundation/hardhat-verify@^2.0.14", "@nomicfoundation/hardhat-verify@^2.0.8":
version "2.1.1"
- resolved "https://registry.yarnpkg.com/@nomicfoundation/hardhat-verify/-/hardhat-verify-2.1.1.tgz#0af5fc4228df860062865fcafb4a01bc0b89f8a3"
+ resolved "https://registry.npmjs.org/@nomicfoundation/hardhat-verify/-/hardhat-verify-2.1.1.tgz"
integrity sha512-K1plXIS42xSHDJZRkrE2TZikqxp9T4y6jUMUNI/imLgN5uCcEQokmfU0DlyP9zzHncYK92HlT5IWP35UVCLrPw==
dependencies:
"@ethersproject/abi" "^5.1.2"
@@ -1537,7 +1708,7 @@
dependencies:
"@opentelemetry/api" "^1.3.0"
-"@opentelemetry/api@^1.0.0", "@opentelemetry/api@^1.3.0", "@opentelemetry/api@^1.8", "@opentelemetry/api@^1.9.0":
+"@opentelemetry/api@^1.0.0", "@opentelemetry/api@^1.1.0", "@opentelemetry/api@^1.3.0", "@opentelemetry/api@^1.7.0", "@opentelemetry/api@^1.8", "@opentelemetry/api@^1.9.0", "@opentelemetry/api@>=1.0.0 <1.10.0":
version "1.9.0"
resolved "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.0.tgz"
integrity sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==
@@ -1547,7 +1718,7 @@
resolved "https://registry.npmjs.org/@opentelemetry/context-async-hooks/-/context-async-hooks-1.30.1.tgz"
integrity sha512-s5vvxXPVdjqS3kTLKMeBMvop9hbWkwzBpu+mUO2M7sZtlkyDJGwFe33wRKnbaYDo8ExRVBIIdwIGrqpxHuKttA==
-"@opentelemetry/core@1.30.1", "@opentelemetry/core@^1.1.0", "@opentelemetry/core@^1.26.0", "@opentelemetry/core@^1.30.1", "@opentelemetry/core@^1.8.0":
+"@opentelemetry/core@^1.1.0", "@opentelemetry/core@^1.26.0", "@opentelemetry/core@^1.30.1", "@opentelemetry/core@^1.8.0", "@opentelemetry/core@1.30.1":
version "1.30.1"
resolved "https://registry.npmjs.org/@opentelemetry/core/-/core-1.30.1.tgz"
integrity sha512-OOCM2C/QIURhJMuKaekP3TRBxBKxG/TWWA0TL2J6nXUtDnuCtccy49LUJF8xPFXMX+0LMcxFpCo8M9cGY1W6rQ==
@@ -1698,23 +1869,23 @@
"@opentelemetry/instrumentation" "^0.57.0"
"@opentelemetry/semantic-conventions" "^1.27.0"
-"@opentelemetry/instrumentation-mysql2@0.45.0":
+"@opentelemetry/instrumentation-mysql@0.45.0":
version "0.45.0"
- resolved "https://registry.npmjs.org/@opentelemetry/instrumentation-mysql2/-/instrumentation-mysql2-0.45.0.tgz"
- integrity sha512-qLslv/EPuLj0IXFvcE3b0EqhWI8LKmrgRPIa4gUd8DllbBpqJAvLNJSv3cC6vWwovpbSI3bagNO/3Q2SuXv2xA==
+ resolved "https://registry.npmjs.org/@opentelemetry/instrumentation-mysql/-/instrumentation-mysql-0.45.0.tgz"
+ integrity sha512-tWWyymgwYcTwZ4t8/rLDfPYbOTF3oYB8SxnYMtIQ1zEf5uDm90Ku3i6U/vhaMyfHNlIHvDhvJh+qx5Nc4Z3Acg==
dependencies:
"@opentelemetry/instrumentation" "^0.57.0"
"@opentelemetry/semantic-conventions" "^1.27.0"
- "@opentelemetry/sql-common" "^0.40.1"
+ "@types/mysql" "2.15.26"
-"@opentelemetry/instrumentation-mysql@0.45.0":
+"@opentelemetry/instrumentation-mysql2@0.45.0":
version "0.45.0"
- resolved "https://registry.npmjs.org/@opentelemetry/instrumentation-mysql/-/instrumentation-mysql-0.45.0.tgz"
- integrity sha512-tWWyymgwYcTwZ4t8/rLDfPYbOTF3oYB8SxnYMtIQ1zEf5uDm90Ku3i6U/vhaMyfHNlIHvDhvJh+qx5Nc4Z3Acg==
+ resolved "https://registry.npmjs.org/@opentelemetry/instrumentation-mysql2/-/instrumentation-mysql2-0.45.0.tgz"
+ integrity sha512-qLslv/EPuLj0IXFvcE3b0EqhWI8LKmrgRPIa4gUd8DllbBpqJAvLNJSv3cC6vWwovpbSI3bagNO/3Q2SuXv2xA==
dependencies:
"@opentelemetry/instrumentation" "^0.57.0"
"@opentelemetry/semantic-conventions" "^1.27.0"
- "@types/mysql" "2.15.26"
+ "@opentelemetry/sql-common" "^0.40.1"
"@opentelemetry/instrumentation-nestjs-core@0.44.0":
version "0.44.0"
@@ -1762,18 +1933,6 @@
"@opentelemetry/core" "^1.8.0"
"@opentelemetry/instrumentation" "^0.57.0"
-"@opentelemetry/instrumentation@0.57.1":
- version "0.57.1"
- resolved "https://registry.npmjs.org/@opentelemetry/instrumentation/-/instrumentation-0.57.1.tgz"
- integrity sha512-SgHEKXoVxOjc20ZYusPG3Fh+RLIZTSa4x8QtD3NfgAUDyqdFFS9W1F2ZVbZkqDCdyMcQG02Ok4duUGLHJXHgbA==
- dependencies:
- "@opentelemetry/api-logs" "0.57.1"
- "@types/shimmer" "^1.2.0"
- import-in-the-middle "^1.8.1"
- require-in-the-middle "^7.1.1"
- semver "^7.5.2"
- shimmer "^1.2.1"
-
"@opentelemetry/instrumentation@^0.49 || ^0.50 || ^0.51 || ^0.52.0 || ^0.53.0":
version "0.53.0"
resolved "https://registry.npmjs.org/@opentelemetry/instrumentation/-/instrumentation-0.53.0.tgz"
@@ -1798,12 +1957,24 @@
semver "^7.5.2"
shimmer "^1.2.1"
+"@opentelemetry/instrumentation@0.57.1":
+ version "0.57.1"
+ resolved "https://registry.npmjs.org/@opentelemetry/instrumentation/-/instrumentation-0.57.1.tgz"
+ integrity sha512-SgHEKXoVxOjc20ZYusPG3Fh+RLIZTSa4x8QtD3NfgAUDyqdFFS9W1F2ZVbZkqDCdyMcQG02Ok4duUGLHJXHgbA==
+ dependencies:
+ "@opentelemetry/api-logs" "0.57.1"
+ "@types/shimmer" "^1.2.0"
+ import-in-the-middle "^1.8.1"
+ require-in-the-middle "^7.1.1"
+ semver "^7.5.2"
+ shimmer "^1.2.1"
+
"@opentelemetry/redis-common@^0.36.2":
version "0.36.2"
resolved "https://registry.npmjs.org/@opentelemetry/redis-common/-/redis-common-0.36.2.tgz"
integrity sha512-faYX1N0gpLhej/6nyp6bgRjzAKXn5GOEMYY7YhciSfCoITAktLUtQ36d24QEWNA1/WA1y6qQunCe0OhHRkVl9g==
-"@opentelemetry/resources@1.30.1", "@opentelemetry/resources@^1.30.1":
+"@opentelemetry/resources@^1.30.1", "@opentelemetry/resources@1.30.1":
version "1.30.1"
resolved "https://registry.npmjs.org/@opentelemetry/resources/-/resources-1.30.1.tgz"
integrity sha512-5UxZqiAgLYGFjS4s9qm5mBVo433u+dSPUFWVWXmLAD4wB65oMCoXaJP1KJa9DIYYMeHu3z4BZcStG3LC593cWA==
@@ -1820,6 +1991,11 @@
"@opentelemetry/resources" "1.30.1"
"@opentelemetry/semantic-conventions" "1.28.0"
+"@opentelemetry/semantic-conventions@^1.27.0", "@opentelemetry/semantic-conventions@^1.28.0":
+ version "1.36.0"
+ resolved "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.36.0.tgz"
+ integrity sha512-TtxJSRD8Ohxp6bKkhrm27JRHAxPczQA7idtcTOMYI+wQRRrfgqxHv1cFbCApcSnNjtXkmzFozn6jQtFrOmbjPQ==
+
"@opentelemetry/semantic-conventions@1.27.0":
version "1.27.0"
resolved "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.27.0.tgz"
@@ -1830,11 +2006,6 @@
resolved "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.28.0.tgz"
integrity sha512-lp4qAiMTD4sNWW4DbKLBkfiMZ4jbAboJIGOQr5DvciMRI494OapieI9qiODpOt0XBr1LjIDy1xAGAnVs5supTA==
-"@opentelemetry/semantic-conventions@^1.27.0", "@opentelemetry/semantic-conventions@^1.28.0":
- version "1.36.0"
- resolved "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.36.0.tgz"
- integrity sha512-TtxJSRD8Ohxp6bKkhrm27JRHAxPczQA7idtcTOMYI+wQRRrfgqxHv1cFbCApcSnNjtXkmzFozn6jQtFrOmbjPQ==
-
"@opentelemetry/sql-common@^0.40.1":
version "0.40.1"
resolved "https://registry.npmjs.org/@opentelemetry/sql-common/-/sql-common-0.40.1.tgz"
@@ -2020,6 +2191,87 @@
resolved "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz"
integrity sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==
+"@puppeteer/browsers@2.6.1":
+ version "2.6.1"
+ resolved "https://registry.npmjs.org/@puppeteer/browsers/-/browsers-2.6.1.tgz"
+ integrity sha512-aBSREisdsGH890S2rQqK82qmQYU3uFpSH8wcZWHgHzl3LfzsxAKbLNiAG9mO8v1Y0UICBeClICxPJvyr0rcuxg==
+ dependencies:
+ debug "^4.4.0"
+ extract-zip "^2.0.1"
+ progress "^2.0.3"
+ proxy-agent "^6.5.0"
+ semver "^7.6.3"
+ tar-fs "^3.0.6"
+ unbzip2-stream "^1.4.3"
+ yargs "^17.7.2"
+
+"@react-aria/focus@^3.20.2":
+ version "3.21.5"
+ resolved "https://registry.npmjs.org/@react-aria/focus/-/focus-3.21.5.tgz"
+ integrity sha512-V18fwCyf8zqgJdpLQeDU5ZRNd9TeOfBbhLgmX77Zr5ae9XwaoJ1R3SFJG1wCJX60t34AW+aLZSEEK+saQElf3Q==
+ dependencies:
+ "@react-aria/interactions" "^3.27.1"
+ "@react-aria/utils" "^3.33.1"
+ "@react-types/shared" "^3.33.1"
+ "@swc/helpers" "^0.5.0"
+ clsx "^2.0.0"
+
+"@react-aria/interactions@^3.25.0", "@react-aria/interactions@^3.27.1":
+ version "3.27.1"
+ resolved "https://registry.npmjs.org/@react-aria/interactions/-/interactions-3.27.1.tgz"
+ integrity sha512-M3wLpTTmDflI0QGNK0PJNUaBXXfeBXue8ZxLMngfc1piHNiH4G5lUvWd9W14XVbqrSCVY8i8DfGrNYpyyZu0tw==
+ dependencies:
+ "@react-aria/ssr" "^3.9.10"
+ "@react-aria/utils" "^3.33.1"
+ "@react-stately/flags" "^3.1.2"
+ "@react-types/shared" "^3.33.1"
+ "@swc/helpers" "^0.5.0"
+
+"@react-aria/ssr@^3.9.10":
+ version "3.9.10"
+ resolved "https://registry.npmjs.org/@react-aria/ssr/-/ssr-3.9.10.tgz"
+ integrity sha512-hvTm77Pf+pMBhuBm760Li0BVIO38jv1IBws1xFm1NoL26PU+fe+FMW5+VZWyANR6nYL65joaJKZqOdTQMkO9IQ==
+ dependencies:
+ "@swc/helpers" "^0.5.0"
+
+"@react-aria/utils@^3.33.1":
+ version "3.33.1"
+ resolved "https://registry.npmjs.org/@react-aria/utils/-/utils-3.33.1.tgz"
+ integrity sha512-kIx1Sj6bbAT0pdqCegHuPanR9zrLn5zMRiM7LN12rgRf55S19ptd9g3ncahArifYTRkfEU9VIn+q0HjfMqS9/w==
+ dependencies:
+ "@react-aria/ssr" "^3.9.10"
+ "@react-stately/flags" "^3.1.2"
+ "@react-stately/utils" "^3.11.0"
+ "@react-types/shared" "^3.33.1"
+ "@swc/helpers" "^0.5.0"
+ clsx "^2.0.0"
+
+"@react-stately/flags@^3.1.2":
+ version "3.1.2"
+ resolved "https://registry.npmjs.org/@react-stately/flags/-/flags-3.1.2.tgz"
+ integrity sha512-2HjFcZx1MyQXoPqcBGALwWWmgFVUk2TuKVIQxCbRq7fPyWXIl6VHcakCLurdtYC2Iks7zizvz0Idv48MQ38DWg==
+ dependencies:
+ "@swc/helpers" "^0.5.0"
+
+"@react-stately/utils@^3.11.0":
+ version "3.11.0"
+ resolved "https://registry.npmjs.org/@react-stately/utils/-/utils-3.11.0.tgz"
+ integrity sha512-8LZpYowJ9eZmmYLpudbo/eclIRnbhWIJZ994ncmlKlouNzKohtM8qTC6B1w1pwUbiwGdUoyzLuQbeaIor5Dvcw==
+ dependencies:
+ "@swc/helpers" "^0.5.0"
+
+"@react-types/shared@^3.33.1":
+ version "3.33.1"
+ resolved "https://registry.npmjs.org/@react-types/shared/-/shared-3.33.1.tgz"
+ integrity sha512-oJHtjvLG43VjwemQDadlR5g/8VepK56B/xKO2XORPHt9zlW6IZs3tZrYlvH29BMvoqC7RtE7E5UjgbnbFtDGag==
+
+"@samverschueren/stream-to-observable@^0.3.0":
+ version "0.3.1"
+ resolved "https://registry.npmjs.org/@samverschueren/stream-to-observable/-/stream-to-observable-0.3.1.tgz"
+ integrity sha512-c/qwwcHyafOQuVQJj0IlBjf5yYgBI7YPJ77k4fOJYesb41jio65eaJODRUmfYKhTOFBrIZ66kgvGPlNbjuoRdQ==
+ dependencies:
+ any-observable "^0.3.0"
+
"@scure/base@~1.1.0", "@scure/base@~1.1.6":
version "1.1.9"
resolved "https://registry.npmjs.org/@scure/base/-/base-1.1.9.tgz"
@@ -2197,13 +2449,6 @@
dependencies:
type-detect "4.0.8"
-"@sinonjs/fake-timers@11.2.2":
- version "11.2.2"
- resolved "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-11.2.2.tgz"
- integrity sha512-G2piCSxQ7oWOxwGSAyFHfPIsyeJGXYtc6mFbnFA+kRXkiEnTl8c/8jul2S329iFBnDI9HGoeWWAZvuvOkZccgw==
- dependencies:
- "@sinonjs/commons" "^3.0.0"
-
"@sinonjs/fake-timers@^13.0.1":
version "13.0.5"
resolved "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-13.0.5.tgz"
@@ -2211,6 +2456,13 @@
dependencies:
"@sinonjs/commons" "^3.0.1"
+"@sinonjs/fake-timers@11.2.2":
+ version "11.2.2"
+ resolved "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-11.2.2.tgz"
+ integrity sha512-G2piCSxQ7oWOxwGSAyFHfPIsyeJGXYtc6mFbnFA+kRXkiEnTl8c/8jul2S329iFBnDI9HGoeWWAZvuvOkZccgw==
+ dependencies:
+ "@sinonjs/commons" "^3.0.0"
+
"@sinonjs/samsam@^8.0.0":
version "8.0.2"
resolved "https://registry.npmjs.org/@sinonjs/samsam/-/samsam-8.0.2.tgz"
@@ -2682,6 +2934,13 @@
resolved "https://registry.npmjs.org/@solidity-parser/parser/-/parser-0.20.2.tgz"
integrity sha512-rbu0bzwNvMcwAjH86hiEAcOeRI2EeK8zCkHDrFykh/Al8mvJeFmjy3UrE7GYQjNwOgbGUUtCn5/k8CB8zIu7QA==
+"@swc/helpers@^0.5.0":
+ version "0.5.19"
+ resolved "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.19.tgz"
+ integrity sha512-QamiFeIK3txNjgUTNppE6MiG3p7TdninpZu0E0PbqVh1a9FNLT2FRhisaa4NcaX52XVhA5l7Pk58Ft7Sqi/2sA==
+ dependencies:
+ tslib "^2.8.0"
+
"@szmarczak/http-timer@^5.0.1":
version "5.0.1"
resolved "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-5.0.1.tgz"
@@ -2689,6 +2948,23 @@
dependencies:
defer-to-connect "^2.0.1"
+"@tanstack/react-virtual@^3.13.9":
+ version "3.13.21"
+ resolved "https://registry.npmjs.org/@tanstack/react-virtual/-/react-virtual-3.13.21.tgz"
+ integrity sha512-SYXFrmrbPgXBvf+HsOsKhFgqSe4M6B29VHOsX9Jih9TlNkNkDWx0hWMiMLUghMEzyUz772ndzdEeCEBx+3GIZw==
+ dependencies:
+ "@tanstack/virtual-core" "3.13.21"
+
+"@tanstack/virtual-core@3.13.21":
+ version "3.13.21"
+ resolved "https://registry.npmjs.org/@tanstack/virtual-core/-/virtual-core-3.13.21.tgz"
+ integrity sha512-ww+fmLHyCbPSf7JNbWZP3g7wl6SdNo3ah5Aiw+0e9FDErkVHLKprYUrwTm7dF646FtEkN/KkAKPYezxpmvOjxw==
+
+"@tootallnate/quickjs-emscripten@^0.23.0":
+ version "0.23.0"
+ resolved "https://registry.npmjs.org/@tootallnate/quickjs-emscripten/-/quickjs-emscripten-0.23.0.tgz"
+ integrity sha512-C5Mc6rdnsaJDjO3UpGW/CQTHtCKaYlScZTly4JIu97Jxo/odCiH0ITnDXSJPTOrEKk/ycSZ0AOgTmkDtkOsvIA==
+
"@ts-morph/common@~0.23.0":
version "0.23.0"
resolved "https://registry.npmjs.org/@ts-morph/common/-/common-0.23.0.tgz"
@@ -2728,7 +3004,7 @@
"@types/chai@^4.3.4":
version "4.3.20"
- resolved "https://registry.yarnpkg.com/@types/chai/-/chai-4.3.20.tgz#cb291577ed342ca92600430841a00329ba05cecc"
+ resolved "https://registry.npmjs.org/@types/chai/-/chai-4.3.20.tgz"
integrity sha512-/pC9HAB5I/xMlc5FP77qjCnI16ChlJfW0tGa0IUcFn38VJrTV6DeZ60NU5KZBtaOZqjdpwTWohz5HU1RrhiYxQ==
"@types/connect@3.4.36":
@@ -2738,6 +3014,221 @@
dependencies:
"@types/node" "*"
+"@types/d3-array@*":
+ version "3.2.2"
+ resolved "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.2.tgz"
+ integrity sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw==
+
+"@types/d3-axis@*":
+ version "3.0.6"
+ resolved "https://registry.npmjs.org/@types/d3-axis/-/d3-axis-3.0.6.tgz"
+ integrity sha512-pYeijfZuBd87T0hGn0FO1vQ/cgLk6E1ALJjfkC0oJ8cbwkZl3TpgS8bVBLZN+2jjGgg38epgxb2zmoGtSfvgMw==
+ dependencies:
+ "@types/d3-selection" "*"
+
+"@types/d3-brush@*":
+ version "3.0.6"
+ resolved "https://registry.npmjs.org/@types/d3-brush/-/d3-brush-3.0.6.tgz"
+ integrity sha512-nH60IZNNxEcrh6L1ZSMNA28rj27ut/2ZmI3r96Zd+1jrZD++zD3LsMIjWlvg4AYrHn/Pqz4CF3veCxGjtbqt7A==
+ dependencies:
+ "@types/d3-selection" "*"
+
+"@types/d3-chord@*":
+ version "3.0.6"
+ resolved "https://registry.npmjs.org/@types/d3-chord/-/d3-chord-3.0.6.tgz"
+ integrity sha512-LFYWWd8nwfwEmTZG9PfQxd17HbNPksHBiJHaKuY1XeqscXacsS2tyoo6OdRsjf+NQYeB6XrNL3a25E3gH69lcg==
+
+"@types/d3-color@*":
+ version "3.1.3"
+ resolved "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.3.tgz"
+ integrity sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==
+
+"@types/d3-contour@*":
+ version "3.0.6"
+ resolved "https://registry.npmjs.org/@types/d3-contour/-/d3-contour-3.0.6.tgz"
+ integrity sha512-BjzLgXGnCWjUSYGfH1cpdo41/hgdWETu4YxpezoztawmqsvCeep+8QGfiY6YbDvfgHz/DkjeIkkZVJavB4a3rg==
+ dependencies:
+ "@types/d3-array" "*"
+ "@types/geojson" "*"
+
+"@types/d3-delaunay@*":
+ version "6.0.4"
+ resolved "https://registry.npmjs.org/@types/d3-delaunay/-/d3-delaunay-6.0.4.tgz"
+ integrity sha512-ZMaSKu4THYCU6sV64Lhg6qjf1orxBthaC161plr5KuPHo3CNm8DTHiLw/5Eq2b6TsNP0W0iJrUOFscY6Q450Hw==
+
+"@types/d3-dispatch@*":
+ version "3.0.7"
+ resolved "https://registry.npmjs.org/@types/d3-dispatch/-/d3-dispatch-3.0.7.tgz"
+ integrity sha512-5o9OIAdKkhN1QItV2oqaE5KMIiXAvDWBDPrD85e58Qlz1c1kI/J0NcqbEG88CoTwJrYe7ntUCVfeUl2UJKbWgA==
+
+"@types/d3-drag@*":
+ version "3.0.7"
+ resolved "https://registry.npmjs.org/@types/d3-drag/-/d3-drag-3.0.7.tgz"
+ integrity sha512-HE3jVKlzU9AaMazNufooRJ5ZpWmLIoc90A37WU2JMmeq28w1FQqCZswHZ3xR+SuxYftzHq6WU6KJHvqxKzTxxQ==
+ dependencies:
+ "@types/d3-selection" "*"
+
+"@types/d3-dsv@*":
+ version "3.0.7"
+ resolved "https://registry.npmjs.org/@types/d3-dsv/-/d3-dsv-3.0.7.tgz"
+ integrity sha512-n6QBF9/+XASqcKK6waudgL0pf/S5XHPPI8APyMLLUHd8NqouBGLsU8MgtO7NINGtPBtk9Kko/W4ea0oAspwh9g==
+
+"@types/d3-ease@*":
+ version "3.0.2"
+ resolved "https://registry.npmjs.org/@types/d3-ease/-/d3-ease-3.0.2.tgz"
+ integrity sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==
+
+"@types/d3-fetch@*":
+ version "3.0.7"
+ resolved "https://registry.npmjs.org/@types/d3-fetch/-/d3-fetch-3.0.7.tgz"
+ integrity sha512-fTAfNmxSb9SOWNB9IoG5c8Hg6R+AzUHDRlsXsDZsNp6sxAEOP0tkP3gKkNSO/qmHPoBFTxNrjDprVHDQDvo5aA==
+ dependencies:
+ "@types/d3-dsv" "*"
+
+"@types/d3-force@*":
+ version "3.0.10"
+ resolved "https://registry.npmjs.org/@types/d3-force/-/d3-force-3.0.10.tgz"
+ integrity sha512-ZYeSaCF3p73RdOKcjj+swRlZfnYpK1EbaDiYICEEp5Q6sUiqFaFQ9qgoshp5CzIyyb/yD09kD9o2zEltCexlgw==
+
+"@types/d3-format@*":
+ version "3.0.4"
+ resolved "https://registry.npmjs.org/@types/d3-format/-/d3-format-3.0.4.tgz"
+ integrity sha512-fALi2aI6shfg7vM5KiR1wNJnZ7r6UuggVqtDA+xiEdPZQwy/trcQaHnwShLuLdta2rTymCNpxYTiMZX/e09F4g==
+
+"@types/d3-geo@*":
+ version "3.1.0"
+ resolved "https://registry.npmjs.org/@types/d3-geo/-/d3-geo-3.1.0.tgz"
+ integrity sha512-856sckF0oP/diXtS4jNsiQw/UuK5fQG8l/a9VVLeSouf1/PPbBE1i1W852zVwKwYCBkFJJB7nCFTbk6UMEXBOQ==
+ dependencies:
+ "@types/geojson" "*"
+
+"@types/d3-hierarchy@*":
+ version "3.1.7"
+ resolved "https://registry.npmjs.org/@types/d3-hierarchy/-/d3-hierarchy-3.1.7.tgz"
+ integrity sha512-tJFtNoYBtRtkNysX1Xq4sxtjK8YgoWUNpIiUee0/jHGRwqvzYxkq0hGVbbOGSz+JgFxxRu4K8nb3YpG3CMARtg==
+
+"@types/d3-interpolate@*":
+ version "3.0.4"
+ resolved "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz"
+ integrity sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==
+ dependencies:
+ "@types/d3-color" "*"
+
+"@types/d3-path@*":
+ version "3.1.1"
+ resolved "https://registry.npmjs.org/@types/d3-path/-/d3-path-3.1.1.tgz"
+ integrity sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==
+
+"@types/d3-polygon@*":
+ version "3.0.2"
+ resolved "https://registry.npmjs.org/@types/d3-polygon/-/d3-polygon-3.0.2.tgz"
+ integrity sha512-ZuWOtMaHCkN9xoeEMr1ubW2nGWsp4nIql+OPQRstu4ypeZ+zk3YKqQT0CXVe/PYqrKpZAi+J9mTs05TKwjXSRA==
+
+"@types/d3-quadtree@*":
+ version "3.0.6"
+ resolved "https://registry.npmjs.org/@types/d3-quadtree/-/d3-quadtree-3.0.6.tgz"
+ integrity sha512-oUzyO1/Zm6rsxKRHA1vH0NEDG58HrT5icx/azi9MF1TWdtttWl0UIUsjEQBBh+SIkrpd21ZjEv7ptxWys1ncsg==
+
+"@types/d3-random@*":
+ version "3.0.3"
+ resolved "https://registry.npmjs.org/@types/d3-random/-/d3-random-3.0.3.tgz"
+ integrity sha512-Imagg1vJ3y76Y2ea0871wpabqp613+8/r0mCLEBfdtqC7xMSfj9idOnmBYyMoULfHePJyxMAw3nWhJxzc+LFwQ==
+
+"@types/d3-scale-chromatic@*":
+ version "3.1.0"
+ resolved "https://registry.npmjs.org/@types/d3-scale-chromatic/-/d3-scale-chromatic-3.1.0.tgz"
+ integrity sha512-iWMJgwkK7yTRmWqRB5plb1kadXyQ5Sj8V/zYlFGMUBbIPKQScw+Dku9cAAMgJG+z5GYDoMjWGLVOvjghDEFnKQ==
+
+"@types/d3-scale@*":
+ version "4.0.9"
+ resolved "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-4.0.9.tgz"
+ integrity sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==
+ dependencies:
+ "@types/d3-time" "*"
+
+"@types/d3-selection@*":
+ version "3.0.11"
+ resolved "https://registry.npmjs.org/@types/d3-selection/-/d3-selection-3.0.11.tgz"
+ integrity sha512-bhAXu23DJWsrI45xafYpkQ4NtcKMwWnAC/vKrd2l+nxMFuvOT3XMYTIj2opv8vq8AO5Yh7Qac/nSeP/3zjTK0w==
+
+"@types/d3-shape@*":
+ version "3.1.8"
+ resolved "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.1.8.tgz"
+ integrity sha512-lae0iWfcDeR7qt7rA88BNiqdvPS5pFVPpo5OfjElwNaT2yyekbM0C9vK+yqBqEmHr6lDkRnYNoTBYlAgJa7a4w==
+ dependencies:
+ "@types/d3-path" "*"
+
+"@types/d3-time-format@*":
+ version "4.0.3"
+ resolved "https://registry.npmjs.org/@types/d3-time-format/-/d3-time-format-4.0.3.tgz"
+ integrity sha512-5xg9rC+wWL8kdDj153qZcsJ0FWiFt0J5RB6LYUNZjwSnesfblqrI/bJ1wBdJ8OQfncgbJG5+2F+qfqnqyzYxyg==
+
+"@types/d3-time@*":
+ version "3.0.4"
+ resolved "https://registry.npmjs.org/@types/d3-time/-/d3-time-3.0.4.tgz"
+ integrity sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==
+
+"@types/d3-timer@*":
+ version "3.0.2"
+ resolved "https://registry.npmjs.org/@types/d3-timer/-/d3-timer-3.0.2.tgz"
+ integrity sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==
+
+"@types/d3-transition@*":
+ version "3.0.9"
+ resolved "https://registry.npmjs.org/@types/d3-transition/-/d3-transition-3.0.9.tgz"
+ integrity sha512-uZS5shfxzO3rGlu0cC3bjmMFKsXv+SmZZcgp0KD22ts4uGXp5EVYGzu/0YdwZeKmddhcAccYtREJKkPfXkZuCg==
+ dependencies:
+ "@types/d3-selection" "*"
+
+"@types/d3-zoom@*":
+ version "3.0.8"
+ resolved "https://registry.npmjs.org/@types/d3-zoom/-/d3-zoom-3.0.8.tgz"
+ integrity sha512-iqMC4/YlFCSlO8+2Ii1GGGliCAY4XdeG748w5vQUbevlbDu0zSjH/+jojorQVBK/se0j6DUFNPBGSqD3YWYnDw==
+ dependencies:
+ "@types/d3-interpolate" "*"
+ "@types/d3-selection" "*"
+
+"@types/d3@^7.4.3":
+ version "7.4.3"
+ resolved "https://registry.npmjs.org/@types/d3/-/d3-7.4.3.tgz"
+ integrity sha512-lZXZ9ckh5R8uiFVt8ogUNf+pIrK4EsWrx2Np75WvF/eTpJ0FMHNhjXk8CKEx/+gpHbNQyJWehbFaTvqmHWB3ww==
+ dependencies:
+ "@types/d3-array" "*"
+ "@types/d3-axis" "*"
+ "@types/d3-brush" "*"
+ "@types/d3-chord" "*"
+ "@types/d3-color" "*"
+ "@types/d3-contour" "*"
+ "@types/d3-delaunay" "*"
+ "@types/d3-dispatch" "*"
+ "@types/d3-drag" "*"
+ "@types/d3-dsv" "*"
+ "@types/d3-ease" "*"
+ "@types/d3-fetch" "*"
+ "@types/d3-force" "*"
+ "@types/d3-format" "*"
+ "@types/d3-geo" "*"
+ "@types/d3-hierarchy" "*"
+ "@types/d3-interpolate" "*"
+ "@types/d3-path" "*"
+ "@types/d3-polygon" "*"
+ "@types/d3-quadtree" "*"
+ "@types/d3-random" "*"
+ "@types/d3-scale" "*"
+ "@types/d3-scale-chromatic" "*"
+ "@types/d3-selection" "*"
+ "@types/d3-shape" "*"
+ "@types/d3-time" "*"
+ "@types/d3-time-format" "*"
+ "@types/d3-timer" "*"
+ "@types/d3-transition" "*"
+ "@types/d3-zoom" "*"
+
+"@types/geojson@*":
+ version "7946.0.16"
+ resolved "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.16.tgz"
+ integrity sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==
+
"@types/http-cache-semantics@^4.0.2":
version "4.0.4"
resolved "https://registry.npmjs.org/@types/http-cache-semantics/-/http-cache-semantics-4.0.4.tgz"
@@ -2755,7 +3246,7 @@
dependencies:
"@types/node" "*"
-"@types/node@*", "@types/node@>=13.7.0", "@types/node@^24.2.1":
+"@types/node@*", "@types/node@^24.2.1", "@types/node@>=13.7.0":
version "24.2.1"
resolved "https://registry.npmjs.org/@types/node/-/node-24.2.1.tgz"
integrity sha512-DRh5K+ka5eJic8CjH7td8QpYEV6Zo10gfRkjHCO3weqZHWDtAaSTFtl4+VMqOJ4N5jcuhZ9/l+yy8rVgw7BQeQ==
@@ -2820,18 +3311,65 @@
dependencies:
"@types/node" "*"
+"@types/trusted-types@^2.0.7":
+ version "2.0.7"
+ resolved "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz"
+ integrity sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==
+
"@types/uuid@^9.0.1":
version "9.0.8"
resolved "https://registry.npmjs.org/@types/uuid/-/uuid-9.0.8.tgz"
integrity sha512-jg+97EGIcY9AGHJJRaaPVgetKDsrTgbRjQ5Msgjh/DQKEFl0DtyRr/VCOyD1T2R1MNeWPK/u7JoGhlDZnKBAfA==
-JSONStream@1.3.2:
- version "1.3.2"
- resolved "https://registry.npmjs.org/JSONStream/-/JSONStream-1.3.2.tgz"
- integrity sha512-mn0KSip7N4e0UDPZHnqDsHECo5uGQrixQKnAskOM1BIB8hd7QKbd6il8IPRPudPHOeHiECoCFqhyMaRO9+nWyA==
+"@types/yauzl@^2.9.1":
+ version "2.10.3"
+ resolved "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.10.3.tgz"
+ integrity sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==
dependencies:
- jsonparse "^1.2.0"
- through ">=2.2.7 <3"
+ "@types/node" "*"
+
+"@upsetjs/venn.js@^2.0.0":
+ version "2.0.0"
+ resolved "https://registry.npmjs.org/@upsetjs/venn.js/-/venn.js-2.0.0.tgz"
+ integrity sha512-WbBhLrooyePuQ1VZxrJjtLvTc4NVfpOyKx0sKqioq9bX1C1m7Jgykkn8gLrtwumBioXIqam8DLxp88Adbue6Hw==
+ optionalDependencies:
+ d3-selection "^3.0.0"
+ d3-transition "^3.0.1"
+
+"@zenuml/core@^3.35.2":
+ version "3.46.0"
+ resolved "https://registry.npmjs.org/@zenuml/core/-/core-3.46.0.tgz"
+ integrity sha512-bjG37doCwXer4DhlBh1d7bHUuJbqAK/1I+kHyL7Evy0eWZnBOTEOfp6LodB5cokw3w2IMxzYuj7qyRzwjqoeiA==
+ dependencies:
+ "@floating-ui/react" "^0.27.16"
+ "@headlessui/react" "^2.2.9"
+ "@headlessui/tailwindcss" "^0.2.2"
+ antlr4 "~4.11.0"
+ class-variance-authority "^0.7.1"
+ clsx "^2.1.1"
+ color-string "^2.1.4"
+ dompurify "^3.3.1"
+ highlight.js "^10.7.3"
+ html-to-image "^1.11.13"
+ immer "^10.2.0"
+ jotai "^2.16.1"
+ lodash "^4.17.21"
+ marked "^4.3.0"
+ pako "^2.1.0"
+ pino "^8.21.0"
+ radash "^12.1.1"
+ ramda "^0.28.0"
+ react "^19.2.3"
+ react-dom "^19.2.3"
+ tailwind-merge "^3.4.0"
+ tailwindcss "^3.4.19"
+
+abort-controller@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz"
+ integrity sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==
+ dependencies:
+ event-target-shim "^5.0.0"
acorn-import-attributes@^1.9.5:
version "1.9.5"
@@ -2845,15 +3383,10 @@ acorn-walk@^8.1.1:
dependencies:
acorn "^8.11.0"
-acorn@^8.11.0, acorn@^8.4.1:
- version "8.14.0"
- resolved "https://registry.npmjs.org/acorn/-/acorn-8.14.0.tgz"
- integrity sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==
-
-acorn@^8.14.0:
- version "8.15.0"
- resolved "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz"
- integrity sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==
+acorn@^8, acorn@^8.11.0, acorn@^8.14.0, acorn@^8.16.0, acorn@^8.4.1:
+ version "8.16.0"
+ resolved "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz"
+ integrity sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==
adm-zip@^0.4.16:
version "0.4.16"
@@ -2865,6 +3398,16 @@ aes-js@4.0.0-beta.5:
resolved "https://registry.npmjs.org/aes-js/-/aes-js-4.0.0-beta.5.tgz"
integrity sha512-G965FqalsNyrPqgEGON7nIx1e/OVENSgiEIzyC63haUMuvNnwIgIjMs52hlTCKhkBny7A2ORNlfY9Zu+jmGk1Q==
+agent-base@^7.1.0:
+ version "7.1.4"
+ resolved "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz"
+ integrity sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==
+
+agent-base@^7.1.2:
+ version "7.1.4"
+ resolved "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz"
+ integrity sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==
+
agent-base@6:
version "6.0.2"
resolved "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz"
@@ -2885,7 +3428,7 @@ ajv-errors@^1.0.1:
resolved "https://registry.npmjs.org/ajv-errors/-/ajv-errors-1.0.1.tgz"
integrity sha512-DCRfO/4nQ+89p/RK43i8Ezd41EqdGIU4ld7nGF8OQ14oc/we5rEntLCUa7+jrn3nn83BosfwZA0wb4pon2o8iQ==
-ajv@^6.12.6:
+ajv@^6.12.6, ajv@>=5.0.0, "ajv@4.11.8 - 8":
version "6.12.6"
resolved "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz"
integrity sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==
@@ -2928,6 +3471,11 @@ ansi-colors@^4.1.1, ansi-colors@^4.1.3:
resolved "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.3.tgz"
integrity sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==
+ansi-escapes@^3.0.0:
+ version "3.2.0"
+ resolved "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-3.2.0.tgz"
+ integrity sha512-cBhpre4ma+U0T1oM5fXg7Dy1Jw7zzwv7lt/GoCpr+hDQJoYnKVPLL4dCvSEFMmQurOQvSrwT7SL/DAlhBI97RQ==
+
ansi-escapes@^4.3.0:
version "4.3.2"
resolved "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz"
@@ -2935,6 +3483,16 @@ ansi-escapes@^4.3.0:
dependencies:
type-fest "^0.21.3"
+ansi-regex@^2.0.0:
+ version "2.1.1"
+ resolved "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz"
+ integrity sha512-TIGnTpdo+E3+pCyAluZvtED5p5wCqLdezCyhPZzKPcxvFplEt4i+W7OONCKgeZFT3+y5NZZfOOS/Bdcanm1MYA==
+
+ansi-regex@^3.0.0:
+ version "3.0.1"
+ resolved "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.1.tgz"
+ integrity sha512-+O9Jct8wf++lXxxFc4hc8LsjaSq0HFzzL7cVsw8pRDIPdjKD2mT4ytDZlLuSBZ4cLKZFXIrMGO7DbQCtMJJMKw==
+
ansi-regex@^5.0.1:
version "5.0.1"
resolved "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz"
@@ -2945,8 +3503,20 @@ ansi-regex@^6.0.1:
resolved "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz"
integrity sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==
-ansi-styles@^4.0.0, ansi-styles@^4.1.0:
- version "4.3.0"
+ansi-styles@^2.2.1:
+ version "2.2.1"
+ resolved "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz"
+ integrity sha512-kmCevFghRiWM7HB5zTPULl4r9bVFSWjz62MhqizDGUrq2NWuNMQyuv4tHHoKJHs69M/MF64lEcHdYIocrdWQYA==
+
+ansi-styles@^3.2.1:
+ version "3.2.1"
+ resolved "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz"
+ integrity sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==
+ dependencies:
+ color-convert "^1.9.0"
+
+ansi-styles@^4.0.0, ansi-styles@^4.1.0:
+ version "4.3.0"
resolved "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz"
integrity sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==
dependencies:
@@ -2962,6 +3532,21 @@ antlr4@^4.13.1-patch-1:
resolved "https://registry.npmjs.org/antlr4/-/antlr4-4.13.2.tgz"
integrity sha512-QiVbZhyy4xAZ17UPEuG3YTOt8ZaoeOR1CvEAqrEsDBsOqINslaB147i9xqljZqoyf5S+EUlGStaj+t22LT9MOg==
+antlr4@~4.11.0:
+ version "4.11.0"
+ resolved "https://registry.npmjs.org/antlr4/-/antlr4-4.11.0.tgz"
+ integrity sha512-GUGlpE2JUjAN+G8G5vY+nOoeyNhHsXoIJwP1XF1oRw89vifA1K46T6SEkwLwr7drihN7I/lf0DIjKc4OZvBX8w==
+
+any-observable@^0.3.0:
+ version "0.3.0"
+ resolved "https://registry.npmjs.org/any-observable/-/any-observable-0.3.0.tgz"
+ integrity sha512-/FQM1EDkTsf63Ub2C6O7GuYFDsSXUwsaZDurV0np41ocwq0jthUAYCmhBX9f+KwlaCgIuWyr/4WlUQUBfKfZog==
+
+any-promise@^1.0.0:
+ version "1.3.0"
+ resolved "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz"
+ integrity sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==
+
anymatch@~3.1.2:
version "3.1.3"
resolved "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz"
@@ -2975,6 +3560,18 @@ arg@^4.1.0:
resolved "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz"
integrity sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==
+arg@^5.0.2:
+ version "5.0.2"
+ resolved "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz"
+ integrity sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==
+
+argparse@^1.0.7:
+ version "1.0.10"
+ resolved "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz"
+ integrity sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==
+ dependencies:
+ sprintf-js "~1.0.2"
+
argparse@^2.0.1:
version "2.0.1"
resolved "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz"
@@ -3002,6 +3599,13 @@ ast-parents@^0.0.1:
resolved "https://registry.npmjs.org/ast-parents/-/ast-parents-0.0.1.tgz"
integrity sha512-XHusKxKz3zoYk1ic8Un640joHbFMhbqneyoZfoKnEGtf2ey9Uh/IdpcQplODdO/kENaMIWsD0nJm4+wX3UNLHA==
+ast-types@^0.13.4:
+ version "0.13.4"
+ resolved "https://registry.npmjs.org/ast-types/-/ast-types-0.13.4.tgz"
+ integrity sha512-x1FCFnFifvYDDzTaLII71vG5uvDwgtmDTEVWAxrgeiR8VjMONcCXJx7E+USjDtHlwFmt9MysbqgF9b9Vjr6w+w==
+ dependencies:
+ tslib "^2.0.1"
+
astral-regex@^2.0.0:
version "2.0.0"
resolved "https://registry.npmjs.org/astral-regex/-/astral-regex-2.0.0.tgz"
@@ -3019,6 +3623,11 @@ asynckit@^0.4.0:
resolved "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz"
integrity sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==
+atomic-sleep@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.npmjs.org/atomic-sleep/-/atomic-sleep-1.0.0.tgz"
+ integrity sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ==
+
available-typed-arrays@^1.0.7:
version "1.0.7"
resolved "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz"
@@ -3035,11 +3644,59 @@ axios@^1.7.2, axios@^1.7.4, axios@^1.8.2:
form-data "^4.0.4"
proxy-from-env "^1.1.0"
+b4a@^1.6.4:
+ version "1.8.0"
+ resolved "https://registry.npmjs.org/b4a/-/b4a-1.8.0.tgz"
+ integrity sha512-qRuSmNSkGQaHwNbM7J78Wwy+ghLEYF1zNrSeMxj4Kgw6y33O3mXcQ6Ie9fRvfU/YnxWkOchPXbaLb73TkIsfdg==
+
balanced-match@^1.0.0:
version "1.0.2"
resolved "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz"
integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==
+bare-events@*, bare-events@^2.5.4, bare-events@^2.7.0:
+ version "2.8.2"
+ resolved "https://registry.npmjs.org/bare-events/-/bare-events-2.8.2.tgz"
+ integrity sha512-riJjyv1/mHLIPX4RwiK+oW9/4c3TEUeORHKefKAKnZ5kyslbN+HXowtbaVEqt4IMUB7OXlfixcs6gsFeo/jhiQ==
+
+bare-fs@^4.0.1, bare-fs@^4.5.5:
+ version "4.5.5"
+ resolved "https://registry.npmjs.org/bare-fs/-/bare-fs-4.5.5.tgz"
+ integrity sha512-XvwYM6VZqKoqDll8BmSww5luA5eflDzY0uEFfBJtFKe4PAAtxBjU3YIxzIBzhyaEQBy1VXEQBto4cpN5RZJw+w==
+ dependencies:
+ bare-events "^2.5.4"
+ bare-path "^3.0.0"
+ bare-stream "^2.6.4"
+ bare-url "^2.2.2"
+ fast-fifo "^1.3.2"
+
+bare-os@^3.0.1:
+ version "3.7.1"
+ resolved "https://registry.npmjs.org/bare-os/-/bare-os-3.7.1.tgz"
+ integrity sha512-ebvMaS5BgZKmJlvuWh14dg9rbUI84QeV3WlWn6Ph6lFI8jJoh7ADtVTyD2c93euwbe+zgi0DVrl4YmqXeM9aIA==
+
+bare-path@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.npmjs.org/bare-path/-/bare-path-3.0.0.tgz"
+ integrity sha512-tyfW2cQcB5NN8Saijrhqn0Zh7AnFNsnczRcuWODH0eYAXBsJ5gVxAUuNr7tsHSC6IZ77cA0SitzT+s47kot8Mw==
+ dependencies:
+ bare-os "^3.0.1"
+
+bare-stream@^2.6.4:
+ version "2.8.1"
+ resolved "https://registry.npmjs.org/bare-stream/-/bare-stream-2.8.1.tgz"
+ integrity sha512-bSeR8RfvbRwDpD7HWZvn8M3uYNDrk7m9DQjYOFkENZlXW8Ju/MPaqUPQq5LqJ3kyjEm07siTaAQ7wBKCU59oHg==
+ dependencies:
+ streamx "^2.21.0"
+ teex "^1.0.1"
+
+bare-url@^2.2.2:
+ version "2.3.2"
+ resolved "https://registry.npmjs.org/bare-url/-/bare-url-2.3.2.tgz"
+ integrity sha512-ZMq4gd9ngV5aTMa5p9+UfY0b3skwhHELaDkhEHetMdX0LRkW9kzaym4oo/Eh+Ghm0CCDuMTsRIGM/ytUc1ZYmw==
+ dependencies:
+ bare-path "^3.0.0"
+
base-x@^3.0.2:
version "3.0.11"
resolved "https://registry.npmjs.org/base-x/-/base-x-3.0.11.tgz"
@@ -3052,6 +3709,11 @@ base64-js@^1.0.2, base64-js@^1.3.1:
resolved "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz"
integrity sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==
+basic-ftp@^5.0.2:
+ version "5.2.0"
+ resolved "https://registry.npmjs.org/basic-ftp/-/basic-ftp-5.2.0.tgz"
+ integrity sha512-VoMINM2rqJwJgfdHq6RiUudKt2BV+FY5ZFezP/ypmwayk68+NzzAQy4XXLlqsGD4MCzq3DrmNFD/uUmBJuGoXw==
+
bcrypt-pbkdf@^1.0.2:
version "1.0.2"
resolved "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz"
@@ -3131,6 +3793,14 @@ boxen@^5.1.2:
widest-line "^3.1.0"
wrap-ansi "^7.0.0"
+brace-expansion@^1.1.7:
+ version "1.1.12"
+ resolved "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz"
+ integrity sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==
+ dependencies:
+ balanced-match "^1.0.0"
+ concat-map "0.0.1"
+
brace-expansion@^2.0.1:
version "2.0.2"
resolved "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz"
@@ -3196,6 +3866,11 @@ buffer-alloc@^1.2.0:
buffer-alloc-unsafe "^1.1.0"
buffer-fill "^1.0.0"
+buffer-crc32@~0.2.3:
+ version "0.2.13"
+ resolved "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz"
+ integrity sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==
+
buffer-fill@^1.0.0:
version "1.0.0"
resolved "https://registry.npmjs.org/buffer-fill/-/buffer-fill-1.0.0.tgz"
@@ -3211,14 +3886,13 @@ buffer-xor@^1.0.3:
resolved "https://registry.npmjs.org/buffer-xor/-/buffer-xor-1.0.3.tgz"
integrity sha512-571s0T7nZWK6vB67HI5dyUF7wXiNcfaPPPTl6zYCNApANjIvYJTg7hlud/+cJpdAhS7dVzqMLmfhfHR3rAcOjQ==
-buffer@4.9.2:
- version "4.9.2"
- resolved "https://registry.npmjs.org/buffer/-/buffer-4.9.2.tgz"
- integrity sha512-xq+q3SRMOxGivLhBNaUdC64hDTQwejJ+H0T/NB1XMtTVEwNTrfFF3gAxiyW0Bu/xWEGhjVKgUcMhCrUy2+uCWg==
+buffer@^5.2.1:
+ version "5.7.1"
+ resolved "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz"
+ integrity sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==
dependencies:
- base64-js "^1.0.2"
- ieee754 "^1.1.4"
- isarray "^1.0.0"
+ base64-js "^1.3.1"
+ ieee754 "^1.1.13"
buffer@^5.5.0:
version "5.7.1"
@@ -3228,11 +3902,33 @@ buffer@^5.5.0:
base64-js "^1.3.1"
ieee754 "^1.1.13"
+buffer@^6.0.3:
+ version "6.0.3"
+ resolved "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz"
+ integrity sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==
+ dependencies:
+ base64-js "^1.3.1"
+ ieee754 "^1.2.1"
+
+buffer@4.9.2:
+ version "4.9.2"
+ resolved "https://registry.npmjs.org/buffer/-/buffer-4.9.2.tgz"
+ integrity sha512-xq+q3SRMOxGivLhBNaUdC64hDTQwejJ+H0T/NB1XMtTVEwNTrfFF3gAxiyW0Bu/xWEGhjVKgUcMhCrUy2+uCWg==
+ dependencies:
+ base64-js "^1.0.2"
+ ieee754 "^1.1.4"
+ isarray "^1.0.0"
+
buildcheck@~0.0.6:
version "0.0.6"
resolved "https://registry.npmjs.org/buildcheck/-/buildcheck-0.0.6.tgz"
integrity sha512-8f9ZJCUXyT1M35Jx7MkBgmBMo3oHTTBIPLiY9xyL0pl3T5RwcPEY8cUHr5LBNfu/fk6c2T4DJZuVM/8ZZT2D2A==
+bytes@3.0.0:
+ version "3.0.0"
+ resolved "https://registry.npmjs.org/bytes/-/bytes-3.0.0.tgz"
+ integrity sha512-pMhOfFDPiv9t5jjIXkHosWmkSyQbvsgEVNkz0ERHbuLh2T/7j4Mqqpz523Fe8MVY89KC6Sh/QfS2sM+SjgFDcw==
+
bytes@3.1.2:
version "3.1.2"
resolved "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz"
@@ -3287,6 +3983,11 @@ callsites@^3.0.0, callsites@^3.1.0:
resolved "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz"
integrity sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==
+camelcase-css@^2.0.1:
+ version "2.0.1"
+ resolved "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz"
+ integrity sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==
+
camelcase@^6.0.0, camelcase@^6.2.0:
version "6.3.0"
resolved "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz"
@@ -3313,7 +4014,7 @@ cbor@^9.0.2:
dependencies:
nofilter "^3.1.0"
-chai@^4.3.4, chai@^4.4.1:
+chai@^4.0.0, chai@^4.3.4, chai@^4.4.1:
version "4.5.0"
resolved "https://registry.npmjs.org/chai/-/chai-4.5.0.tgz"
integrity sha512-RITGBfijLkBddZvnn8jdqoTypxvqbOLYQkGGxXzeFjVHvudaPw0HNFD9x928/eUwYWd2dPCugVqspGALTZZQKw==
@@ -3333,6 +4034,26 @@ chalk-template@^1.1.0:
dependencies:
chalk "^5.2.0"
+chalk@^1.0.0, chalk@^1.1.3:
+ version "1.1.3"
+ resolved "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz"
+ integrity sha512-U3lRVLMSlsCfjqYPbLyVv11M9CPW4I728d6TCKMAOJueEeB9/8o+eSsMnxPJD+Q+K909sdESg7C+tIkoH6on1A==
+ dependencies:
+ ansi-styles "^2.2.1"
+ escape-string-regexp "^1.0.2"
+ has-ansi "^2.0.0"
+ strip-ansi "^3.0.0"
+ supports-color "^2.0.0"
+
+chalk@^2.4.1:
+ version "2.4.2"
+ resolved "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz"
+ integrity sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==
+ dependencies:
+ ansi-styles "^3.2.1"
+ escape-string-regexp "^1.0.5"
+ supports-color "^5.3.0"
+
chalk@^4.1.0, chalk@^4.1.2:
version "4.1.2"
resolved "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz"
@@ -3341,6 +4062,11 @@ chalk@^4.1.0, chalk@^4.1.2:
ansi-styles "^4.1.0"
supports-color "^7.1.0"
+chalk@^5.0.1:
+ version "5.6.2"
+ resolved "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz"
+ integrity sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==
+
chalk@^5.2.0:
version "5.3.0"
resolved "https://registry.npmjs.org/chalk/-/chalk-5.3.0.tgz"
@@ -3358,6 +4084,40 @@ check-error@^1.0.3:
dependencies:
get-func-name "^2.0.2"
+chevrotain-allstar@~0.3.1:
+ version "0.3.1"
+ resolved "https://registry.npmjs.org/chevrotain-allstar/-/chevrotain-allstar-0.3.1.tgz"
+ integrity sha512-b7g+y9A0v4mxCW1qUhf3BSVPg+/NvGErk/dOkrDaHA0nQIQGAtrOjlX//9OQtRlSCy+x9rfB5N8yC71lH1nvMw==
+ dependencies:
+ lodash-es "^4.17.21"
+
+chevrotain@^11.0.0, chevrotain@~11.1.1:
+ version "11.1.2"
+ resolved "https://registry.npmjs.org/chevrotain/-/chevrotain-11.1.2.tgz"
+ integrity sha512-opLQzEVriiH1uUQ4Kctsd49bRoFDXGGSC4GUqj7pGyxM3RehRhvTlZJc1FL/Flew2p5uwxa1tUDWKzI4wNM8pg==
+ dependencies:
+ "@chevrotain/cst-dts-gen" "11.1.2"
+ "@chevrotain/gast" "11.1.2"
+ "@chevrotain/regexp-to-ast" "11.1.2"
+ "@chevrotain/types" "11.1.2"
+ "@chevrotain/utils" "11.1.2"
+ lodash-es "4.17.23"
+
+chokidar@^3.5.2:
+ version "3.6.0"
+ resolved "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz"
+ integrity sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==
+ dependencies:
+ anymatch "~3.1.2"
+ braces "~3.0.2"
+ glob-parent "~5.1.2"
+ is-binary-path "~2.1.0"
+ is-glob "~4.0.1"
+ normalize-path "~3.0.0"
+ readdirp "~3.6.0"
+ optionalDependencies:
+ fsevents "~2.3.2"
+
chokidar@^3.5.3:
version "3.6.0"
resolved "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz"
@@ -3373,6 +4133,21 @@ chokidar@^3.5.3:
optionalDependencies:
fsevents "~2.3.2"
+chokidar@^3.6.0:
+ version "3.6.0"
+ resolved "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz"
+ integrity sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==
+ dependencies:
+ anymatch "~3.1.2"
+ braces "~3.0.2"
+ glob-parent "~5.1.2"
+ is-binary-path "~2.1.0"
+ is-glob "~4.0.1"
+ normalize-path "~3.0.0"
+ readdirp "~3.6.0"
+ optionalDependencies:
+ fsevents "~2.3.2"
+
chokidar@^4.0.0:
version "4.0.1"
resolved "https://registry.npmjs.org/chokidar/-/chokidar-4.0.1.tgz"
@@ -3392,6 +4167,14 @@ chownr@^1.0.1, chownr@^1.1.1:
resolved "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz"
integrity sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==
+chromium-bidi@0.11.0:
+ version "0.11.0"
+ resolved "https://registry.npmjs.org/chromium-bidi/-/chromium-bidi-0.11.0.tgz"
+ integrity sha512-6CJWHkNRoyZyjV9Rwv2lYONZf1Xm0IuDyNq97nwSsxxP3wf5Bwy15K5rOvVKMtJ127jJBmxFUanSAOjgFRxgrA==
+ dependencies:
+ mitt "3.0.1"
+ zod "3.23.8"
+
ci-info@^2.0.0:
version "2.0.0"
resolved "https://registry.npmjs.org/ci-info/-/ci-info-2.0.0.tgz"
@@ -3410,6 +4193,13 @@ cjs-module-lexer@^1.2.2:
resolved "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-1.4.3.tgz"
integrity sha512-9z8TZaGM1pfswYeXrUpzPrkx8UnWYdhJclsiYMm6x/w5+nN+8Tf/LnAgfLGQCm59qAOxU8WwHEq2vNwF6i4j+Q==
+class-variance-authority@^0.7.1:
+ version "0.7.1"
+ resolved "https://registry.npmjs.org/class-variance-authority/-/class-variance-authority-0.7.1.tgz"
+ integrity sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==
+ dependencies:
+ clsx "^2.1.1"
+
clean-stack@^2.0.0:
version "2.2.0"
resolved "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz"
@@ -3428,6 +4218,21 @@ cli-boxes@^2.2.1:
resolved "https://registry.npmjs.org/cli-boxes/-/cli-boxes-2.2.1.tgz"
integrity sha512-y4coMcylgSCdVinjiDBuR8PCC2bLjyGTwEmPb9NHR/QaNU6EUOXcTY/s6VjGMD6ENSEaeQYHCY0GNGS5jfMwPw==
+cli-cursor@^2.0.0, cli-cursor@^2.1.0:
+ version "2.1.0"
+ resolved "https://registry.npmjs.org/cli-cursor/-/cli-cursor-2.1.0.tgz"
+ integrity sha512-8lgKz8LmCRYZZQDpRyT2m5rKJ08TnU4tR9FFFW2rxpxR1FzWi4PQ/NfyODchAatHaUgnSPVcx/R5w6NuTBzFiw==
+ dependencies:
+ restore-cursor "^2.0.0"
+
+cli-truncate@^0.2.1:
+ version "0.2.1"
+ resolved "https://registry.npmjs.org/cli-truncate/-/cli-truncate-0.2.1.tgz"
+ integrity sha512-f4r4yJnbT++qUPI9NR4XLDLq41gQ+uqnPItWG0F5ZkehuNiTTa3EY0S4AqTSUOeJ7/zU41oWPQSNkW5BqPL9bg==
+ dependencies:
+ slice-ansi "0.0.4"
+ string-width "^1.0.1"
+
cliui@^7.0.2:
version "7.0.4"
resolved "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz"
@@ -3446,11 +4251,28 @@ cliui@^8.0.1:
strip-ansi "^6.0.1"
wrap-ansi "^7.0.0"
+clsx@^2.0.0, clsx@^2.1.1:
+ version "2.1.1"
+ resolved "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz"
+ integrity sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==
+
code-block-writer@^13.0.1:
version "13.0.3"
resolved "https://registry.npmjs.org/code-block-writer/-/code-block-writer-13.0.3.tgz"
integrity sha512-Oofo0pq3IKnsFtuHqSF7TqBfr71aeyZDVJ0HpmqB7FBM2qEigL0iPONSCZSO9pE9dZTAxANe5XHG9Uy0YMv8cg==
+code-point-at@^1.0.0:
+ version "1.1.0"
+ resolved "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz"
+ integrity sha512-RpAVKQA5T63xEj6/giIbUEtZwJ4UFIc3ZtvEkiaUERylqe8xb5IvqcgOurZLahv93CLKfxcw5YI+DZcUBRyLXA==
+
+color-convert@^1.9.0:
+ version "1.9.3"
+ resolved "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz"
+ integrity sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==
+ dependencies:
+ color-name "1.1.3"
+
color-convert@^2.0.1:
version "2.0.1"
resolved "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz"
@@ -3458,11 +4280,28 @@ color-convert@^2.0.1:
dependencies:
color-name "~1.1.4"
+color-name@^2.0.0:
+ version "2.1.0"
+ resolved "https://registry.npmjs.org/color-name/-/color-name-2.1.0.tgz"
+ integrity sha512-1bPaDNFm0axzE4MEAzKPuqKWeRaT43U/hyxKPBdqTfmPF+d6n7FSoTFxLVULUJOmiLp01KjhIPPH+HrXZJN4Rg==
+
color-name@~1.1.4:
version "1.1.4"
resolved "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz"
integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==
+color-name@1.1.3:
+ version "1.1.3"
+ resolved "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz"
+ integrity sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==
+
+color-string@^2.1.4:
+ version "2.1.4"
+ resolved "https://registry.npmjs.org/color-string/-/color-string-2.1.4.tgz"
+ integrity sha512-Bb6Cq8oq0IjDOe8wJmi4JeNn763Xs9cfrBcaylK1tPypWzyoy2G3l90v9k64kjphl/ZJjPIShFztenRomi8WTg==
+ dependencies:
+ color-name "^2.0.0"
+
combined-stream@^1.0.8:
version "1.0.8"
resolved "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz"
@@ -3481,15 +4320,30 @@ commander@^10.0.0:
integrity sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==
commander@^14.0.0:
- version "14.0.0"
- resolved "https://registry.npmjs.org/commander/-/commander-14.0.0.tgz"
- integrity sha512-2uM9rYjPvyq39NwLRqaiLtWHyDC1FvryJDa2ATTVims5YAS4PupsEQsDvP14FqhFr0P49CYDugi59xaxJlTXRA==
+ version "14.0.3"
+ resolved "https://registry.npmjs.org/commander/-/commander-14.0.3.tgz"
+ integrity sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw==
+
+commander@^4.0.0:
+ version "4.1.1"
+ resolved "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz"
+ integrity sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==
commander@^8.1.0:
version "8.3.0"
resolved "https://registry.npmjs.org/commander/-/commander-8.3.0.tgz"
integrity sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==
+commander@^8.3.0:
+ version "8.3.0"
+ resolved "https://registry.npmjs.org/commander/-/commander-8.3.0.tgz"
+ integrity sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==
+
+commander@7:
+ version "7.2.0"
+ resolved "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz"
+ integrity sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==
+
comment-json@^4.2.5:
version "4.2.5"
resolved "https://registry.npmjs.org/comment-json/-/comment-json-4.2.5.tgz"
@@ -3506,6 +4360,11 @@ compare-versions@^6.0.0, compare-versions@^6.1.0:
resolved "https://registry.npmjs.org/compare-versions/-/compare-versions-6.1.1.tgz"
integrity sha512-4hm4VPpIecmlg59CHXnRDnqGplJFrbLG4aFEl5vl6cK1u76ws3LLvX7ikFnTDl5vo39sjWD6AaDPYodJp/NNHg==
+concat-map@0.0.1:
+ version "0.0.1"
+ resolved "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz"
+ integrity sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==
+
concat-stream@~1.6.2:
version "1.6.2"
resolved "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.2.tgz"
@@ -3516,6 +4375,11 @@ concat-stream@~1.6.2:
readable-stream "^2.2.2"
typedarray "^0.0.6"
+confbox@^0.1.8:
+ version "0.1.8"
+ resolved "https://registry.npmjs.org/confbox/-/confbox-0.1.8.tgz"
+ integrity sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==
+
config-chain@^1.1.11:
version "1.1.13"
resolved "https://registry.npmjs.org/config-chain/-/config-chain-1.1.13.tgz"
@@ -3531,16 +4395,35 @@ console-table-printer@^2.9.0:
dependencies:
simple-wcswidth "^1.0.1"
-cookie@^0.4.1:
- version "0.4.2"
- resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.4.2.tgz#0e41f24de5ecf317947c82fc789e06a884824432"
- integrity sha512-aSWTXFzaKWkvHO1Ny/s+ePFpvKsPnjc551iI41v3ny/ow6tBG5Vd+FuqGNhh1LxOmVzOlGUriIlOaokOvhaStA==
+content-disposition@0.5.2:
+ version "0.5.2"
+ resolved "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.2.tgz"
+ integrity sha512-kRGRZw3bLlFISDBgwTSA1TMBFN6J6GWDeubmDE3AF+3+yXL8hTWv8r5rkLbqYXY4RjPk/EzHnClI3zQf1cFmHA==
+
+cookie@^0.7.2:
+ version "0.7.2"
+ resolved "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz"
+ integrity sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==
core-util-is@^1.0.3, core-util-is@~1.0.0:
version "1.0.3"
resolved "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz"
integrity sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==
+cose-base@^1.0.0:
+ version "1.0.3"
+ resolved "https://registry.npmjs.org/cose-base/-/cose-base-1.0.3.tgz"
+ integrity sha512-s9whTXInMSgAp/NVXVNuVxVKzGH2qck3aQlVHxDCdAEPgtMKwc4Wq6/QKhgdEdgbLSi9rBTAcPoRa6JpiG4ksg==
+ dependencies:
+ layout-base "^1.0.0"
+
+cose-base@^2.2.0:
+ version "2.2.0"
+ resolved "https://registry.npmjs.org/cose-base/-/cose-base-2.2.0.tgz"
+ integrity sha512-AzlgcsCbUMymkADOJtQm3wO9S3ltPfYOFD5033keQn9NJzIbtnZj+UdBJe7DYml/8TdbtHJW3j58SOnKhWY/5g==
+ dependencies:
+ layout-base "^2.0.0"
+
cosmiconfig@^8.0.0:
version "8.3.6"
resolved "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-8.3.6.tgz"
@@ -3551,6 +4434,16 @@ cosmiconfig@^8.0.0:
parse-json "^5.2.0"
path-type "^4.0.0"
+cosmiconfig@^9.0.0:
+ version "9.0.1"
+ resolved "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-9.0.1.tgz"
+ integrity sha512-hr4ihw+DBqcvrsEDioRO31Z17x71pUYoNe/4h6Z0wB72p7MU7/9gH8Q3s12NFhHPfYBBOV3qyfUxmr/Yn3shnQ==
+ dependencies:
+ env-paths "^2.2.1"
+ import-fresh "^3.3.0"
+ js-yaml "^4.1.0"
+ parse-json "^5.2.0"
+
cpu-features@~0.0.10:
version "0.0.10"
resolved "https://registry.npmjs.org/cpu-features/-/cpu-features-0.0.10.tgz"
@@ -3722,17 +4615,328 @@ cspell@^9.2.0:
semver "^7.7.2"
tinyglobby "^0.2.14"
+cssesc@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz"
+ integrity sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==
+
csv-parse@^6.1.0:
version "6.1.0"
resolved "https://registry.npmjs.org/csv-parse/-/csv-parse-6.1.0.tgz"
integrity sha512-CEE+jwpgLn+MmtCpVcPtiCZpVtB6Z2OKPTr34pycYYoL7sxdOkXDdQ4lRiw6ioC0q6BLqhc6cKweCVvral8yhw==
-debug@4, debug@^4.1.1, debug@^4.3.5:
- version "4.3.7"
- resolved "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz"
- integrity sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==
+cytoscape-cose-bilkent@^4.1.0:
+ version "4.1.0"
+ resolved "https://registry.npmjs.org/cytoscape-cose-bilkent/-/cytoscape-cose-bilkent-4.1.0.tgz"
+ integrity sha512-wgQlVIUJF13Quxiv5e1gstZ08rnZj2XaLHGoFMYXz7SkNfCDOOteKBE6SYRfA9WxxI/iBc3ajfDoc6hb/MRAHQ==
dependencies:
- ms "^2.1.3"
+ cose-base "^1.0.0"
+
+cytoscape-fcose@^2.2.0:
+ version "2.2.0"
+ resolved "https://registry.npmjs.org/cytoscape-fcose/-/cytoscape-fcose-2.2.0.tgz"
+ integrity sha512-ki1/VuRIHFCzxWNrsshHYPs6L7TvLu3DL+TyIGEsRcvVERmxokbf5Gdk7mFxZnTdiGtnA4cfSmjZJMviqSuZrQ==
+ dependencies:
+ cose-base "^2.2.0"
+
+cytoscape@^3.2.0, cytoscape@^3.33.1:
+ version "3.33.1"
+ resolved "https://registry.npmjs.org/cytoscape/-/cytoscape-3.33.1.tgz"
+ integrity sha512-iJc4TwyANnOGR1OmWhsS9ayRS3s+XQ185FmuHObThD+5AeJCakAAbWv8KimMTt08xCCLNgneQwFp+JRJOr9qGQ==
+
+d3-array@^3.2.0, "d3-array@2 - 3", "d3-array@2.10.0 - 3", "d3-array@2.5.0 - 3", d3-array@3:
+ version "3.2.4"
+ resolved "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz"
+ integrity sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==
+ dependencies:
+ internmap "1 - 2"
+
+"d3-array@1 - 2":
+ version "2.12.1"
+ resolved "https://registry.npmjs.org/d3-array/-/d3-array-2.12.1.tgz"
+ integrity sha512-B0ErZK/66mHtEsR1TkPEEkwdy+WDesimkM5gpZr5Dsg54BiTA5RXtYW5qTLIAcekaS9xfZrzBLF/OAkB3Qn1YQ==
+ dependencies:
+ internmap "^1.0.0"
+
+d3-axis@3:
+ version "3.0.0"
+ resolved "https://registry.npmjs.org/d3-axis/-/d3-axis-3.0.0.tgz"
+ integrity sha512-IH5tgjV4jE/GhHkRV0HiVYPDtvfjHQlQfJHs0usq7M30XcSBvOotpmH1IgkcXsO/5gEQZD43B//fc7SRT5S+xw==
+
+d3-brush@3:
+ version "3.0.0"
+ resolved "https://registry.npmjs.org/d3-brush/-/d3-brush-3.0.0.tgz"
+ integrity sha512-ALnjWlVYkXsVIGlOsuWH1+3udkYFI48Ljihfnh8FZPF2QS9o+PzGLBslO0PjzVoHLZ2KCVgAM8NVkXPJB2aNnQ==
+ dependencies:
+ d3-dispatch "1 - 3"
+ d3-drag "2 - 3"
+ d3-interpolate "1 - 3"
+ d3-selection "3"
+ d3-transition "3"
+
+d3-chord@3:
+ version "3.0.1"
+ resolved "https://registry.npmjs.org/d3-chord/-/d3-chord-3.0.1.tgz"
+ integrity sha512-VE5S6TNa+j8msksl7HwjxMHDM2yNK3XCkusIlpX5kwauBfXuyLAtNg9jCp/iHH61tgI4sb6R/EIMWCqEIdjT/g==
+ dependencies:
+ d3-path "1 - 3"
+
+"d3-color@1 - 3", d3-color@3:
+ version "3.1.0"
+ resolved "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz"
+ integrity sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==
+
+d3-contour@4:
+ version "4.0.2"
+ resolved "https://registry.npmjs.org/d3-contour/-/d3-contour-4.0.2.tgz"
+ integrity sha512-4EzFTRIikzs47RGmdxbeUvLWtGedDUNkTcmzoeyg4sP/dvCexO47AaQL7VKy/gul85TOxw+IBgA8US2xwbToNA==
+ dependencies:
+ d3-array "^3.2.0"
+
+d3-delaunay@6:
+ version "6.0.4"
+ resolved "https://registry.npmjs.org/d3-delaunay/-/d3-delaunay-6.0.4.tgz"
+ integrity sha512-mdjtIZ1XLAM8bm/hx3WwjfHt6Sggek7qH043O8KEjDXN40xi3vx/6pYSVTwLjEgiXQTbvaouWKynLBiUZ6SK6A==
+ dependencies:
+ delaunator "5"
+
+"d3-dispatch@1 - 3", d3-dispatch@3:
+ version "3.0.1"
+ resolved "https://registry.npmjs.org/d3-dispatch/-/d3-dispatch-3.0.1.tgz"
+ integrity sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==
+
+"d3-drag@2 - 3", d3-drag@3:
+ version "3.0.0"
+ resolved "https://registry.npmjs.org/d3-drag/-/d3-drag-3.0.0.tgz"
+ integrity sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg==
+ dependencies:
+ d3-dispatch "1 - 3"
+ d3-selection "3"
+
+"d3-dsv@1 - 3", d3-dsv@3:
+ version "3.0.1"
+ resolved "https://registry.npmjs.org/d3-dsv/-/d3-dsv-3.0.1.tgz"
+ integrity sha512-UG6OvdI5afDIFP9w4G0mNq50dSOsXHJaRE8arAS5o9ApWnIElp8GZw1Dun8vP8OyHOZ/QJUKUJwxiiCCnUwm+Q==
+ dependencies:
+ commander "7"
+ iconv-lite "0.6"
+ rw "1"
+
+"d3-ease@1 - 3", d3-ease@3:
+ version "3.0.1"
+ resolved "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz"
+ integrity sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==
+
+d3-fetch@3:
+ version "3.0.1"
+ resolved "https://registry.npmjs.org/d3-fetch/-/d3-fetch-3.0.1.tgz"
+ integrity sha512-kpkQIM20n3oLVBKGg6oHrUchHM3xODkTzjMoj7aWQFq5QEM+R6E4WkzT5+tojDY7yjez8KgCBRoj4aEr99Fdqw==
+ dependencies:
+ d3-dsv "1 - 3"
+
+d3-force@3:
+ version "3.0.0"
+ resolved "https://registry.npmjs.org/d3-force/-/d3-force-3.0.0.tgz"
+ integrity sha512-zxV/SsA+U4yte8051P4ECydjD/S+qeYtnaIyAs9tgHCqfguma/aAQDjo85A9Z6EKhBirHRJHXIgJUlffT4wdLg==
+ dependencies:
+ d3-dispatch "1 - 3"
+ d3-quadtree "1 - 3"
+ d3-timer "1 - 3"
+
+"d3-format@1 - 3", d3-format@3:
+ version "3.1.2"
+ resolved "https://registry.npmjs.org/d3-format/-/d3-format-3.1.2.tgz"
+ integrity sha512-AJDdYOdnyRDV5b6ArilzCPPwc1ejkHcoyFarqlPqT7zRYjhavcT3uSrqcMvsgh2CgoPbK3RCwyHaVyxYcP2Arg==
+
+d3-geo@3:
+ version "3.1.1"
+ resolved "https://registry.npmjs.org/d3-geo/-/d3-geo-3.1.1.tgz"
+ integrity sha512-637ln3gXKXOwhalDzinUgY83KzNWZRKbYubaG+fGVuc/dxO64RRljtCTnf5ecMyE1RIdtqpkVcq0IbtU2S8j2Q==
+ dependencies:
+ d3-array "2.5.0 - 3"
+
+d3-hierarchy@3:
+ version "3.1.2"
+ resolved "https://registry.npmjs.org/d3-hierarchy/-/d3-hierarchy-3.1.2.tgz"
+ integrity sha512-FX/9frcub54beBdugHjDCdikxThEqjnR93Qt7PvQTOHxyiNCAlvMrHhclk3cD5VeAaq9fxmfRp+CnWw9rEMBuA==
+
+"d3-interpolate@1 - 3", "d3-interpolate@1.2.0 - 3", d3-interpolate@3:
+ version "3.0.1"
+ resolved "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz"
+ integrity sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==
+ dependencies:
+ d3-color "1 - 3"
+
+d3-path@^3.1.0, "d3-path@1 - 3", d3-path@3:
+ version "3.1.0"
+ resolved "https://registry.npmjs.org/d3-path/-/d3-path-3.1.0.tgz"
+ integrity sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==
+
+d3-path@1:
+ version "1.0.9"
+ resolved "https://registry.npmjs.org/d3-path/-/d3-path-1.0.9.tgz"
+ integrity sha512-VLaYcn81dtHVTjEHd8B+pbe9yHWpXKZUC87PzoFmsFrJqgFwDe/qxfp5MlfsfM1V5E/iVt0MmEbWQ7FVIXh/bg==
+
+d3-polygon@3:
+ version "3.0.1"
+ resolved "https://registry.npmjs.org/d3-polygon/-/d3-polygon-3.0.1.tgz"
+ integrity sha512-3vbA7vXYwfe1SYhED++fPUQlWSYTTGmFmQiany/gdbiWgU/iEyQzyymwL9SkJjFFuCS4902BSzewVGsHHmHtXg==
+
+"d3-quadtree@1 - 3", d3-quadtree@3:
+ version "3.0.1"
+ resolved "https://registry.npmjs.org/d3-quadtree/-/d3-quadtree-3.0.1.tgz"
+ integrity sha512-04xDrxQTDTCFwP5H6hRhsRcb9xxv2RzkcsygFzmkSIOJy3PeRJP7sNk3VRIbKXcog561P9oU0/rVH6vDROAgUw==
+
+d3-random@3:
+ version "3.0.1"
+ resolved "https://registry.npmjs.org/d3-random/-/d3-random-3.0.1.tgz"
+ integrity sha512-FXMe9GfxTxqd5D6jFsQ+DJ8BJS4E/fT5mqqdjovykEB2oFbTMDVdg1MGFxfQW+FBOGoB++k8swBrgwSHT1cUXQ==
+
+d3-sankey@^0.12.3:
+ version "0.12.3"
+ resolved "https://registry.npmjs.org/d3-sankey/-/d3-sankey-0.12.3.tgz"
+ integrity sha512-nQhsBRmM19Ax5xEIPLMY9ZmJ/cDvd1BG3UVvt5h3WRxKg5zGRbvnteTyWAbzeSvlh3tW7ZEmq4VwR5mB3tutmQ==
+ dependencies:
+ d3-array "1 - 2"
+ d3-shape "^1.2.0"
+
+d3-scale-chromatic@3:
+ version "3.1.0"
+ resolved "https://registry.npmjs.org/d3-scale-chromatic/-/d3-scale-chromatic-3.1.0.tgz"
+ integrity sha512-A3s5PWiZ9YCXFye1o246KoscMWqf8BsD9eRiJ3He7C9OBaxKhAd5TFCdEx/7VbKtxxTsu//1mMJFrEt572cEyQ==
+ dependencies:
+ d3-color "1 - 3"
+ d3-interpolate "1 - 3"
+
+d3-scale@4:
+ version "4.0.2"
+ resolved "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz"
+ integrity sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==
+ dependencies:
+ d3-array "2.10.0 - 3"
+ d3-format "1 - 3"
+ d3-interpolate "1.2.0 - 3"
+ d3-time "2.1.1 - 3"
+ d3-time-format "2 - 4"
+
+d3-selection@^3.0.0, "d3-selection@2 - 3", d3-selection@3:
+ version "3.0.0"
+ resolved "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz"
+ integrity sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==
+
+d3-shape@^1.2.0:
+ version "1.3.7"
+ resolved "https://registry.npmjs.org/d3-shape/-/d3-shape-1.3.7.tgz"
+ integrity sha512-EUkvKjqPFUAZyOlhY5gzCxCeI0Aep04LwIRpsZ/mLFelJiUfnK56jo5JMDSE7yyP2kLSb6LtF+S5chMk7uqPqw==
+ dependencies:
+ d3-path "1"
+
+d3-shape@3:
+ version "3.2.0"
+ resolved "https://registry.npmjs.org/d3-shape/-/d3-shape-3.2.0.tgz"
+ integrity sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==
+ dependencies:
+ d3-path "^3.1.0"
+
+"d3-time-format@2 - 4", d3-time-format@4:
+ version "4.1.0"
+ resolved "https://registry.npmjs.org/d3-time-format/-/d3-time-format-4.1.0.tgz"
+ integrity sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==
+ dependencies:
+ d3-time "1 - 3"
+
+"d3-time@1 - 3", "d3-time@2.1.1 - 3", d3-time@3:
+ version "3.1.0"
+ resolved "https://registry.npmjs.org/d3-time/-/d3-time-3.1.0.tgz"
+ integrity sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==
+ dependencies:
+ d3-array "2 - 3"
+
+"d3-timer@1 - 3", d3-timer@3:
+ version "3.0.1"
+ resolved "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz"
+ integrity sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==
+
+d3-transition@^3.0.1, "d3-transition@2 - 3", d3-transition@3:
+ version "3.0.1"
+ resolved "https://registry.npmjs.org/d3-transition/-/d3-transition-3.0.1.tgz"
+ integrity sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w==
+ dependencies:
+ d3-color "1 - 3"
+ d3-dispatch "1 - 3"
+ d3-ease "1 - 3"
+ d3-interpolate "1 - 3"
+ d3-timer "1 - 3"
+
+d3-zoom@3:
+ version "3.0.0"
+ resolved "https://registry.npmjs.org/d3-zoom/-/d3-zoom-3.0.0.tgz"
+ integrity sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw==
+ dependencies:
+ d3-dispatch "1 - 3"
+ d3-drag "2 - 3"
+ d3-interpolate "1 - 3"
+ d3-selection "2 - 3"
+ d3-transition "2 - 3"
+
+d3@^7.9.0:
+ version "7.9.0"
+ resolved "https://registry.npmjs.org/d3/-/d3-7.9.0.tgz"
+ integrity sha512-e1U46jVP+w7Iut8Jt8ri1YsPOvFpg46k+K8TpCb0P+zjCkjkPnV7WzfDJzMHy1LnA+wj5pLT1wjO901gLXeEhA==
+ dependencies:
+ d3-array "3"
+ d3-axis "3"
+ d3-brush "3"
+ d3-chord "3"
+ d3-color "3"
+ d3-contour "4"
+ d3-delaunay "6"
+ d3-dispatch "3"
+ d3-drag "3"
+ d3-dsv "3"
+ d3-ease "3"
+ d3-fetch "3"
+ d3-force "3"
+ d3-format "3"
+ d3-geo "3"
+ d3-hierarchy "3"
+ d3-interpolate "3"
+ d3-path "3"
+ d3-polygon "3"
+ d3-quadtree "3"
+ d3-random "3"
+ d3-scale "4"
+ d3-scale-chromatic "3"
+ d3-selection "3"
+ d3-shape "3"
+ d3-time "3"
+ d3-time-format "4"
+ d3-timer "3"
+ d3-transition "3"
+ d3-zoom "3"
+
+dagre-d3-es@7.0.14:
+ version "7.0.14"
+ resolved "https://registry.npmjs.org/dagre-d3-es/-/dagre-d3-es-7.0.14.tgz"
+ integrity sha512-P4rFMVq9ESWqmOgK+dlXvOtLwYg0i7u0HBGJER0LZDJT2VHIPAMZ/riPxqJceWMStH5+E61QxFra9kIS3AqdMg==
+ dependencies:
+ d3 "^7.9.0"
+ lodash-es "^4.17.21"
+
+data-uri-to-buffer@^6.0.2:
+ version "6.0.2"
+ resolved "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-6.0.2.tgz"
+ integrity sha512-7hvf7/GW8e86rW0ptuwS3OcBGDjIi6SZva7hCyWC0yYry2cOPmLIjXAUHI6DK2HsnwJd9ifmt57i8eV2n4YNpw==
+
+date-fns@^1.27.2:
+ version "1.30.1"
+ resolved "https://registry.npmjs.org/date-fns/-/date-fns-1.30.1.tgz"
+ integrity sha512-hBSVCvSmWC+QypYObzwGOd9wqdDpOt+0wl0KbU+R+uuZBS1jN8VsD1ss3irQDknRj5NvxiTF6oj/nDRnN/UQNw==
+
+dayjs@^1.11.19:
+ version "1.11.19"
+ resolved "https://registry.npmjs.org/dayjs/-/dayjs-1.11.19.tgz"
+ integrity sha512-t5EcLVS6QPBNqM2z8fakk/NKel+Xzshgt8FFKAn+qwlD1pzZWxh0nVCrvFK7ZDb6XucZeF9z8C7CBWTRIVApAw==
debug@^3.2.6:
version "3.2.7"
@@ -3741,6 +4945,13 @@ debug@^3.2.6:
dependencies:
ms "^2.1.1"
+debug@^4.1.1, debug@^4.3.4, debug@^4.3.5, debug@^4.4.0, debug@4:
+ version "4.4.3"
+ resolved "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz"
+ integrity sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==
+ dependencies:
+ ms "^2.1.3"
+
decamelize@^4.0.0:
version "4.0.0"
resolved "https://registry.npmjs.org/decamelize/-/decamelize-4.0.0.tgz"
@@ -3779,6 +4990,22 @@ define-data-property@^1.1.4:
es-errors "^1.3.0"
gopd "^1.0.1"
+degenerator@^5.0.0:
+ version "5.0.1"
+ resolved "https://registry.npmjs.org/degenerator/-/degenerator-5.0.1.tgz"
+ integrity sha512-TllpMR/t0M5sqCXfj85i4XaAzxmS5tVA16dqvdkMwGmzI+dXLXnw3J+3Vdv7VKw+ThlTMboK6i9rnZ6Nntj5CQ==
+ dependencies:
+ ast-types "^0.13.4"
+ escodegen "^2.1.0"
+ esprima "^4.0.1"
+
+delaunator@5:
+ version "5.0.1"
+ resolved "https://registry.npmjs.org/delaunator/-/delaunator-5.0.1.tgz"
+ integrity sha512-8nvh+XBe96aCESrGOqMp/84b13H9cdKbG5P2ejQCh4d4sK9RL4371qou9drQjMhvnPmhWl5hnmqbEE0fXr9Xnw==
+ dependencies:
+ robust-predicates "^3.0.2"
+
delayed-stream@~1.0.0:
version "1.0.0"
resolved "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz"
@@ -3789,6 +5016,16 @@ depd@2.0.0:
resolved "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz"
integrity sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==
+devtools-protocol@*, devtools-protocol@0.0.1367902:
+ version "0.0.1367902"
+ resolved "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1367902.tgz"
+ integrity sha512-XxtPuC3PGakY6PD7dG66/o8KwJ/LkH2/EKe19Dcw58w53dv4/vSQEkn/SzuyhHE2q4zPgCkxQBxus3VV4ql+Pg==
+
+didyoumean@^1.2.2:
+ version "1.2.2"
+ resolved "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz"
+ integrity sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==
+
diff@^4.0.1:
version "4.0.2"
resolved "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz"
@@ -3804,13 +5041,18 @@ diff@^7.0.0:
resolved "https://registry.npmjs.org/diff/-/diff-7.0.0.tgz"
integrity sha512-PJWHUb1RFevKCwaFA9RlG5tCd+FO5iRh9A8HEtkmBH2Li03iJriB6m6JIN4rGz3K3JLawI7/veA1xzRKP6ISBw==
+dlv@^1.1.3:
+ version "1.1.3"
+ resolved "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz"
+ integrity sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==
+
docker-modem@^1.0.8:
version "1.0.9"
resolved "https://registry.npmjs.org/docker-modem/-/docker-modem-1.0.9.tgz"
integrity sha512-lVjqCSCIAUDZPAZIeyM125HXfNvOmYYInciphNrLrylUtKyW66meAjSPXWchKVzoIYZx69TPnAepVSSkeawoIw==
dependencies:
- JSONStream "1.3.2"
debug "^3.2.6"
+ JSONStream "1.3.2"
readable-stream "~1.0.26-4"
split-ca "^1.0.0"
@@ -3846,6 +5088,13 @@ dockerode@^4.0.2:
tar-fs "~2.1.2"
uuid "^10.0.0"
+dompurify@^3.3.1:
+ version "3.3.2"
+ resolved "https://registry.npmjs.org/dompurify/-/dompurify-3.3.2.tgz"
+ integrity sha512-6obghkliLdmKa56xdbLOpUZ43pAR6xFy1uOrxBaIDjT+yaRuuybLjGS9eVBoSR/UPU5fq3OXClEHLJNGvbxKpQ==
+ optionalDependencies:
+ "@types/trusted-types" "^2.0.7"
+
dotenv@^17.2.1:
version "17.2.1"
resolved "https://registry.npmjs.org/dotenv/-/dotenv-17.2.1.tgz"
@@ -3865,7 +5114,12 @@ eastasianwidth@^0.2.0:
resolved "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz"
integrity sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==
-elliptic@6.6.1, elliptic@^6.5.7:
+elegant-spinner@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.npmjs.org/elegant-spinner/-/elegant-spinner-1.0.1.tgz"
+ integrity sha512-B+ZM+RXvRqQaAmkMlO/oSe5nMUOaUnyfGYCEHoR8wrXsZR2mA0XVibsxV1bvTwxdRWah1PkQqso2EzhILGHtEQ==
+
+elliptic@^6.5.7, elliptic@6.6.1:
version "6.6.1"
resolved "https://registry.npmjs.org/elliptic/-/elliptic-6.6.1.tgz"
integrity sha512-RaddvvMatK2LJHqFJ+YA4WysVN5Ita9E35botqIYspQ4TkRAlCicdzKOjlyv/1Za5RyTNn7di//eEV0uTAfe3g==
@@ -3952,16 +5206,47 @@ escalade@^3.1.1:
resolved "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz"
integrity sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==
+escape-string-regexp@^1.0.2:
+ version "1.0.5"
+ resolved "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz"
+ integrity sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==
+
+escape-string-regexp@^1.0.5:
+ version "1.0.5"
+ resolved "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz"
+ integrity sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==
+
escape-string-regexp@^4.0.0:
version "4.0.0"
resolved "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz"
integrity sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==
-esprima@^4.0.1:
+escodegen@^2.1.0:
+ version "2.1.0"
+ resolved "https://registry.npmjs.org/escodegen/-/escodegen-2.1.0.tgz"
+ integrity sha512-2NlIDTwUWJN0mRPQOdtQBzbUHvdGY2P1VXSyU83Q3xKxM7WHX2Ql8dKq782Q9TgQUNOLEzEYu9bzLNj1q88I5w==
+ dependencies:
+ esprima "^4.0.1"
+ estraverse "^5.2.0"
+ esutils "^2.0.2"
+ optionalDependencies:
+ source-map "~0.6.1"
+
+esprima@^4.0.0, esprima@^4.0.1:
version "4.0.1"
resolved "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz"
integrity sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==
+estraverse@^5.2.0:
+ version "5.3.0"
+ resolved "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz"
+ integrity sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==
+
+esutils@^2.0.2:
+ version "2.0.3"
+ resolved "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz"
+ integrity sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==
+
ethereum-cryptography@^0.1.3:
version "0.1.3"
resolved "https://registry.npmjs.org/ethereum-cryptography/-/ethereum-cryptography-0.1.3.tgz"
@@ -4014,7 +5299,7 @@ ethereumjs-util@^7.0.3, ethereumjs-util@^7.1.5:
ethereum-cryptography "^0.1.3"
rlp "^2.2.4"
-ethers@^6.12.2, ethers@^6.15.0:
+ethers@^6.12.2, ethers@^6.14.0, ethers@^6.15.0, ethers@^6.6.0, ethers@^6.7.1:
version "6.15.0"
resolved "https://registry.npmjs.org/ethers/-/ethers-6.15.0.tgz"
integrity sha512-Kf/3ZW54L4UT0pZtsY/rf+EkBU7Qi5nnhonjUb8yTXcxH3cdcWrV2cRyk0Xk/4jK6OoHhxxZHriyhje20If2hQ==
@@ -4027,6 +5312,23 @@ ethers@^6.12.2, ethers@^6.15.0:
tslib "2.7.0"
ws "8.17.1"
+event-target-shim@^5.0.0:
+ version "5.0.1"
+ resolved "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz"
+ integrity sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==
+
+events-universal@^1.0.0:
+ version "1.0.1"
+ resolved "https://registry.npmjs.org/events-universal/-/events-universal-1.0.1.tgz"
+ integrity sha512-LUd5euvbMLpwOF8m6ivPCbhQeSiYVNb8Vs0fQ8QjXo0JTkEHpz8pxdQf0gStltaPpw0Cca8b39KxvK9cfKRiAw==
+ dependencies:
+ bare-events "^2.7.0"
+
+events@^3.3.0:
+ version "3.3.0"
+ resolved "https://registry.npmjs.org/events/-/events-3.3.0.tgz"
+ integrity sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==
+
evp_bytestokey@^1.0.3:
version "1.0.3"
resolved "https://registry.npmjs.org/evp_bytestokey/-/evp_bytestokey-1.0.3.tgz"
@@ -4035,6 +5337,24 @@ evp_bytestokey@^1.0.3:
md5.js "^1.3.4"
safe-buffer "^5.1.1"
+extend-shallow@^2.0.1:
+ version "2.0.1"
+ resolved "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz"
+ integrity sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==
+ dependencies:
+ is-extendable "^0.1.0"
+
+extract-zip@^2.0.1:
+ version "2.0.1"
+ resolved "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz"
+ integrity sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==
+ dependencies:
+ debug "^4.1.1"
+ get-stream "^5.1.0"
+ yauzl "^2.10.0"
+ optionalDependencies:
+ "@types/yauzl" "^2.9.1"
+
fast-base64-decode@^1.0.0:
version "1.0.0"
resolved "https://registry.npmjs.org/fast-base64-decode/-/fast-base64-decode-1.0.0.tgz"
@@ -4055,6 +5375,11 @@ fast-equals@^5.2.2:
resolved "https://registry.npmjs.org/fast-equals/-/fast-equals-5.2.2.tgz"
integrity sha512-V7/RktU11J3I36Nwq2JnZEM7tNm17eBJz+u25qdxBZeCKiX6BkVSZQjwWIr+IobgnZy+ag73tTZgZi7tr0LrBw==
+fast-fifo@^1.2.0, fast-fifo@^1.3.2:
+ version "1.3.2"
+ resolved "https://registry.npmjs.org/fast-fifo/-/fast-fifo-1.3.2.tgz"
+ integrity sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==
+
fast-glob@^3.3.2:
version "3.3.2"
resolved "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz"
@@ -4071,6 +5396,11 @@ fast-json-stable-stringify@^2.0.0, fast-json-stable-stringify@^2.1.0:
resolved "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz"
integrity sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==
+fast-redact@^3.1.1:
+ version "3.5.0"
+ resolved "https://registry.npmjs.org/fast-redact/-/fast-redact-3.5.0.tgz"
+ integrity sha512-dwsoQlS7h9hMeYUq1W++23NDcBLV4KqONnITDV9DjfS3q1SgDGVrBdvvTLUotWtPSD7asWDV9/CmsZPy8Hf70A==
+
fast-uri@^3.0.1:
version "3.0.3"
resolved "https://registry.npmjs.org/fast-uri/-/fast-uri-3.0.3.tgz"
@@ -4090,6 +5420,13 @@ fastq@^1.6.0:
dependencies:
reusify "^1.0.4"
+fd-slicer@~1.1.0:
+ version "1.1.0"
+ resolved "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz"
+ integrity sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==
+ dependencies:
+ pend "~1.2.0"
+
fdir@^6.4.2:
version "6.4.2"
resolved "https://registry.npmjs.org/fdir/-/fdir-6.4.2.tgz"
@@ -4100,6 +5437,21 @@ fdir@^6.4.4:
resolved "https://registry.npmjs.org/fdir/-/fdir-6.4.6.tgz"
integrity sha512-hiFoqpyZcfNm1yc4u8oWCf9A2c4D3QjCrks3zmoVKVxpQRzmPNar1hUJcBG2RQHvEVGDN+Jm81ZheVLAQMK6+w==
+figures@^1.7.0:
+ version "1.7.0"
+ resolved "https://registry.npmjs.org/figures/-/figures-1.7.0.tgz"
+ integrity sha512-UxKlfCRuCBxSXU4C6t9scbDyWZ4VlaFFdojKtzJuSkuOBQ5CNFum+zZXFwHjo+CxBC1t6zlYPgHIgFjL8ggoEQ==
+ dependencies:
+ escape-string-regexp "^1.0.5"
+ object-assign "^4.1.0"
+
+figures@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.npmjs.org/figures/-/figures-2.0.0.tgz"
+ integrity sha512-Oa2M9atig69ZkfwiApY8F2Yy+tzMbazyvqv21R0NsSC8floSOC09BbT1ITWAdoMGQvJ/aZnR1KMwdx9tvHnTNA==
+ dependencies:
+ escape-string-regexp "^1.0.5"
+
fill-keys@^1.0.2:
version "1.0.2"
resolved "https://registry.npmjs.org/fill-keys/-/fill-keys-1.0.2.tgz"
@@ -4174,16 +5526,16 @@ forwarded-parse@2.1.2:
resolved "https://registry.npmjs.org/forwarded-parse/-/forwarded-parse-2.1.2.tgz"
integrity sha512-alTFZZQDKMporBH77856pXgzhEzaUVmLCDk+egLgIgHst3Tpndzz8MnKe+GzRJRfvVdn69HhpW7cmXzvtLvJAw==
-fp-ts@1.19.3:
- version "1.19.3"
- resolved "https://registry.npmjs.org/fp-ts/-/fp-ts-1.19.3.tgz"
- integrity sha512-H5KQDspykdHuztLTg+ajGN0Z2qUjcEf3Ybxc6hLt0k7/zPkn29XnKnxlBPyW2XIddWrGaJBzBl4VLYOtk39yZg==
-
fp-ts@^1.0.0:
version "1.19.5"
resolved "https://registry.npmjs.org/fp-ts/-/fp-ts-1.19.5.tgz"
integrity sha512-wDNqTimnzs8QqpldiId9OavWK2NptormjXnRJTQecNjzwfyp6P/8s/zG8e4h3ja3oqkKaY72UlTjQYt/1yXf9A==
+fp-ts@1.19.3:
+ version "1.19.3"
+ resolved "https://registry.npmjs.org/fp-ts/-/fp-ts-1.19.3.tgz"
+ integrity sha512-H5KQDspykdHuztLTg+ajGN0Z2qUjcEf3Ybxc6hLt0k7/zPkn29XnKnxlBPyW2XIddWrGaJBzBl4VLYOtk39yZg==
+
fs-constants@^1.0.0:
version "1.0.0"
resolved "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz"
@@ -4262,6 +5614,11 @@ get-intrinsic@^1.2.4, get-intrinsic@^1.2.6, get-intrinsic@^1.3.0:
hasown "^2.0.2"
math-intrinsics "^1.1.0"
+get-port@^5.1.1:
+ version "5.1.1"
+ resolved "https://registry.npmjs.org/get-port/-/get-port-5.1.1.tgz"
+ integrity sha512-g/Q1aTSDOxFpchXC4i8ZWvxA1lnPqx/JHqcpIw0/LX9T8x/GBbi6YnlN5nhaKIFkT8oFsscUKgDJYxfwfS6QsQ==
+
get-proto@^1.0.1:
version "1.0.1"
resolved "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz"
@@ -4270,11 +5627,32 @@ get-proto@^1.0.1:
dunder-proto "^1.0.1"
es-object-atoms "^1.0.0"
+get-stdin@^8.0.0:
+ version "8.0.0"
+ resolved "https://registry.npmjs.org/get-stdin/-/get-stdin-8.0.0.tgz"
+ integrity sha512-sY22aA6xchAzprjyqmSEQv4UbAAzRN0L2dQB0NlN5acTTK9Don6nhoc3eAbUnpZiCANAMfd/+40kVdKfFygohg==
+
+get-stream@^5.1.0:
+ version "5.2.0"
+ resolved "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz"
+ integrity sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==
+ dependencies:
+ pump "^3.0.0"
+
get-stream@^6.0.1:
version "6.0.1"
resolved "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz"
integrity sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==
+get-uri@^6.0.1:
+ version "6.0.5"
+ resolved "https://registry.npmjs.org/get-uri/-/get-uri-6.0.5.tgz"
+ integrity sha512-b1O07XYq8eRuVzBNgJLstU6FYc1tS6wnMtF1I1D9lE8LxZSOGZ7LhxN54yPP6mGw5f2CkXY2BQUL9Fx41qvcIg==
+ dependencies:
+ basic-ftp "^5.0.2"
+ data-uri-to-buffer "^6.0.2"
+ debug "^4.3.4"
+
glob-parent@^5.1.2, glob-parent@~5.1.2:
version "5.1.2"
resolved "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz"
@@ -4282,6 +5660,13 @@ glob-parent@^5.1.2, glob-parent@~5.1.2:
dependencies:
is-glob "^4.0.1"
+glob-parent@^6.0.2:
+ version "6.0.2"
+ resolved "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz"
+ integrity sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==
+ dependencies:
+ is-glob "^4.0.3"
+
glob@^10.4.1, glob@^10.4.5:
version "10.4.5"
resolved "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz"
@@ -4294,7 +5679,18 @@ glob@^10.4.1, glob@^10.4.5:
package-json-from-dist "^1.0.0"
path-scurry "^1.11.1"
-glob@^8.0.3, glob@^8.1.0:
+glob@^8.0.3:
+ version "8.1.0"
+ resolved "https://registry.npmjs.org/glob/-/glob-8.1.0.tgz"
+ integrity sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==
+ dependencies:
+ fs.realpath "^1.0.0"
+ inflight "^1.0.4"
+ inherits "2"
+ minimatch "^5.0.1"
+ once "^1.3.0"
+
+glob@^8.1.0:
version "8.1.0"
resolved "https://registry.npmjs.org/glob/-/glob-8.1.0.tgz"
integrity sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==
@@ -4334,15 +5730,30 @@ got@^12.1.0:
p-cancelable "^3.0.0"
responselike "^3.0.0"
+graceful-fs@^4.1.2, graceful-fs@^4.1.6, graceful-fs@^4.2.0, graceful-fs@^4.2.4:
+ version "4.2.11"
+ resolved "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz"
+ integrity sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==
+
graceful-fs@4.2.10:
version "4.2.10"
resolved "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.10.tgz"
integrity sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA==
-graceful-fs@^4.1.2, graceful-fs@^4.1.6, graceful-fs@^4.2.0, graceful-fs@^4.2.4:
- version "4.2.11"
- resolved "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz"
- integrity sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==
+gray-matter@^4.0.3:
+ version "4.0.3"
+ resolved "https://registry.npmjs.org/gray-matter/-/gray-matter-4.0.3.tgz"
+ integrity sha512-5v6yZd4JK3eMI3FqqCouswVqwugaA9r4dNZB1wwcmrD02QkV5H0y7XBQW8QwQqEaZY1pM9aqORSORhJRdNK44Q==
+ dependencies:
+ js-yaml "^3.13.1"
+ kind-of "^6.0.2"
+ section-matter "^1.0.0"
+ strip-bom-string "^1.0.0"
+
+hachure-fill@^0.5.2:
+ version "0.5.2"
+ resolved "https://registry.npmjs.org/hachure-fill/-/hachure-fill-0.5.2.tgz"
+ integrity sha512-3GKBOn+m2LX9iq+JC1064cSFprJY4jL1jCXTcpnfER5HYE2l/4EfWSGzkPa/ZDBmYI0ZOEj5VHV/eKnPGkHuOg==
hardhat-storage-layout@^0.1.7:
version "0.1.7"
@@ -4351,9 +5762,9 @@ hardhat-storage-layout@^0.1.7:
dependencies:
console-table-printer "^2.9.0"
-hardhat@^2.22.5, hardhat@^2.26.3:
+hardhat@^2.0.3, hardhat@^2.22.5, hardhat@^2.24.1, hardhat@^2.26.0, hardhat@^2.26.3:
version "2.26.3"
- resolved "https://registry.yarnpkg.com/hardhat/-/hardhat-2.26.3.tgz#87f3f4b6d1001970299d5bff135d57e8adae7a07"
+ resolved "https://registry.npmjs.org/hardhat/-/hardhat-2.26.3.tgz"
integrity sha512-gBfjbxCCEaRgMCRgTpjo1CEoJwqNPhyGMMVHYZJxoQ3LLftp2erSVf8ZF6hTQC0r2wst4NcqNmLWqMnHg1quTw==
dependencies:
"@ethereumjs/util" "^9.1.0"
@@ -4396,6 +5807,18 @@ hardhat@^2.22.5, hardhat@^2.26.3:
uuid "^8.3.2"
ws "^7.4.6"
+has-ansi@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.npmjs.org/has-ansi/-/has-ansi-2.0.0.tgz"
+ integrity sha512-C8vBJ8DwUCx19vhm7urhTuUsr4/IyP6l4VzNQDv+ryHQObW3TTTp9yB68WpYgRe2bbaGuZ/se74IqFeVnMnLZg==
+ dependencies:
+ ansi-regex "^2.0.0"
+
+has-flag@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz"
+ integrity sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==
+
has-flag@^4.0.0:
version "4.0.0"
resolved "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz"
@@ -4441,7 +5864,7 @@ hash-base@^3.0.0:
readable-stream "^3.6.0"
safe-buffer "^5.2.0"
-hash.js@1.1.7, hash.js@^1.0.0, hash.js@^1.0.3, hash.js@^1.1.7:
+hash.js@^1.0.0, hash.js@^1.0.3, hash.js@^1.1.7, hash.js@1.1.7:
version "1.1.7"
resolved "https://registry.npmjs.org/hash.js/-/hash.js-1.1.7.tgz"
integrity sha512-taOaskGt4z4SOANNseOviYDvjEJinIkRgmp7LbKP2YTTmVxWBl87s/uzK9r+44BclBSp2X7K1hqeNfz9JbBeXA==
@@ -4461,6 +5884,16 @@ he@^1.2.0:
resolved "https://registry.npmjs.org/he/-/he-1.2.0.tgz"
integrity sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==
+highlight.js@^10.7.3:
+ version "10.7.3"
+ resolved "https://registry.npmjs.org/highlight.js/-/highlight.js-10.7.3.tgz"
+ integrity sha512-tzcUFauisWKNHaRkN4Wjl/ZA07gENAjFl3J/c480dprkGTg5EQstgaNFqBfUqCq54kZRIEcreTsAgF/m2quD7A==
+
+highlight.js@^11.7.0:
+ version "11.11.1"
+ resolved "https://registry.npmjs.org/highlight.js/-/highlight.js-11.11.1.tgz"
+ integrity sha512-Xwwo44whKBVCYoliBQwaPvtd/2tYFkRQtXDWj1nackaV2JPXx3L0+Jvd8/qCJ2p+ML0/XVkJ2q+Mr+UVdpJK5w==
+
hmac-drbg@^1.0.1:
version "1.0.1"
resolved "https://registry.npmjs.org/hmac-drbg/-/hmac-drbg-1.0.1.tgz"
@@ -4470,6 +5903,11 @@ hmac-drbg@^1.0.1:
minimalistic-assert "^1.0.0"
minimalistic-crypto-utils "^1.0.1"
+html-to-image@^1.11.13:
+ version "1.11.13"
+ resolved "https://registry.npmjs.org/html-to-image/-/html-to-image-1.11.13.tgz"
+ integrity sha512-cuOPoI7WApyhBElTTb9oqsawRvZ0rHhaHwghRLlTuffoD1B2aDemlCruLeZrUIIdvG7gs9xeELEPm6PhuASqrg==
+
http-cache-semantics@^4.1.1:
version "4.1.1"
resolved "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.1.tgz"
@@ -4486,6 +5924,14 @@ http-errors@2.0.0:
statuses "2.0.1"
toidentifier "1.0.1"
+http-proxy-agent@^7.0.0, http-proxy-agent@^7.0.1:
+ version "7.0.2"
+ resolved "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz"
+ integrity sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==
+ dependencies:
+ agent-base "^7.1.0"
+ debug "^4.3.4"
+
http2-wrapper@^2.1.10:
version "2.2.1"
resolved "https://registry.npmjs.org/http2-wrapper/-/http2-wrapper-2.2.1.tgz"
@@ -4502,6 +5948,21 @@ https-proxy-agent@^5.0.0:
agent-base "6"
debug "4"
+https-proxy-agent@^7.0.6:
+ version "7.0.6"
+ resolved "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz"
+ integrity sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==
+ dependencies:
+ agent-base "^7.1.2"
+ debug "4"
+
+iconv-lite@^0.6.3:
+ version "0.6.3"
+ resolved "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz"
+ integrity sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==
+ dependencies:
+ safer-buffer ">= 2.1.2 < 3.0.0"
+
iconv-lite@0.4.24:
version "0.4.24"
resolved "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz"
@@ -4509,7 +5970,14 @@ iconv-lite@0.4.24:
dependencies:
safer-buffer ">= 2.1.2 < 3"
-ieee754@^1.1.13, ieee754@^1.1.4:
+iconv-lite@0.6:
+ version "0.6.3"
+ resolved "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz"
+ integrity sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==
+ dependencies:
+ safer-buffer ">= 2.1.2 < 3.0.0"
+
+ieee754@^1.1.13, ieee754@^1.1.4, ieee754@^1.2.1:
version "1.2.1"
resolved "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz"
integrity sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==
@@ -4519,6 +5987,11 @@ ignore@^5.2.4:
resolved "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz"
integrity sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==
+immer@^10.2.0:
+ version "10.2.0"
+ resolved "https://registry.npmjs.org/immer/-/immer-10.2.0.tgz"
+ integrity sha512-d/+XTN3zfODyjr89gM3mPq1WNX2B8pYsu7eORitdwyA2sBubnTl3laYlBk4sXY5FUa5qTZGBDPJICVbvqzjlbw==
+
immutable@^4.0.0-rc.12:
version "4.3.7"
resolved "https://registry.npmjs.org/immutable/-/immutable-4.3.7.tgz"
@@ -4555,6 +6028,11 @@ import-meta-resolve@^4.1.0:
resolved "https://registry.npmjs.org/import-meta-resolve/-/import-meta-resolve-4.1.0.tgz"
integrity sha512-I6fiaX09Xivtk+THaMfAwnA3MVA5Big1WHF1Dfx9hFuvNIWpXnorlkzhcQf6ehrqQiiZECRt1poOAkPmer3ruw==
+indent-string@^3.0.0:
+ version "3.2.0"
+ resolved "https://registry.npmjs.org/indent-string/-/indent-string-3.2.0.tgz"
+ integrity sha512-BYqTHXTGUIvg7t1r4sJNKcbDZkL92nkXA8YtRpbjFHRHGDL/NtUeiBJMeE60kIFN/Mg8ESaWQvftaYMGJzQZCQ==
+
indent-string@^4.0.0:
version "4.0.0"
resolved "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz"
@@ -4568,20 +6046,30 @@ inflight@^1.0.4:
once "^1.3.0"
wrappy "1"
-inherits@2, inherits@2.0.4, inherits@^2.0.1, inherits@^2.0.3, inherits@^2.0.4, inherits@~2.0.1, inherits@~2.0.3:
+inherits@^2.0.1, inherits@^2.0.3, inherits@^2.0.4, inherits@~2.0.1, inherits@~2.0.3, inherits@2, inherits@2.0.4:
version "2.0.4"
resolved "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz"
integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==
+ini@^1.3.4, ini@~1.3.0:
+ version "1.3.8"
+ resolved "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz"
+ integrity sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==
+
ini@4.1.1:
version "4.1.1"
resolved "https://registry.npmjs.org/ini/-/ini-4.1.1.tgz"
integrity sha512-QQnnxNyfvmHFIsj7gkPcYymR8Jdw/o7mp5ZFihxn6h8Ci6fh3Dx4E1gPjpQEpIuPo9XVNY/ZUwh4BPMjGyL01g==
-ini@^1.3.4, ini@~1.3.0:
- version "1.3.8"
- resolved "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz"
- integrity sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==
+internmap@^1.0.0:
+ version "1.0.1"
+ resolved "https://registry.npmjs.org/internmap/-/internmap-1.0.1.tgz"
+ integrity sha512-lDB5YccMydFBtasVtxnZ3MRBHuaoE8GKsppq+EchKL2U4nK/DmEpPHNH8MZe5HkMtpSiTSOZwfN0tzYjO/lJEw==
+
+"internmap@1 - 2":
+ version "2.0.3"
+ resolved "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz"
+ integrity sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==
io-ts@1.10.4:
version "1.10.4"
@@ -4590,6 +6078,11 @@ io-ts@1.10.4:
dependencies:
fp-ts "^1.0.0"
+ip-address@^10.0.1:
+ version "10.1.0"
+ resolved "https://registry.npmjs.org/ip-address/-/ip-address-10.1.0.tgz"
+ integrity sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q==
+
is-arrayish@^0.2.1:
version "0.2.1"
resolved "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz"
@@ -4607,31 +6100,41 @@ is-callable@^1.2.7:
resolved "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz"
integrity sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==
-is-core-module@^2.13.0:
- version "2.15.1"
- resolved "https://registry.npmjs.org/is-core-module/-/is-core-module-2.15.1.tgz"
- integrity sha512-z0vtXSwucUJtANQWldhbtbt7BnL0vxiFjIdDLAatwhDYty2bad6s+rijD6Ri4YuYJubLzIJLUidCh09e1djEVQ==
- dependencies:
- hasown "^2.0.2"
-
-is-core-module@^2.16.0:
+is-core-module@^2.13.0, is-core-module@^2.16.0, is-core-module@^2.16.1:
version "2.16.1"
resolved "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz"
integrity sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==
dependencies:
hasown "^2.0.2"
+is-extendable@^0.1.0:
+ version "0.1.1"
+ resolved "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz"
+ integrity sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==
+
is-extglob@^2.1.1:
version "2.1.1"
resolved "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz"
integrity sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==
+is-fullwidth-code-point@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz"
+ integrity sha512-1pqUqRjkhPJ9miNq9SwMfdvi6lBJcd6eFxvfaivQhaH3SgisfiuudvFntdKOmxuee/77l+FPjKrQjWvmPjWrRw==
+ dependencies:
+ number-is-nan "^1.0.0"
+
+is-fullwidth-code-point@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz"
+ integrity sha512-VHskAKYM8RfSFXwee5t5cbN5PZeq1Wrh6qd5bkyiXIf6UQcN6w/A0eXM9r6t8d+GYOh+o6ZhiEnb88LN/Y8m2w==
+
is-fullwidth-code-point@^3.0.0:
version "3.0.0"
resolved "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz"
integrity sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==
-is-glob@^4.0.1, is-glob@~4.0.1:
+is-glob@^4.0.1, is-glob@^4.0.3, is-glob@~4.0.1:
version "4.0.3"
resolved "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz"
integrity sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==
@@ -4648,11 +6151,28 @@ is-object@~1.0.1:
resolved "https://registry.npmjs.org/is-object/-/is-object-1.0.2.tgz"
integrity sha512-2rRIahhZr2UWb45fIOuvZGpFtz0TyOZLf32KxBbSoUCeZR495zCKlWUKKUByk3geS2eAs7ZAABt0Y/Rx0GiQGA==
+is-observable@^1.1.0:
+ version "1.1.0"
+ resolved "https://registry.npmjs.org/is-observable/-/is-observable-1.1.0.tgz"
+ integrity sha512-NqCa4Sa2d+u7BWc6CukaObG3Fh+CU9bvixbpcXYhy2VvYS7vVGIdAgnIS5Ks3A/cqk4rebLJ9s8zBstT2aKnIA==
+ dependencies:
+ symbol-observable "^1.1.0"
+
is-plain-obj@^2.1.0:
version "2.1.0"
resolved "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-2.1.0.tgz"
integrity sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==
+is-promise@^2.1.0:
+ version "2.2.2"
+ resolved "https://registry.npmjs.org/is-promise/-/is-promise-2.2.2.tgz"
+ integrity sha512-+lP4/6lKUBfQjZ2pdxThZvLUAafmZb8OAxFb8XXtiQmS35INgr85hdOGoEs124ez1FCnZJt6jau/T+alh58QFQ==
+
+is-stream@^1.1.0:
+ version "1.1.0"
+ resolved "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz"
+ integrity sha512-uQPm8kcs47jx38atAcWTVxyltQYoPT68y9aWYdV6yWXSyW8mzSat0TL6CiWdZeCdF3KrAvpVtnHbTv4RN+rqdQ==
+
is-typed-array@^1.1.14:
version "1.1.15"
resolved "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.15.tgz"
@@ -4665,11 +6185,6 @@ is-unicode-supported@^0.1.0:
resolved "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz"
integrity sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==
-isarray@0.0.1:
- version "0.0.1"
- resolved "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz"
- integrity sha512-D2S+3GLxWH+uhrNEcoh/fnmYeP8E8/zHl644d/jdA0g2uyXvy3sb0qxotE+ne0LtccHknQzWwZEzhak7oJ0COQ==
-
isarray@^1.0.0, isarray@~1.0.0:
version "1.0.0"
resolved "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz"
@@ -4680,6 +6195,11 @@ isarray@^2.0.5:
resolved "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz"
integrity sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==
+isarray@0.0.1:
+ version "0.0.1"
+ resolved "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz"
+ integrity sha512-D2S+3GLxWH+uhrNEcoh/fnmYeP8E8/zHl644d/jdA0g2uyXvy3sb0qxotE+ne0LtccHknQzWwZEzhak7oJ0COQ==
+
isexe@^2.0.0:
version "2.0.0"
resolved "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz"
@@ -4702,6 +6222,16 @@ jackspeak@^3.1.2:
optionalDependencies:
"@pkgjs/parseargs" "^0.11.0"
+jiti@^1.21.7, jiti@>=1.21.0:
+ version "1.21.7"
+ resolved "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz"
+ integrity sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==
+
+jotai@^2.16.1:
+ version "2.18.1"
+ resolved "https://registry.npmjs.org/jotai/-/jotai-2.18.1.tgz"
+ integrity sha512-e0NOzK+yRFwHo7DOp0DS0Ycq74KMEAObDWFGmfEL28PD9nLqBTt3/Ug7jf9ca72x0gC9LQZG9zH+0ISICmy3iA==
+
js-cookie@^2.2.1:
version "2.2.1"
resolved "https://registry.npmjs.org/js-cookie/-/js-cookie-2.2.1.tgz"
@@ -4717,6 +6247,14 @@ js-tokens@^4.0.0:
resolved "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz"
integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==
+js-yaml@^3.13.1:
+ version "3.14.2"
+ resolved "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.2.tgz"
+ integrity sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==
+ dependencies:
+ argparse "^1.0.7"
+ esprima "^4.0.0"
+
js-yaml@^4.1.0:
version "4.1.0"
resolved "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz"
@@ -4775,11 +6313,26 @@ jsonpointer@^5.0.1:
resolved "https://registry.npmjs.org/jsonpointer/-/jsonpointer-5.0.1.tgz"
integrity sha512-p/nXbhSEcu3pZRdkW1OfJhpsVtW1gd4Wa1fnQc9YLiTfAjn0312eMKimbdIQzuZl9aa9xUGaRlP9T/CJE/ditQ==
+JSONStream@1.3.2:
+ version "1.3.2"
+ resolved "https://registry.npmjs.org/JSONStream/-/JSONStream-1.3.2.tgz"
+ integrity sha512-mn0KSip7N4e0UDPZHnqDsHECo5uGQrixQKnAskOM1BIB8hd7QKbd6il8IPRPudPHOeHiECoCFqhyMaRO9+nWyA==
+ dependencies:
+ jsonparse "^1.2.0"
+ through ">=2.2.7 <3"
+
just-extend@^6.2.0:
version "6.2.0"
resolved "https://registry.npmjs.org/just-extend/-/just-extend-6.2.0.tgz"
integrity sha512-cYofQu2Xpom82S6qD778jBDpwvvy39s1l/hrYij2u9AMdQcGRpaBu6kY4mVhuno5kJVi1DAz4aiphA2WI1/OAw==
+katex@^0.16.25:
+ version "0.16.38"
+ resolved "https://registry.npmjs.org/katex/-/katex-0.16.38.tgz"
+ integrity sha512-cjHooZUmIAUmDsHBN+1n8LaZdpmbj03LtYeYPyuYB7OuloiaeaV6N4LcfjcnHVzGWjVQmKrxxTrpDcmSzEZQwQ==
+ dependencies:
+ commander "^8.3.0"
+
keccak@^3.0.0, keccak@^3.0.2:
version "3.0.4"
resolved "https://registry.npmjs.org/keccak/-/keccak-3.0.4.tgz"
@@ -4796,6 +6349,27 @@ keyv@^4.5.3:
dependencies:
json-buffer "3.0.1"
+khroma@^2.1.0:
+ version "2.1.0"
+ resolved "https://registry.npmjs.org/khroma/-/khroma-2.1.0.tgz"
+ integrity sha512-Ls993zuzfayK269Svk9hzpeGUKob/sIgZzyHYdjQoAdQetRKpOLj+k/QQQ/6Qi0Yz65mlROrfd+Ev+1+7dz9Kw==
+
+kind-of@^6.0.0, kind-of@^6.0.2:
+ version "6.0.3"
+ resolved "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz"
+ integrity sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==
+
+langium@^4.0.0:
+ version "4.2.1"
+ resolved "https://registry.npmjs.org/langium/-/langium-4.2.1.tgz"
+ integrity sha512-zu9QWmjpzJcomzdJQAHgDVhLGq5bLosVak1KVa40NzQHXfqr4eAHupvnPOVXEoLkg6Ocefvf/93d//SB7du4YQ==
+ dependencies:
+ chevrotain "~11.1.1"
+ chevrotain-allstar "~0.3.1"
+ vscode-languageserver "~9.0.1"
+ vscode-languageserver-textdocument "~1.0.11"
+ vscode-uri "~3.1.0"
+
latest-version@^7.0.0:
version "7.0.0"
resolved "https://registry.npmjs.org/latest-version/-/latest-version-7.0.0.tgz"
@@ -4803,16 +6377,75 @@ latest-version@^7.0.0:
dependencies:
package-json "^8.1.0"
+layout-base@^1.0.0:
+ version "1.0.2"
+ resolved "https://registry.npmjs.org/layout-base/-/layout-base-1.0.2.tgz"
+ integrity sha512-8h2oVEZNktL4BH2JCOI90iD1yXwL6iNW7KcCKT2QZgQJR2vbqDsldCTPRU9NifTCqHZci57XvQQ15YTu+sTYPg==
+
+layout-base@^2.0.0:
+ version "2.0.1"
+ resolved "https://registry.npmjs.org/layout-base/-/layout-base-2.0.1.tgz"
+ integrity sha512-dp3s92+uNI1hWIpPGH3jK2kxE2lMjdXdr+DH8ynZHpd6PUlH6x6cbuXnoMmiNumznqaNO31xu9e79F0uuZ0JFg==
+
"leven@^3.1.0 < 4":
version "3.1.0"
resolved "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz"
integrity sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==
+lilconfig@^3.1.1, lilconfig@^3.1.3:
+ version "3.1.3"
+ resolved "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz"
+ integrity sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==
+
lines-and-columns@^1.1.6:
version "1.2.4"
resolved "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz"
integrity sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==
+listr-silent-renderer@^1.1.1:
+ version "1.1.1"
+ resolved "https://registry.npmjs.org/listr-silent-renderer/-/listr-silent-renderer-1.1.1.tgz"
+ integrity sha512-L26cIFm7/oZeSNVhWB6faeorXhMg4HNlb/dS/7jHhr708jxlXrtrBWo4YUxZQkc6dGoxEAe6J/D3juTRBUzjtA==
+
+listr-update-renderer@^0.5.0:
+ version "0.5.0"
+ resolved "https://registry.npmjs.org/listr-update-renderer/-/listr-update-renderer-0.5.0.tgz"
+ integrity sha512-tKRsZpKz8GSGqoI/+caPmfrypiaq+OQCbd+CovEC24uk1h952lVj5sC7SqyFUm+OaJ5HN/a1YLt5cit2FMNsFA==
+ dependencies:
+ chalk "^1.1.3"
+ cli-truncate "^0.2.1"
+ elegant-spinner "^1.0.1"
+ figures "^1.7.0"
+ indent-string "^3.0.0"
+ log-symbols "^1.0.2"
+ log-update "^2.3.0"
+ strip-ansi "^3.0.1"
+
+listr-verbose-renderer@^0.5.0:
+ version "0.5.0"
+ resolved "https://registry.npmjs.org/listr-verbose-renderer/-/listr-verbose-renderer-0.5.0.tgz"
+ integrity sha512-04PDPqSlsqIOaaaGZ+41vq5FejI9auqTInicFRndCBgE3bXG8D6W1I+mWhk+1nqbHmyhla/6BUrd5OSiHwKRXw==
+ dependencies:
+ chalk "^2.4.1"
+ cli-cursor "^2.1.0"
+ date-fns "^1.27.2"
+ figures "^2.0.0"
+
+listr@^0.14.2, listr@^0.14.3:
+ version "0.14.3"
+ resolved "https://registry.npmjs.org/listr/-/listr-0.14.3.tgz"
+ integrity sha512-RmAl7su35BFd/xoMamRjpIE4j3v+L28o8CT5YhAXQJm1fD+1l9ngXY8JAQRJ+tFK2i5njvi0iRUKV09vPwA0iA==
+ dependencies:
+ "@samverschueren/stream-to-observable" "^0.3.0"
+ is-observable "^1.1.0"
+ is-promise "^2.1.0"
+ is-stream "^1.1.0"
+ listr-silent-renderer "^1.1.1"
+ listr-update-renderer "^0.5.0"
+ listr-verbose-renderer "^0.5.0"
+ p-map "^2.0.0"
+ rxjs "^6.3.3"
+
locate-path@^6.0.0:
version "6.0.0"
resolved "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz"
@@ -4820,6 +6453,11 @@ locate-path@^6.0.0:
dependencies:
p-locate "^5.0.0"
+lodash-es@^4.17.21, lodash-es@^4.17.23, lodash-es@4.17.23:
+ version "4.17.23"
+ resolved "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.23.tgz"
+ integrity sha512-kVI48u3PZr38HdYz98UmfPnXl2DXrpdctLrFLCd3kOx1xUkOmpFPx7gCWWM5MPkL/fD8zb+Ph0QzjGFs4+hHWg==
+
lodash.camelcase@^4.3.0:
version "4.3.0"
resolved "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz"
@@ -4850,6 +6488,13 @@ lodash@^4.17.11, lodash@^4.17.21:
resolved "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz"
integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==
+log-symbols@^1.0.2:
+ version "1.0.2"
+ resolved "https://registry.npmjs.org/log-symbols/-/log-symbols-1.0.2.tgz"
+ integrity sha512-mmPrW0Fh2fxOzdBbFv4g1m6pR72haFLPJ2G5SJEELf1y+iaQrDG6cWCPjy54RHYbZAt7X+ls690Kw62AdWXBzQ==
+ dependencies:
+ chalk "^1.0.0"
+
log-symbols@^4.1.0:
version "4.1.0"
resolved "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz"
@@ -4858,6 +6503,15 @@ log-symbols@^4.1.0:
chalk "^4.1.0"
is-unicode-supported "^0.1.0"
+log-update@^2.3.0:
+ version "2.3.0"
+ resolved "https://registry.npmjs.org/log-update/-/log-update-2.3.0.tgz"
+ integrity sha512-vlP11XfFGyeNQlmEn9tJ66rEW1coA/79m5z6BCkudjbAGE83uhAcGYrBFwfs3AdLiLzGRusRPAbSPK9xZteCmg==
+ dependencies:
+ ansi-escapes "^3.0.0"
+ cli-cursor "^2.0.0"
+ wrap-ansi "^3.0.1"
+
long@^5.0.0:
version "5.3.2"
resolved "https://registry.npmjs.org/long/-/long-5.3.2.tgz"
@@ -4875,26 +6529,60 @@ lowercase-keys@^3.0.0:
resolved "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-3.0.0.tgz"
integrity sha512-ozCC6gdQ+glXOQsveKD0YsDy8DSQFjDTz4zyzEHNV5+JP5D62LmfDZ6o1cycFx9ouG940M5dE8C8CTewdj2YWQ==
+lru_map@^0.3.3:
+ version "0.3.3"
+ resolved "https://registry.npmjs.org/lru_map/-/lru_map-0.3.3.tgz"
+ integrity sha512-Pn9cox5CsMYngeDbmChANltQl+5pi6XmTrraMSzhPmMBbmgcxmqWry0U3PGapCU1yB4/LqCcom7qhHZiF/jGfQ==
+
lru-cache@^10.2.0:
version "10.4.3"
resolved "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz"
integrity sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==
-lru_map@^0.3.3:
- version "0.3.3"
- resolved "https://registry.npmjs.org/lru_map/-/lru_map-0.3.3.tgz"
- integrity sha512-Pn9cox5CsMYngeDbmChANltQl+5pi6XmTrraMSzhPmMBbmgcxmqWry0U3PGapCU1yB4/LqCcom7qhHZiF/jGfQ==
+lru-cache@^7.14.1:
+ version "7.18.3"
+ resolved "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz"
+ integrity sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==
make-error@^1.1.1:
version "1.3.6"
resolved "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz"
integrity sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==
+marked@^16.3.0:
+ version "16.4.2"
+ resolved "https://registry.npmjs.org/marked/-/marked-16.4.2.tgz"
+ integrity sha512-TI3V8YYWvkVf3KJe1dRkpnjs68JUPyEa5vjKrp1XEEJUAOaQc+Qj+L1qWbPd0SJuAdQkFU0h73sXXqwDYxsiDA==
+
+marked@^4.2.12, marked@^4.3.0:
+ version "4.3.0"
+ resolved "https://registry.npmjs.org/marked/-/marked-4.3.0.tgz"
+ integrity sha512-PRsaiG84bK+AMvxziE/lCFss8juXjNaWzVbN5tXAm4XjeaS9NAHhop+PjQxz2A9h8Q4M/xGmzP8vqNwy6JeK0A==
+
math-intrinsics@^1.1.0:
version "1.1.0"
resolved "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz"
integrity sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==
+md-to-pdf@*:
+ version "5.2.5"
+ resolved "https://registry.npmjs.org/md-to-pdf/-/md-to-pdf-5.2.5.tgz"
+ integrity sha512-TG8TgDM0PmEwCldR6j/1QP9gBElLL3DSn5ID8P3bEXEl3Y2zHOUSyszHzabWnDNxklRjKbi40ybli8YQJ5Ym5w==
+ dependencies:
+ arg "^5.0.2"
+ chalk "^4.1.2"
+ chokidar "^3.5.2"
+ get-port "^5.1.1"
+ get-stdin "^8.0.0"
+ gray-matter "^4.0.3"
+ highlight.js "^11.7.0"
+ iconv-lite "^0.6.3"
+ listr "^0.14.3"
+ marked "^4.2.12"
+ puppeteer ">=8.0.0"
+ semver "^7.3.7"
+ serve-handler "^6.1.3"
+
md5.js@^1.3.4:
version "1.3.5"
resolved "https://registry.npmjs.org/md5.js/-/md5.js-1.3.5.tgz"
@@ -4919,6 +6607,33 @@ merge2@^1.3.0:
resolved "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz"
integrity sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==
+"mermaid@^10 || ^11", mermaid@^11.0.2:
+ version "11.13.0"
+ resolved "https://registry.npmjs.org/mermaid/-/mermaid-11.13.0.tgz"
+ integrity sha512-fEnci+Immw6lKMFI8sqzjlATTyjLkRa6axrEgLV2yHTfv8r+h1wjFbV6xeRtd4rUV1cS4EpR9rwp3Rci7TRWDw==
+ dependencies:
+ "@braintree/sanitize-url" "^7.1.1"
+ "@iconify/utils" "^3.0.2"
+ "@mermaid-js/parser" "^1.0.1"
+ "@types/d3" "^7.4.3"
+ "@upsetjs/venn.js" "^2.0.0"
+ cytoscape "^3.33.1"
+ cytoscape-cose-bilkent "^4.1.0"
+ cytoscape-fcose "^2.2.0"
+ d3 "^7.9.0"
+ d3-sankey "^0.12.3"
+ dagre-d3-es "7.0.14"
+ dayjs "^1.11.19"
+ dompurify "^3.3.1"
+ katex "^0.16.25"
+ khroma "^2.1.0"
+ lodash-es "^4.17.23"
+ marked "^16.3.0"
+ roughjs "^4.6.6"
+ stylis "^4.3.6"
+ ts-dedent "^2.2.0"
+ uuid "^11.1.0"
+
micro-eth-signer@^0.14.0:
version "0.14.0"
resolved "https://registry.npmjs.org/micro-eth-signer/-/micro-eth-signer-0.14.0.tgz"
@@ -4935,7 +6650,7 @@ micro-packed@~0.7.2:
dependencies:
"@scure/base" "~1.2.5"
-micromatch@^4.0.4:
+micromatch@^4.0.4, micromatch@^4.0.8:
version "4.0.8"
resolved "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz"
integrity sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==
@@ -4943,6 +6658,11 @@ micromatch@^4.0.4:
braces "^3.0.3"
picomatch "^2.3.1"
+mime-db@~1.33.0:
+ version "1.33.0"
+ resolved "https://registry.npmjs.org/mime-db/-/mime-db-1.33.0.tgz"
+ integrity sha512-BHJ/EKruNIqJf/QahvxwQZXKygOQ256myeN/Ew+THcAa5q+PjyTTMMeNQC4DZw5AwfvelsUrA6B67NKMqXDbzQ==
+
mime-db@1.52.0:
version "1.52.0"
resolved "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz"
@@ -4955,6 +6675,18 @@ mime-types@^2.1.12:
dependencies:
mime-db "1.52.0"
+mime-types@2.1.18:
+ version "2.1.18"
+ resolved "https://registry.npmjs.org/mime-types/-/mime-types-2.1.18.tgz"
+ integrity sha512-lc/aahn+t4/SWV/qcmumYjymLsWfN3ELhpmVuUFjgsORruuZPVSwAQryq+HHGvO/SI2KVX26bx+En+zhM8g8hQ==
+ dependencies:
+ mime-db "~1.33.0"
+
+mimic-fn@^1.0.0:
+ version "1.2.0"
+ resolved "https://registry.npmjs.org/mimic-fn/-/mimic-fn-1.2.0.tgz"
+ integrity sha512-jf84uxzwiuiIVKiOLpfYk7N46TSy8ubTonmneY9vrpHNAnp0QBt2BxWV9dO3/j+BoVAb+a5G6YDPW3M5HOdMWQ==
+
mimic-response@^3.1.0:
version "3.1.0"
resolved "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz"
@@ -4989,6 +6721,13 @@ minimatch@^9.0.3, minimatch@^9.0.4, minimatch@^9.0.5:
dependencies:
brace-expansion "^2.0.1"
+minimatch@3.1.5:
+ version "3.1.5"
+ resolved "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz"
+ integrity sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==
+ dependencies:
+ brace-expansion "^1.1.7"
+
minimist@^1.2.0, minimist@^1.2.6, minimist@^1.2.7:
version "1.2.8"
resolved "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz"
@@ -4999,6 +6738,11 @@ minimist@^1.2.0, minimist@^1.2.6, minimist@^1.2.7:
resolved "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz"
integrity sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==
+mitt@3.0.1:
+ version "3.0.1"
+ resolved "https://registry.npmjs.org/mitt/-/mitt-3.0.1.tgz"
+ integrity sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==
+
mkdirp-classic@^0.5.2:
version "0.5.3"
resolved "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz"
@@ -5016,6 +6760,16 @@ mkdirp@^3.0.1:
resolved "https://registry.npmjs.org/mkdirp/-/mkdirp-3.0.1.tgz"
integrity sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg==
+mlly@^1.7.4, mlly@^1.8.0:
+ version "1.8.1"
+ resolved "https://registry.npmjs.org/mlly/-/mlly-1.8.1.tgz"
+ integrity sha512-SnL6sNutTwRWWR/vcmCYHSADjiEesp5TGQQ0pXyLhW5IoeibRlF/CbSLailbB3CNqJUk9cVJ9dUDnbD7GrcHBQ==
+ dependencies:
+ acorn "^8.16.0"
+ pathe "^2.0.3"
+ pkg-types "^1.3.1"
+ ufo "^1.6.3"
+
mnemonist@^0.38.0:
version "0.38.5"
resolved "https://registry.npmjs.org/mnemonist/-/mnemonist-0.38.5.tgz"
@@ -5090,11 +6844,30 @@ ms@^2.1.1, ms@^2.1.3:
resolved "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz"
integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==
+mz@^2.7.0:
+ version "2.7.0"
+ resolved "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz"
+ integrity sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==
+ dependencies:
+ any-promise "^1.0.0"
+ object-assign "^4.0.1"
+ thenify-all "^1.0.0"
+
nan@^2.19.0, nan@^2.20.0:
version "2.23.0"
resolved "https://registry.npmjs.org/nan/-/nan-2.23.0.tgz"
integrity sha512-1UxuyYGdoQHcGg87Lkqm3FzefucTa0NAiOcuRsDmysep3c1LVCRK2krrUDafMWtjSG04htvAmvg96+SDknOmgQ==
+nanoid@^3.3.11:
+ version "3.3.11"
+ resolved "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz"
+ integrity sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==
+
+netmask@^2.0.2:
+ version "2.0.2"
+ resolved "https://registry.npmjs.org/netmask/-/netmask-2.0.2.tgz"
+ integrity sha512-dBpDMdxv9Irdq66304OLfEmQ9tbNRFnFTuZiLo+bD+r332bBmMJ8GBLXklIXXgxd3+v9+KUnZaUR5PJMa75Gsg==
+
nise@^6.0.0:
version "6.1.1"
resolved "https://registry.npmjs.org/nise/-/nise-6.1.1.tgz"
@@ -5143,11 +6916,31 @@ normalize-url@^8.0.0:
resolved "https://registry.npmjs.org/normalize-url/-/normalize-url-8.0.1.tgz"
integrity sha512-IO9QvjUMWxPQQhs60oOu10CRkWCiZzSUkzbXGGV9pviYl1fXYcvkzQ5jV9z8Y6un8ARoVRl4EtC6v6jNqbaJ/w==
+number-is-nan@^1.0.0:
+ version "1.0.1"
+ resolved "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz"
+ integrity sha512-4jbtZXNAsfZbAHiiqjLPBiCl16dES1zI4Hpzzxw61Tk+loF+sBDBKx1ICKKKwIqQ7M0mFn1TmkN7euSncWgHiQ==
+
+object-assign@^4.0.1, object-assign@^4.1.0:
+ version "4.1.1"
+ resolved "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz"
+ integrity sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==
+
+object-hash@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz"
+ integrity sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==
+
obliterator@^2.0.0:
version "2.0.4"
resolved "https://registry.npmjs.org/obliterator/-/obliterator-2.0.4.tgz"
integrity sha512-lgHwxlxV1qIg1Eap7LgIeoBWIMFibOjbrYPIPJZcI1mmGAI2m3lNYpK12Y+GBdPQ0U1hRwSord7GIaawz962qQ==
+on-exit-leak-free@^2.1.0:
+ version "2.1.2"
+ resolved "https://registry.npmjs.org/on-exit-leak-free/-/on-exit-leak-free-2.1.2.tgz"
+ integrity sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA==
+
once@^1.3.0, once@^1.3.1, once@^1.4.0:
version "1.4.0"
resolved "https://registry.npmjs.org/once/-/once-1.4.0.tgz"
@@ -5155,6 +6948,13 @@ once@^1.3.0, once@^1.3.1, once@^1.4.0:
dependencies:
wrappy "1"
+onetime@^2.0.0:
+ version "2.0.1"
+ resolved "https://registry.npmjs.org/onetime/-/onetime-2.0.1.tgz"
+ integrity sha512-oyyPpiMaKARvvcgip+JV+7zci5L8D1W9RZIz2l1o08AM3pfspitVWnPt3mzHcBPp12oYMTy0pqrFs/C+m3EwsQ==
+ dependencies:
+ mimic-fn "^1.0.0"
+
os-tmpdir@~1.0.2:
version "1.0.2"
resolved "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz"
@@ -5179,6 +6979,11 @@ p-locate@^5.0.0:
dependencies:
p-limit "^3.0.2"
+p-map@^2.0.0:
+ version "2.1.0"
+ resolved "https://registry.npmjs.org/p-map/-/p-map-2.1.0.tgz"
+ integrity sha512-y3b8Kpd8OAN444hxfBbFfj1FY/RjtTd8tzYwhUqNYXx0fXx2iX4maP4Qr6qhIKbQXI02wTLAda4fYUbDagTUFw==
+
p-map@^4.0.0:
version "4.0.0"
resolved "https://registry.npmjs.org/p-map/-/p-map-4.0.0.tgz"
@@ -5186,6 +6991,28 @@ p-map@^4.0.0:
dependencies:
aggregate-error "^3.0.0"
+pac-proxy-agent@^7.1.0:
+ version "7.2.0"
+ resolved "https://registry.npmjs.org/pac-proxy-agent/-/pac-proxy-agent-7.2.0.tgz"
+ integrity sha512-TEB8ESquiLMc0lV8vcd5Ql/JAKAoyzHFXaStwjkzpOpC5Yv+pIzLfHvjTSdf3vpa2bMiUQrg9i6276yn8666aA==
+ dependencies:
+ "@tootallnate/quickjs-emscripten" "^0.23.0"
+ agent-base "^7.1.2"
+ debug "^4.3.4"
+ get-uri "^6.0.1"
+ http-proxy-agent "^7.0.0"
+ https-proxy-agent "^7.0.6"
+ pac-resolver "^7.0.1"
+ socks-proxy-agent "^8.0.5"
+
+pac-resolver@^7.0.1:
+ version "7.0.1"
+ resolved "https://registry.npmjs.org/pac-resolver/-/pac-resolver-7.0.1.tgz"
+ integrity sha512-5NPgf87AT2STgwa2ntRMr45jTKrYBGkVU36yT0ig/n/GMAa3oPqhZfIQ2kMEimReg0+t9kZViDVZ83qfVUlckg==
+ dependencies:
+ degenerator "^5.0.0"
+ netmask "^2.0.2"
+
package-json-from-dist@^1.0.0:
version "1.0.1"
resolved "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz"
@@ -5201,6 +7028,16 @@ package-json@^8.1.0:
registry-url "^6.0.0"
semver "^7.3.7"
+package-manager-detector@^1.3.0:
+ version "1.6.0"
+ resolved "https://registry.npmjs.org/package-manager-detector/-/package-manager-detector-1.6.0.tgz"
+ integrity sha512-61A5ThoTiDG/C8s8UMZwSorAGwMJ0ERVGj2OjoW5pAalsNOg15+iQiPzrLJ4jhZ1HJzmC2PIHT2oEiH3R5fzNA==
+
+pako@^2.1.0:
+ version "2.1.0"
+ resolved "https://registry.npmjs.org/pako/-/pako-2.1.0.tgz"
+ integrity sha512-w+eufiZ1WuJYgPXbV/PO3NCMEc3xqylkKHzp8bxp1uW4qaSNQUkwmLLEc3kKsfz8lpV1F8Ht3U1Cm+9Srog2ug==
+
parent-module@^1.0.0:
version "1.0.1"
resolved "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz"
@@ -5230,11 +7067,21 @@ path-browserify@^1.0.1:
resolved "https://registry.npmjs.org/path-browserify/-/path-browserify-1.0.1.tgz"
integrity sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==
+path-data-parser@^0.1.0, path-data-parser@0.1.0:
+ version "0.1.0"
+ resolved "https://registry.npmjs.org/path-data-parser/-/path-data-parser-0.1.0.tgz"
+ integrity sha512-NOnmBpt5Y2RWbuv0LMzsayp3lVylAHLPUTut412ZA3l+C4uw4ZVkQbjShYCQ8TCpUMdPapr4YjUqLYD6v68j+w==
+
path-exists@^4.0.0:
version "4.0.0"
resolved "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz"
integrity sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==
+path-is-inside@1.0.2:
+ version "1.0.2"
+ resolved "https://registry.npmjs.org/path-is-inside/-/path-is-inside-1.0.2.tgz"
+ integrity sha512-DUWJr3+ULp4zXmol/SZkFf3JGsS9/SIv+Y3Rt93/UjPpDpklB5f1er4O3POIbUuUJ3FXgqte2Q7SrU6zAqwk8w==
+
path-key@^3.1.0:
version "3.1.1"
resolved "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz"
@@ -5258,11 +7105,21 @@ path-to-regexp@^8.1.0:
resolved "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.2.0.tgz"
integrity sha512-TdrF7fW9Rphjq4RjrW0Kp2AW0Ahwu9sRGTkS6bvDi0SCwZlEZYmcfDbEsTz8RVk0EHIS/Vd1bv3JhG+1xZuAyQ==
+path-to-regexp@3.3.0:
+ version "3.3.0"
+ resolved "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-3.3.0.tgz"
+ integrity sha512-qyCH421YQPS2WFDxDjftfc1ZR5WKQzVzqsp4n9M2kQhVOo/ByahFoUNJfl58kOcEGfQ//7weFTDhm+ss8Ecxgw==
+
path-type@^4.0.0:
version "4.0.0"
resolved "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz"
integrity sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==
+pathe@^2.0.1, pathe@^2.0.3:
+ version "2.0.3"
+ resolved "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz"
+ integrity sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==
+
pathval@^1.1.1:
version "1.1.1"
resolved "https://registry.npmjs.org/pathval/-/pathval-1.1.1.tgz"
@@ -5280,6 +7137,11 @@ pbkdf2@^3.0.17:
sha.js "^2.4.11"
to-buffer "^1.2.0"
+pend@~1.2.0:
+ version "1.2.0"
+ resolved "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz"
+ integrity sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==
+
pg-int8@1.0.1:
version "1.0.1"
resolved "https://registry.npmjs.org/pg-int8/-/pg-int8-1.0.1.tgz"
@@ -5306,12 +7168,22 @@ picocolors@^1.1.0, picocolors@^1.1.1:
resolved "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz"
integrity sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==
-picomatch@^2.0.4, picomatch@^2.2.1, picomatch@^2.3.1:
+picomatch@^2.0.4:
+ version "2.3.1"
+ resolved "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz"
+ integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==
+
+picomatch@^2.2.1:
+ version "2.3.1"
+ resolved "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz"
+ integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==
+
+picomatch@^2.3.1:
version "2.3.1"
resolved "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz"
integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==
-picomatch@^4.0.2:
+"picomatch@^3 || ^4", picomatch@^4.0.2:
version "4.0.2"
resolved "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz"
integrity sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==
@@ -5321,16 +7193,130 @@ picomatch@^4.0.3:
resolved "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz"
integrity sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==
+pify@^2.3.0:
+ version "2.3.0"
+ resolved "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz"
+ integrity sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==
+
+pino-abstract-transport@^1.2.0:
+ version "1.2.0"
+ resolved "https://registry.npmjs.org/pino-abstract-transport/-/pino-abstract-transport-1.2.0.tgz"
+ integrity sha512-Guhh8EZfPCfH+PMXAb6rKOjGQEoy0xlAIn+irODG5kgfYV+BQ0rGYYWTIel3P5mmyXqkYkPmdIkywsn6QKUR1Q==
+ dependencies:
+ readable-stream "^4.0.0"
+ split2 "^4.0.0"
+
+pino-std-serializers@^6.0.0:
+ version "6.2.2"
+ resolved "https://registry.npmjs.org/pino-std-serializers/-/pino-std-serializers-6.2.2.tgz"
+ integrity sha512-cHjPPsE+vhj/tnhCy/wiMh3M3z3h/j15zHQX+S9GkTBgqJuTuJzYJ4gUyACLhDaJ7kk9ba9iRDmbH2tJU03OiA==
+
+pino@^8.21.0:
+ version "8.21.0"
+ resolved "https://registry.npmjs.org/pino/-/pino-8.21.0.tgz"
+ integrity sha512-ip4qdzjkAyDDZklUaZkcRFb2iA118H9SgRh8yzTkSQK8HilsOJF7rSY8HoW5+I0M46AZgX/pxbprf2vvzQCE0Q==
+ dependencies:
+ atomic-sleep "^1.0.0"
+ fast-redact "^3.1.1"
+ on-exit-leak-free "^2.1.0"
+ pino-abstract-transport "^1.2.0"
+ pino-std-serializers "^6.0.0"
+ process-warning "^3.0.0"
+ quick-format-unescaped "^4.0.3"
+ real-require "^0.2.0"
+ safe-stable-stringify "^2.3.1"
+ sonic-boom "^3.7.0"
+ thread-stream "^2.6.0"
+
+pirates@^4.0.1:
+ version "4.0.7"
+ resolved "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz"
+ integrity sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==
+
+pkg-types@^1.3.1:
+ version "1.3.1"
+ resolved "https://registry.npmjs.org/pkg-types/-/pkg-types-1.3.1.tgz"
+ integrity sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==
+ dependencies:
+ confbox "^0.1.8"
+ mlly "^1.7.4"
+ pathe "^2.0.1"
+
pluralize@^8.0.0:
version "8.0.0"
resolved "https://registry.npmjs.org/pluralize/-/pluralize-8.0.0.tgz"
integrity sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA==
+points-on-curve@^0.2.0, points-on-curve@0.2.0:
+ version "0.2.0"
+ resolved "https://registry.npmjs.org/points-on-curve/-/points-on-curve-0.2.0.tgz"
+ integrity sha512-0mYKnYYe9ZcqMCWhUjItv/oHjvgEsfKvnUTg8sAtnHr3GVy7rGkXCb6d5cSyqrWqL4k81b9CPg3urd+T7aop3A==
+
+points-on-path@^0.2.1:
+ version "0.2.1"
+ resolved "https://registry.npmjs.org/points-on-path/-/points-on-path-0.2.1.tgz"
+ integrity sha512-25ClnWWuw7JbWZcgqY/gJ4FQWadKxGWk+3kR/7kD0tCaDtPPMj7oHu2ToLaVhfpnHrZzYby2w6tUA0eOIuUg8g==
+ dependencies:
+ path-data-parser "0.1.0"
+ points-on-curve "0.2.0"
+
possible-typed-array-names@^1.0.0:
version "1.1.0"
resolved "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz"
integrity sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==
+postcss-import@^15.1.0:
+ version "15.1.0"
+ resolved "https://registry.npmjs.org/postcss-import/-/postcss-import-15.1.0.tgz"
+ integrity sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==
+ dependencies:
+ postcss-value-parser "^4.0.0"
+ read-cache "^1.0.0"
+ resolve "^1.1.7"
+
+postcss-js@^4.0.1:
+ version "4.1.0"
+ resolved "https://registry.npmjs.org/postcss-js/-/postcss-js-4.1.0.tgz"
+ integrity sha512-oIAOTqgIo7q2EOwbhb8UalYePMvYoIeRY2YKntdpFQXNosSu3vLrniGgmH9OKs/qAkfoj5oB3le/7mINW1LCfw==
+ dependencies:
+ camelcase-css "^2.0.1"
+
+"postcss-load-config@^4.0.2 || ^5.0 || ^6.0":
+ version "6.0.1"
+ resolved "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-6.0.1.tgz"
+ integrity sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g==
+ dependencies:
+ lilconfig "^3.1.1"
+
+postcss-nested@^6.2.0:
+ version "6.2.0"
+ resolved "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.2.0.tgz"
+ integrity sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ==
+ dependencies:
+ postcss-selector-parser "^6.1.1"
+
+postcss-selector-parser@^6.1.1, postcss-selector-parser@^6.1.2:
+ version "6.1.2"
+ resolved "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz"
+ integrity sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==
+ dependencies:
+ cssesc "^3.0.0"
+ util-deprecate "^1.0.2"
+
+postcss-value-parser@^4.0.0:
+ version "4.2.0"
+ resolved "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz"
+ integrity sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==
+
+postcss@^8.0.0, postcss@^8.2.14, postcss@^8.4.21, postcss@^8.4.47, postcss@>=8.0.9:
+ version "8.5.8"
+ resolved "https://registry.npmjs.org/postcss/-/postcss-8.5.8.tgz"
+ integrity sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==
+ dependencies:
+ nanoid "^3.3.11"
+ picocolors "^1.1.1"
+ source-map-js "^1.2.1"
+
postgres-array@~2.0.0:
version "2.0.0"
resolved "https://registry.npmjs.org/postgres-array/-/postgres-array-2.0.0.tgz"
@@ -5370,6 +7356,21 @@ process-nextick-args@~2.0.0:
resolved "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz"
integrity sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==
+process-warning@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.npmjs.org/process-warning/-/process-warning-3.0.0.tgz"
+ integrity sha512-mqn0kFRl0EoqhnL0GQ0veqFHyIN1yig9RHh/InzORTUiZHFRAur+aMtRkELNwGs9aNwKS6tg/An4NYBPGwvtzQ==
+
+process@^0.11.10:
+ version "0.11.10"
+ resolved "https://registry.npmjs.org/process/-/process-0.11.10.tgz"
+ integrity sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==
+
+progress@^2.0.3:
+ version "2.0.3"
+ resolved "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz"
+ integrity sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==
+
proper-lockfile@^4.1.1, proper-lockfile@^4.1.2:
version "4.1.2"
resolved "https://registry.npmjs.org/proper-lockfile/-/proper-lockfile-4.1.2.tgz"
@@ -5402,6 +7403,20 @@ protobufjs@^7.2.5, protobufjs@^7.3.2:
"@types/node" ">=13.7.0"
long "^5.0.0"
+proxy-agent@^6.5.0:
+ version "6.5.0"
+ resolved "https://registry.npmjs.org/proxy-agent/-/proxy-agent-6.5.0.tgz"
+ integrity sha512-TmatMXdr2KlRiA2CyDu8GqR8EjahTG3aY3nXjdzFyoZbmB8hrBsTyMezhULIXKnC0jpfjlmiZ3+EaCzoInSu/A==
+ dependencies:
+ agent-base "^7.1.2"
+ debug "^4.3.4"
+ http-proxy-agent "^7.0.1"
+ https-proxy-agent "^7.0.6"
+ lru-cache "^7.14.1"
+ pac-proxy-agent "^7.1.0"
+ proxy-from-env "^1.1.0"
+ socks-proxy-agent "^8.0.5"
+
proxy-from-env@^1.1.0:
version "1.1.0"
resolved "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz"
@@ -5437,16 +7452,55 @@ punycode@^2.1.0:
resolved "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz"
integrity sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==
+puppeteer-core@23.11.1:
+ version "23.11.1"
+ resolved "https://registry.npmjs.org/puppeteer-core/-/puppeteer-core-23.11.1.tgz"
+ integrity sha512-3HZ2/7hdDKZvZQ7dhhITOUg4/wOrDRjyK2ZBllRB0ZCOi9u0cwq1ACHDjBB+nX+7+kltHjQvBRdeY7+W0T+7Gg==
+ dependencies:
+ "@puppeteer/browsers" "2.6.1"
+ chromium-bidi "0.11.0"
+ debug "^4.4.0"
+ devtools-protocol "0.0.1367902"
+ typed-query-selector "^2.12.0"
+ ws "^8.18.0"
+
+puppeteer@^23, puppeteer@>=8.0.0:
+ version "23.11.1"
+ resolved "https://registry.npmjs.org/puppeteer/-/puppeteer-23.11.1.tgz"
+ integrity sha512-53uIX3KR5en8l7Vd8n5DUv90Ae9QDQsyIthaUFVzwV6yU750RjqRznEtNMBT20VthqAdemnJN+hxVdmMHKt7Zw==
+ dependencies:
+ "@puppeteer/browsers" "2.6.1"
+ chromium-bidi "0.11.0"
+ cosmiconfig "^9.0.0"
+ devtools-protocol "0.0.1367902"
+ puppeteer-core "23.11.1"
+ typed-query-selector "^2.12.0"
+
queue-microtask@^1.2.2:
version "1.2.3"
resolved "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz"
integrity sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==
+quick-format-unescaped@^4.0.3:
+ version "4.0.4"
+ resolved "https://registry.npmjs.org/quick-format-unescaped/-/quick-format-unescaped-4.0.4.tgz"
+ integrity sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==
+
quick-lru@^5.1.1:
version "5.1.1"
resolved "https://registry.npmjs.org/quick-lru/-/quick-lru-5.1.1.tgz"
integrity sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==
+radash@^12.1.1:
+ version "12.1.1"
+ resolved "https://registry.npmjs.org/radash/-/radash-12.1.1.tgz"
+ integrity sha512-h36JMxKRqrAxVD8201FrCpyeNuUY9Y5zZwujr20fFO77tpUtGa6EZzfKw/3WaiBX95fq7+MpsuMLNdSnORAwSA==
+
+ramda@^0.28.0:
+ version "0.28.0"
+ resolved "https://registry.npmjs.org/ramda/-/ramda-0.28.0.tgz"
+ integrity sha512-9QnLuG/kPVgWvMQ4aODhsBUFKOUmnbUnsSXACv+NCQZcHbeb+v8Lodp8OVxtRULN1/xOyYLLaL6npE6dMq5QTA==
+
randombytes@^2.1.0:
version "2.1.0"
resolved "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz"
@@ -5454,6 +7508,11 @@ randombytes@^2.1.0:
dependencies:
safe-buffer "^5.1.0"
+range-parser@1.2.0:
+ version "1.2.0"
+ resolved "https://registry.npmjs.org/range-parser/-/range-parser-1.2.0.tgz"
+ integrity sha512-kA5WQoNVo4t9lNx2kQNFCxKeBl5IbbSNBl1M/tLkw9WCn+hxNBAW5Qh8gdhs63CJnhjJ2zQWFoqPJP2sK1AV5A==
+
raw-body@^2.4.1:
version "2.5.2"
resolved "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz"
@@ -5464,17 +7523,62 @@ raw-body@^2.4.1:
iconv-lite "0.4.24"
unpipe "1.0.0"
-rc@1.2.8:
- version "1.2.8"
- resolved "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz"
- integrity sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==
+rc@1.2.8:
+ version "1.2.8"
+ resolved "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz"
+ integrity sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==
+ dependencies:
+ deep-extend "^0.6.0"
+ ini "~1.3.0"
+ minimist "^1.2.0"
+ strip-json-comments "~2.0.1"
+
+"react-dom@^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom@^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", "react-dom@^18 || ^19 || ^19.0.0-rc", react-dom@^19.2.3, react-dom@>=16.8.0, react-dom@>=17.0.0:
+ version "19.2.4"
+ resolved "https://registry.npmjs.org/react-dom/-/react-dom-19.2.4.tgz"
+ integrity sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ==
+ dependencies:
+ scheduler "^0.27.0"
+
+"react@^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react@^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", "react@^18 || ^19 || ^19.0.0-rc", react@^19.2.3, react@^19.2.4, react@>=16.8.0, react@>=17.0.0:
+ version "19.2.4"
+ resolved "https://registry.npmjs.org/react/-/react-19.2.4.tgz"
+ integrity sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==
+
+read-cache@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz"
+ integrity sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==
+ dependencies:
+ pify "^2.3.0"
+
+readable-stream@^2.2.2:
+ version "2.3.8"
+ resolved "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz"
+ integrity sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==
+ dependencies:
+ core-util-is "~1.0.0"
+ inherits "~2.0.3"
+ isarray "~1.0.0"
+ process-nextick-args "~2.0.0"
+ safe-buffer "~5.1.1"
+ string_decoder "~1.1.1"
+ util-deprecate "~1.0.1"
+
+readable-stream@^2.3.0:
+ version "2.3.8"
+ resolved "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz"
+ integrity sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==
dependencies:
- deep-extend "^0.6.0"
- ini "~1.3.0"
- minimist "^1.2.0"
- strip-json-comments "~2.0.1"
+ core-util-is "~1.0.0"
+ inherits "~2.0.3"
+ isarray "~1.0.0"
+ process-nextick-args "~2.0.0"
+ safe-buffer "~5.1.1"
+ string_decoder "~1.1.1"
+ util-deprecate "~1.0.1"
-readable-stream@^2.2.2, readable-stream@^2.3.0, readable-stream@^2.3.5:
+readable-stream@^2.3.5:
version "2.3.8"
resolved "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz"
integrity sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==
@@ -5496,6 +7600,17 @@ readable-stream@^3.1.1, readable-stream@^3.4.0, readable-stream@^3.5.0, readable
string_decoder "^1.1.1"
util-deprecate "^1.0.1"
+readable-stream@^4.0.0:
+ version "4.7.0"
+ resolved "https://registry.npmjs.org/readable-stream/-/readable-stream-4.7.0.tgz"
+ integrity sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg==
+ dependencies:
+ abort-controller "^3.0.0"
+ buffer "^6.0.3"
+ events "^3.3.0"
+ process "^0.11.10"
+ string_decoder "^1.3.0"
+
readable-stream@~1.0.26-4:
version "1.0.34"
resolved "https://registry.npmjs.org/readable-stream/-/readable-stream-1.0.34.tgz"
@@ -5523,6 +7638,11 @@ readline-sync@^1.4.10:
resolved "https://registry.npmjs.org/readline-sync/-/readline-sync-1.4.10.tgz"
integrity sha512-gNva8/6UAe8QYepIQH/jQ2qn91Qj0B9sYjMBBs3QOB8F2CXcKgLxQaJRP76sWVRQt+QU+8fAkCbCvjjMFu7Ycw==
+real-require@^0.2.0:
+ version "0.2.0"
+ resolved "https://registry.npmjs.org/real-require/-/real-require-0.2.0.tgz"
+ integrity sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg==
+
registry-auth-token@^5.0.1:
version "5.0.2"
resolved "https://registry.npmjs.org/registry-auth-token/-/registry-auth-token-5.0.2.tgz"
@@ -5576,7 +7696,7 @@ resolve-from@^5.0.0:
resolved "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz"
integrity sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==
-resolve@1.17.0:
+resolve@^1.1.7, resolve@1.17.0:
version "1.17.0"
resolved "https://registry.npmjs.org/resolve/-/resolve-1.17.0.tgz"
integrity sha512-ic+7JYiV8Vi2yzQGFWOkiZD5Z9z7O2Zhm9XMaTxdJExKasieFCr+yXZ/WmXsckHiKl12ar0y6XiXDx3m4RHn1w==
@@ -5608,29 +7728,29 @@ responselike@^3.0.0:
dependencies:
lowercase-keys "^3.0.0"
-retry@0.13.1:
- version "0.13.1"
- resolved "https://registry.npmjs.org/retry/-/retry-0.13.1.tgz"
- integrity sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==
+restore-cursor@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.npmjs.org/restore-cursor/-/restore-cursor-2.0.0.tgz"
+ integrity sha512-6IzJLuGi4+R14vwagDHX+JrXmPVtPpn4mffDJ1UdR7/Edm87fl6yi8mMBIVvFtJaNTUvjughmW4hwLhRG7gC1Q==
+ dependencies:
+ onetime "^2.0.0"
+ signal-exit "^3.0.2"
retry@^0.12.0:
version "0.12.0"
resolved "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz"
integrity sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==
+retry@0.13.1:
+ version "0.13.1"
+ resolved "https://registry.npmjs.org/retry/-/retry-0.13.1.tgz"
+ integrity sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==
+
reusify@^1.0.4:
version "1.0.4"
resolved "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz"
integrity sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==
-ripemd160@=2.0.1:
- version "2.0.1"
- resolved "https://registry.npmjs.org/ripemd160/-/ripemd160-2.0.1.tgz"
- integrity sha512-J7f4wutN8mdbV08MJnXibYpCOPHR+yzy+iQ/AsjMv2j8cLavQ8VGagDFUwwTAdF8FmRKVeNpbTTEwNHCW1g94w==
- dependencies:
- hash-base "^2.0.0"
- inherits "^2.0.1"
-
ripemd160@^2.0.0, ripemd160@^2.0.1:
version "2.0.2"
resolved "https://registry.npmjs.org/ripemd160/-/ripemd160-2.0.2.tgz"
@@ -5639,6 +7759,14 @@ ripemd160@^2.0.0, ripemd160@^2.0.1:
hash-base "^3.0.0"
inherits "^2.0.1"
+ripemd160@=2.0.1:
+ version "2.0.1"
+ resolved "https://registry.npmjs.org/ripemd160/-/ripemd160-2.0.1.tgz"
+ integrity sha512-J7f4wutN8mdbV08MJnXibYpCOPHR+yzy+iQ/AsjMv2j8cLavQ8VGagDFUwwTAdF8FmRKVeNpbTTEwNHCW1g94w==
+ dependencies:
+ hash-base "^2.0.0"
+ inherits "^2.0.1"
+
rlp@^2.2.4:
version "2.2.7"
resolved "https://registry.npmjs.org/rlp/-/rlp-2.2.7.tgz"
@@ -5646,6 +7774,21 @@ rlp@^2.2.4:
dependencies:
bn.js "^5.2.0"
+robust-predicates@^3.0.2:
+ version "3.0.2"
+ resolved "https://registry.npmjs.org/robust-predicates/-/robust-predicates-3.0.2.tgz"
+ integrity sha512-IXgzBWvWQwE6PrDI05OvmXUIruQTcoMDzRsOd5CDvHCVLcLHMTSYvOK5Cm46kWqlV3yAbuSpBZdJ5oP5OUoStg==
+
+roughjs@^4.6.6:
+ version "4.6.6"
+ resolved "https://registry.npmjs.org/roughjs/-/roughjs-4.6.6.tgz"
+ integrity sha512-ZUz/69+SYpFN/g/lUlo2FXcIjRkSu3nDarreVdGGndHEBJ6cXPdKguS8JGxwj5HA5xIbVKSmLgr5b3AWxtRfvQ==
+ dependencies:
+ hachure-fill "^0.5.2"
+ path-data-parser "^0.1.0"
+ points-on-curve "^0.2.0"
+ points-on-path "^0.2.1"
+
run-parallel@^1.1.9:
version "1.2.0"
resolved "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz"
@@ -5653,6 +7796,18 @@ run-parallel@^1.1.9:
dependencies:
queue-microtask "^1.2.2"
+rw@1:
+ version "1.3.3"
+ resolved "https://registry.npmjs.org/rw/-/rw-1.3.3.tgz"
+ integrity sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ==
+
+rxjs@^6.3.3:
+ version "6.6.7"
+ resolved "https://registry.npmjs.org/rxjs/-/rxjs-6.6.7.tgz"
+ integrity sha512-hTdwr+7yYNIT5n4AMYp85KA6yw2Va0FLa3Rguvbpa4W3I5xynaBZo41cM3XM+4Q6fRMj3sBYIR1VAmZMXYJvRQ==
+ dependencies:
+ tslib "^1.9.0"
+
safe-buffer@^5.0.1, safe-buffer@^5.1.0, safe-buffer@^5.1.1, safe-buffer@^5.1.2, safe-buffer@^5.2.0, safe-buffer@^5.2.1, safe-buffer@~5.2.0:
version "5.2.1"
resolved "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz"
@@ -5663,11 +7818,21 @@ safe-buffer@~5.1.0, safe-buffer@~5.1.1:
resolved "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz"
integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==
-"safer-buffer@>= 2.1.2 < 3", safer-buffer@~2.1.0:
+safe-stable-stringify@^2.3.1:
+ version "2.5.0"
+ resolved "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.5.0.tgz"
+ integrity sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==
+
+"safer-buffer@>= 2.1.2 < 3", "safer-buffer@>= 2.1.2 < 3.0.0", safer-buffer@~2.1.0:
version "2.1.2"
resolved "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz"
integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==
+scheduler@^0.27.0:
+ version "0.27.0"
+ resolved "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz"
+ integrity sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==
+
scrypt-js@^3.0.0:
version "3.0.1"
resolved "https://registry.npmjs.org/scrypt-js/-/scrypt-js-3.0.1.tgz"
@@ -5682,6 +7847,14 @@ secp256k1@^4.0.1:
node-addon-api "^5.0.0"
node-gyp-build "^4.2.0"
+section-matter@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.npmjs.org/section-matter/-/section-matter-1.0.0.tgz"
+ integrity sha512-vfD3pmTzGpufjScBh50YHKzEu2lxBWhVEHsNGoEXmCmn2hKGfeNLYMzCJpe8cD7gqX7TJluOVpBkAequ6dgMmA==
+ dependencies:
+ extend-shallow "^2.0.1"
+ kind-of "^6.0.0"
+
semver@^5.5.0:
version "5.7.2"
resolved "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz"
@@ -5692,7 +7865,7 @@ semver@^6.3.0:
resolved "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz"
integrity sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==
-semver@^7.3.7, semver@^7.5.2, semver@^7.6.2:
+semver@^7.3.7, semver@^7.5.2, semver@^7.6.2, semver@^7.6.3:
version "7.6.3"
resolved "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz"
integrity sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==
@@ -5709,6 +7882,19 @@ serialize-javascript@^6.0.2:
dependencies:
randombytes "^2.1.0"
+serve-handler@^6.1.3:
+ version "6.1.7"
+ resolved "https://registry.npmjs.org/serve-handler/-/serve-handler-6.1.7.tgz"
+ integrity sha512-CinAq1xWb0vR3twAv9evEU8cNWkXCb9kd5ePAHUKJBkOsUpR1wt/CvGdeca7vqumL1U5cSaeVQ6zZMxiJ3yWsg==
+ dependencies:
+ bytes "3.0.0"
+ content-disposition "0.5.2"
+ mime-types "2.1.18"
+ minimatch "3.1.5"
+ path-is-inside "1.0.2"
+ path-to-regexp "3.3.0"
+ range-parser "1.2.0"
+
set-function-length@^1.2.2:
version "1.2.2"
resolved "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz"
@@ -5776,7 +7962,7 @@ sinon-chai@^3.7.0:
resolved "https://registry.npmjs.org/sinon-chai/-/sinon-chai-3.7.0.tgz"
integrity sha512-mf5NURdUaSdnatJx3uhoBOrY9dtL19fiOtAdT1Azxg3+lNJFiuN0uzaU3xX1LeAfL17kHQhTAJgpsfhbMJMY2g==
-sinon@^18.0.0:
+sinon@^18.0.0, sinon@>=4.0.0:
version "18.0.1"
resolved "https://registry.npmjs.org/sinon/-/sinon-18.0.1.tgz"
integrity sha512-a2N2TDY1uGviajJ6r4D1CyRAkzE9NNVlYOV1wX5xQDuAk0ONgzgRl0EjCQuRCPxOwp13ghsMwt9Gdldujs39qw==
@@ -5797,11 +7983,38 @@ slice-ansi@^4.0.0:
astral-regex "^2.0.0"
is-fullwidth-code-point "^3.0.0"
+slice-ansi@0.0.4:
+ version "0.0.4"
+ resolved "https://registry.npmjs.org/slice-ansi/-/slice-ansi-0.0.4.tgz"
+ integrity sha512-up04hB2hR92PgjpyU3y/eg91yIBILyjVY26NvvciY3EVVPjybkMszMpXQ9QAkcS3I5rtJBDLoTxxg+qvW8c7rw==
+
+smart-buffer@^4.2.0:
+ version "4.2.0"
+ resolved "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz"
+ integrity sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==
+
smol-toml@^1.4.1:
version "1.4.2"
resolved "https://registry.npmjs.org/smol-toml/-/smol-toml-1.4.2.tgz"
integrity sha512-rInDH6lCNiEyn3+hH8KVGFdbjc099j47+OSgbMrfDYX1CmXLfdKd7qi6IfcWj2wFxvSVkuI46M+wPGYfEOEj6g==
+socks-proxy-agent@^8.0.5:
+ version "8.0.5"
+ resolved "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-8.0.5.tgz"
+ integrity sha512-HehCEsotFqbPW9sJ8WVYB6UbmIMv7kUUORIF2Nncq4VQvBfNBLibW9YZR5dlYCSUhwcD628pRllm7n+E+YTzJw==
+ dependencies:
+ agent-base "^7.1.2"
+ debug "^4.3.4"
+ socks "^2.8.3"
+
+socks@^2.8.3:
+ version "2.8.7"
+ resolved "https://registry.npmjs.org/socks/-/socks-2.8.7.tgz"
+ integrity sha512-HLpt+uLy/pxB+bum/9DzAgiKS8CX1EvbWxI4zlmgGCExImLdiad2iCwXT5Z4c9c3Eq8rP2318mPW2c+QbtjK8A==
+ dependencies:
+ ip-address "^10.0.1"
+ smart-buffer "^4.2.0"
+
solc@0.8.26:
version "0.8.26"
resolved "https://registry.npmjs.org/solc/-/solc-0.8.26.tgz"
@@ -5849,6 +8062,18 @@ solidity-ast@^0.4.56, solidity-ast@^0.4.60:
resolved "https://registry.npmjs.org/solidity-ast/-/solidity-ast-0.4.60.tgz"
integrity sha512-UwhasmQ37ji1ul8cIp0XlrQ/+SVQhy09gGqJH4jnwdo2TgI6YIByzi0PI5QvIGcIdFOs1pbSmJW1pnWB7AVh2w==
+sonic-boom@^3.7.0:
+ version "3.8.1"
+ resolved "https://registry.npmjs.org/sonic-boom/-/sonic-boom-3.8.1.tgz"
+ integrity sha512-y4Z8LCDBuum+PBP3lSV7RHrXscqksve/bi0as7mhwVnBW+/wUqKT/2Kb7um8yqcFy0duYbbPxzt89Zy2nOCaxg==
+ dependencies:
+ atomic-sleep "^1.0.0"
+
+source-map-js@^1.2.1:
+ version "1.2.1"
+ resolved "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz"
+ integrity sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==
+
source-map-support@^0.5.13, source-map-support@^0.5.21:
version "0.5.21"
resolved "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz"
@@ -5857,7 +8082,7 @@ source-map-support@^0.5.13, source-map-support@^0.5.21:
buffer-from "^1.0.0"
source-map "^0.6.0"
-source-map@^0.6.0:
+source-map@^0.6.0, source-map@~0.6.1:
version "0.6.1"
resolved "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz"
integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==
@@ -5867,6 +8092,16 @@ split-ca@^1.0.0, split-ca@^1.0.1:
resolved "https://registry.npmjs.org/split-ca/-/split-ca-1.0.1.tgz"
integrity sha512-Q5thBSxp5t8WPTTJQS59LrGqOZqOsrhDGDVm8azCqIBjSBd7nd9o2PM+mDulQQkh8h//4U6hFZnc/mul8t5pWQ==
+split2@^4.0.0:
+ version "4.2.0"
+ resolved "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz"
+ integrity sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==
+
+sprintf-js@~1.0.2:
+ version "1.0.3"
+ resolved "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz"
+ integrity sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==
+
ssh2@^1.15.0:
version "1.16.0"
resolved "https://registry.npmjs.org/ssh2/-/ssh2-1.16.0.tgz"
@@ -5890,7 +8125,61 @@ statuses@2.0.1:
resolved "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz"
integrity sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==
-"string-width-cjs@npm:string-width@^4.2.0", string-width@^4.0.0, string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.2, string-width@^4.2.3:
+streamx@^2.12.5, streamx@^2.15.0, streamx@^2.21.0:
+ version "2.23.0"
+ resolved "https://registry.npmjs.org/streamx/-/streamx-2.23.0.tgz"
+ integrity sha512-kn+e44esVfn2Fa/O0CPFcex27fjIL6MkVae0Mm6q+E6f0hWv578YCERbv+4m02cjxvDsPKLnmxral/rR6lBMAg==
+ dependencies:
+ events-universal "^1.0.0"
+ fast-fifo "^1.3.2"
+ text-decoder "^1.1.0"
+
+string_decoder@^1.1.1, string_decoder@^1.3.0:
+ version "1.3.0"
+ resolved "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz"
+ integrity sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==
+ dependencies:
+ safe-buffer "~5.2.0"
+
+string_decoder@~0.10.x:
+ version "0.10.31"
+ resolved "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz"
+ integrity sha512-ev2QzSzWPYmy9GuqfIVildA4OdcGLeFZQrq5ys6RtiuF+RQQiZWr8TZNyAcuVXyQRYfEO+MsoB/1BuQVhOJuoQ==
+
+string_decoder@~1.1.1:
+ version "1.1.1"
+ resolved "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz"
+ integrity sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==
+ dependencies:
+ safe-buffer "~5.1.0"
+
+"string-width-cjs@npm:string-width@^4.2.0":
+ version "4.2.3"
+ resolved "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz"
+ integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
+ dependencies:
+ emoji-regex "^8.0.0"
+ is-fullwidth-code-point "^3.0.0"
+ strip-ansi "^6.0.1"
+
+string-width@^1.0.1:
+ version "1.0.2"
+ resolved "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz"
+ integrity sha512-0XsVpQLnVCXHJfyEs8tC0zpTVIr5PKKsQtkT29IwupnPTjtPmQ3xT/4yCREF9hYkV/3M3kzcUTSAZT6a6h81tw==
+ dependencies:
+ code-point-at "^1.0.0"
+ is-fullwidth-code-point "^1.0.0"
+ strip-ansi "^3.0.0"
+
+string-width@^2.1.1:
+ version "2.1.1"
+ resolved "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz"
+ integrity sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==
+ dependencies:
+ is-fullwidth-code-point "^2.0.0"
+ strip-ansi "^4.0.0"
+
+string-width@^4.0.0, string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.2, string-width@^4.2.3:
version "4.2.3"
resolved "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz"
integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
@@ -5908,26 +8197,28 @@ string-width@^5.0.1, string-width@^5.1.2:
emoji-regex "^9.2.2"
strip-ansi "^7.0.1"
-string_decoder@^1.1.1:
- version "1.3.0"
- resolved "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz"
- integrity sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==
+"strip-ansi-cjs@npm:strip-ansi@^6.0.1":
+ version "6.0.1"
+ resolved "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz"
+ integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
dependencies:
- safe-buffer "~5.2.0"
+ ansi-regex "^5.0.1"
-string_decoder@~0.10.x:
- version "0.10.31"
- resolved "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz"
- integrity sha512-ev2QzSzWPYmy9GuqfIVildA4OdcGLeFZQrq5ys6RtiuF+RQQiZWr8TZNyAcuVXyQRYfEO+MsoB/1BuQVhOJuoQ==
+strip-ansi@^3.0.0, strip-ansi@^3.0.1:
+ version "3.0.1"
+ resolved "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz"
+ integrity sha512-VhumSSbBqDTP8p2ZLKj40UjBCV4+v8bUSEpUb4KjRgWk9pbqGF4REFj6KEagidb2f/M6AzC0EmFyDNGaw9OCzg==
+ dependencies:
+ ansi-regex "^2.0.0"
-string_decoder@~1.1.1:
- version "1.1.1"
- resolved "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz"
- integrity sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==
+strip-ansi@^4.0.0:
+ version "4.0.0"
+ resolved "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz"
+ integrity sha512-4XaJ2zQdCzROZDivEVIDPkcQn8LMFSa8kj8Gxb/Lnwzv9A8VctNZ+lfivC/sV3ivW8ElJTERXZoPBRrZKkNKow==
dependencies:
- safe-buffer "~5.1.0"
+ ansi-regex "^3.0.0"
-"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.0, strip-ansi@^6.0.1:
+strip-ansi@^6.0.0, strip-ansi@^6.0.1:
version "6.0.1"
resolved "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz"
integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
@@ -5941,6 +8232,11 @@ strip-ansi@^7.0.1:
dependencies:
ansi-regex "^6.0.1"
+strip-bom-string@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.npmjs.org/strip-bom-string/-/strip-bom-string-1.0.0.tgz"
+ integrity sha512-uCC2VHvQRYu+lMh4My/sFNmF2klFymLX1wHJeXnbEJERpV/ZsVuonzerjfrGpIGF7LBVa1O7i9kjiWvJiFck8g==
+
strip-json-comments@^3.1.1:
version "3.1.1"
resolved "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz"
@@ -5956,6 +8252,36 @@ strnum@^2.1.0:
resolved "https://registry.npmjs.org/strnum/-/strnum-2.1.1.tgz"
integrity sha512-7ZvoFTiCnGxBtDqJ//Cu6fWtZtc7Y3x+QOirG15wztbdngGSkht27o2pyGWrVy0b4WAy3jbKmnoK6g5VlVNUUw==
+stylis@^4.3.6:
+ version "4.3.6"
+ resolved "https://registry.npmjs.org/stylis/-/stylis-4.3.6.tgz"
+ integrity sha512-yQ3rwFWRfwNUY7H5vpU0wfdkNSnvnJinhF9830Swlaxl03zsOjCfmX0ugac+3LtK0lYSgwL/KXc8oYL3mG4YFQ==
+
+sucrase@^3.35.0:
+ version "3.35.1"
+ resolved "https://registry.npmjs.org/sucrase/-/sucrase-3.35.1.tgz"
+ integrity sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw==
+ dependencies:
+ "@jridgewell/gen-mapping" "^0.3.2"
+ commander "^4.0.0"
+ lines-and-columns "^1.1.6"
+ mz "^2.7.0"
+ pirates "^4.0.1"
+ tinyglobby "^0.2.11"
+ ts-interface-checker "^0.1.9"
+
+supports-color@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz"
+ integrity sha512-KKNVtd6pCYgPIKU4cp2733HWYCpplQhddZLBUryaAHou723x+FRzQ5Df824Fj+IyyuiQTRoub4SnIFfIcrp70g==
+
+supports-color@^5.3.0:
+ version "5.5.0"
+ resolved "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz"
+ integrity sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==
+ dependencies:
+ has-flag "^3.0.0"
+
supports-color@^7, supports-color@^7.1.0:
version "7.2.0"
resolved "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz"
@@ -5975,6 +8301,16 @@ supports-preserve-symlinks-flag@^1.0.0:
resolved "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz"
integrity sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==
+symbol-observable@^1.1.0:
+ version "1.2.0"
+ resolved "https://registry.npmjs.org/symbol-observable/-/symbol-observable-1.2.0.tgz"
+ integrity sha512-e900nM8RRtGhlV36KGEU9k65K3mPb1WV70OdjfxlG2EAuM1noi/E/BaW/uMhL7bPEssK8QV57vN3esixjUvcXQ==
+
+tabbable@^6.0.0:
+ version "6.4.0"
+ resolved "https://registry.npmjs.org/tabbable/-/tabbable-6.4.0.tgz"
+ integrity sha512-05PUHKSNE8ou2dwIxTngl4EzcnsCDZGJ/iCLtDflR/SHB/ny14rXc+qU5P4mG9JkusiV7EivzY9Mhm55AzAvCg==
+
table@^6.8.0, table@^6.8.1:
version "6.8.2"
resolved "https://registry.npmjs.org/table/-/table-6.8.2.tgz"
@@ -5986,6 +8322,50 @@ table@^6.8.0, table@^6.8.1:
string-width "^4.2.3"
strip-ansi "^6.0.1"
+tailwind-merge@^3.4.0:
+ version "3.5.0"
+ resolved "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-3.5.0.tgz"
+ integrity sha512-I8K9wewnVDkL1NTGoqWmVEIlUcB9gFriAEkXkfCjX5ib8ezGxtR3xD7iZIxrfArjEsH7F1CHD4RFUtxefdqV/A==
+
+"tailwindcss@^3.0 || ^4.0", tailwindcss@^3.4.19:
+ version "3.4.19"
+ resolved "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.19.tgz"
+ integrity sha512-3ofp+LL8E+pK/JuPLPggVAIaEuhvIz4qNcf3nA1Xn2o/7fb7s/TYpHhwGDv1ZU3PkBluUVaF8PyCHcm48cKLWQ==
+ dependencies:
+ "@alloc/quick-lru" "^5.2.0"
+ arg "^5.0.2"
+ chokidar "^3.6.0"
+ didyoumean "^1.2.2"
+ dlv "^1.1.3"
+ fast-glob "^3.3.2"
+ glob-parent "^6.0.2"
+ is-glob "^4.0.3"
+ jiti "^1.21.7"
+ lilconfig "^3.1.3"
+ micromatch "^4.0.8"
+ normalize-path "^3.0.0"
+ object-hash "^3.0.0"
+ picocolors "^1.1.1"
+ postcss "^8.4.47"
+ postcss-import "^15.1.0"
+ postcss-js "^4.0.1"
+ postcss-load-config "^4.0.2 || ^5.0 || ^6.0"
+ postcss-nested "^6.2.0"
+ postcss-selector-parser "^6.1.2"
+ resolve "^1.22.8"
+ sucrase "^3.35.0"
+
+tar-fs@^3.0.6:
+ version "3.1.2"
+ resolved "https://registry.npmjs.org/tar-fs/-/tar-fs-3.1.2.tgz"
+ integrity sha512-QGxxTxxyleAdyM3kpFs14ymbYmNFrfY+pHj7Z8FgtbZ7w2//VAgLMac7sT6nRpIHjppXO2AwwEOg0bPFVRcmXw==
+ dependencies:
+ pump "^3.0.0"
+ tar-stream "^3.1.5"
+ optionalDependencies:
+ bare-fs "^4.0.1"
+ bare-path "^3.0.0"
+
tar-fs@~1.16.3:
version "1.16.5"
resolved "https://registry.npmjs.org/tar-fs/-/tar-fs-1.16.5.tgz"
@@ -6030,17 +8410,67 @@ tar-stream@^2.1.4:
inherits "^2.0.3"
readable-stream "^3.1.1"
+tar-stream@^3.1.5:
+ version "3.1.8"
+ resolved "https://registry.npmjs.org/tar-stream/-/tar-stream-3.1.8.tgz"
+ integrity sha512-U6QpVRyCGHva435KoNWy9PRoi2IFYCgtEhq9nmrPPpbRacPs9IH4aJ3gbrFC8dPcXvdSZ4XXfXT5Fshbp2MtlQ==
+ dependencies:
+ b4a "^1.6.4"
+ bare-fs "^4.5.5"
+ fast-fifo "^1.2.0"
+ streamx "^2.15.0"
+
+teex@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.npmjs.org/teex/-/teex-1.0.1.tgz"
+ integrity sha512-eYE6iEI62Ni1H8oIa7KlDU6uQBtqr4Eajni3wX7rpfXD8ysFx8z0+dri+KWEPWpBsxXfxu58x/0jvTVT1ekOSg==
+ dependencies:
+ streamx "^2.12.5"
+
+text-decoder@^1.1.0:
+ version "1.2.7"
+ resolved "https://registry.npmjs.org/text-decoder/-/text-decoder-1.2.7.tgz"
+ integrity sha512-vlLytXkeP4xvEq2otHeJfSQIRyWxo/oZGEbXrtEEF9Hnmrdly59sUbzZ/QgyWuLYHctCHxFF4tRQZNQ9k60ExQ==
+ dependencies:
+ b4a "^1.6.4"
+
text-table@^0.2.0:
version "0.2.0"
resolved "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz"
integrity sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==
-"through@>=2.2.7 <3":
+thenify-all@^1.0.0:
+ version "1.6.0"
+ resolved "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz"
+ integrity sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==
+ dependencies:
+ thenify ">= 3.1.0 < 4"
+
+"thenify@>= 3.1.0 < 4":
+ version "3.3.1"
+ resolved "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz"
+ integrity sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==
+ dependencies:
+ any-promise "^1.0.0"
+
+thread-stream@^2.6.0:
+ version "2.7.0"
+ resolved "https://registry.npmjs.org/thread-stream/-/thread-stream-2.7.0.tgz"
+ integrity sha512-qQiRWsU/wvNolI6tbbCKd9iKaTnCXsTwVxhhKM6nctPdujTyztjlbUkUTUymidWcMnZ5pWR0ej4a0tjsW021vw==
+ dependencies:
+ real-require "^0.2.0"
+
+through@^2.3.8, "through@>=2.2.7 <3":
version "2.3.8"
resolved "https://registry.npmjs.org/through/-/through-2.3.8.tgz"
integrity sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==
-tinyglobby@^0.2.14:
+tinyexec@^1.0.1:
+ version "1.0.2"
+ resolved "https://registry.npmjs.org/tinyexec/-/tinyexec-1.0.2.tgz"
+ integrity sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg==
+
+tinyglobby@^0.2.11, tinyglobby@^0.2.14:
version "0.2.14"
resolved "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.14.tgz"
integrity sha512-tX5e7OM1HnYr2+a2C/4V0htOcSQcoSTH9KgJnVvNm5zm/cyEWKJ7j7YutsH9CxMdtOkkLFy2AHrMci9IM8IPZQ==
@@ -6089,6 +8519,16 @@ tr46@~0.0.3:
resolved "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz"
integrity sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==
+ts-dedent@^2.2.0:
+ version "2.2.0"
+ resolved "https://registry.npmjs.org/ts-dedent/-/ts-dedent-2.2.0.tgz"
+ integrity sha512-q5W7tVM71e2xjHZTlgfTDoPF/SmqKG5hddq9SzR49CH2hayqRKJtQ4mtRlSxKaJlR/+9rEM+mnBHf7I2/BQcpQ==
+
+ts-interface-checker@^0.1.9:
+ version "0.1.13"
+ resolved "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz"
+ integrity sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==
+
ts-morph@^22.0.0:
version "22.0.0"
resolved "https://registry.npmjs.org/ts-morph/-/ts-morph-22.0.0.tgz"
@@ -6097,7 +8537,7 @@ ts-morph@^22.0.0:
"@ts-morph/common" "~0.23.0"
code-block-writer "^13.0.1"
-ts-node@^10.9.2:
+ts-node@*, ts-node@^10.9.2:
version "10.9.2"
resolved "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz"
integrity sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==
@@ -6116,21 +8556,31 @@ ts-node@^10.9.2:
v8-compile-cache-lib "^3.0.1"
yn "3.1.1"
-tslib@2.7.0:
- version "2.7.0"
- resolved "https://registry.npmjs.org/tslib/-/tslib-2.7.0.tgz"
- integrity sha512-gLXCKdN1/j47AiHiOkJN69hJmcbGTHI0ImLmbYLHykhgeN0jVGola9yVjFgzCUklsZQMW55o+dW7IXv3RCXDzA==
+tslib@^1.11.1:
+ version "1.14.1"
+ resolved "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz"
+ integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==
+
+tslib@^1.9.0:
+ version "1.14.1"
+ resolved "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz"
+ integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==
-tslib@^1.11.1, tslib@^1.9.3:
+tslib@^1.9.3:
version "1.14.1"
resolved "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz"
integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==
-tslib@^2.3.1, tslib@^2.6.2:
+tslib@^2.0.1, tslib@^2.3.1, tslib@^2.6.2, tslib@^2.8.0:
version "2.8.1"
resolved "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz"
integrity sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==
+tslib@2.7.0:
+ version "2.7.0"
+ resolved "https://registry.npmjs.org/tslib/-/tslib-2.7.0.tgz"
+ integrity sha512-gLXCKdN1/j47AiHiOkJN69hJmcbGTHI0ImLmbYLHykhgeN0jVGola9yVjFgzCUklsZQMW55o+dW7IXv3RCXDzA==
+
tsort@0.0.1:
version "0.0.1"
resolved "https://registry.npmjs.org/tsort/-/tsort-0.0.1.tgz"
@@ -6141,16 +8591,16 @@ tweetnacl@^0.14.3:
resolved "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz"
integrity sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA==
-type-detect@4.0.8:
- version "4.0.8"
- resolved "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz"
- integrity sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==
-
type-detect@^4.0.0, type-detect@^4.1.0:
version "4.1.0"
resolved "https://registry.npmjs.org/type-detect/-/type-detect-4.1.0.tgz"
integrity sha512-Acylog8/luQ8L7il+geoSxhEkazvkslg7PSNKOX59mbB9cOveP5aq9h74Y7YU8yDpJwetzQQrfIwtf4Wp4LKcw==
+type-detect@4.0.8:
+ version "4.0.8"
+ resolved "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz"
+ integrity sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==
+
type-fest@^0.20.2:
version "0.20.2"
resolved "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz"
@@ -6175,16 +8625,34 @@ typed-array-buffer@^1.0.3:
es-errors "^1.3.0"
is-typed-array "^1.1.14"
+typed-query-selector@^2.12.0:
+ version "2.12.1"
+ resolved "https://registry.npmjs.org/typed-query-selector/-/typed-query-selector-2.12.1.tgz"
+ integrity sha512-uzR+FzI8qrUEIu96oaeBJmd9E7CFEiQ3goA5qCVgc4s5llSubcfGHq9yUstZx/k4s9dXHVKsE35YWoFyvEqEHA==
+
typedarray@^0.0.6:
version "0.0.6"
resolved "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz"
integrity sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==
-typescript@^5.9.2:
+typescript@*, typescript@^5.9.2, typescript@>=2.7, typescript@>=4.9.5:
version "5.9.2"
resolved "https://registry.npmjs.org/typescript/-/typescript-5.9.2.tgz"
integrity sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A==
+ufo@^1.6.3:
+ version "1.6.3"
+ resolved "https://registry.npmjs.org/ufo/-/ufo-1.6.3.tgz"
+ integrity sha512-yDJTmhydvl5lJzBmy/hyOAA0d+aqCBuwl818haVdYCRrWV84o7YyeVm4QlVHStqNrrJSTb6jKuFAVqAFsr+K3Q==
+
+unbzip2-stream@^1.4.3:
+ version "1.4.3"
+ resolved "https://registry.npmjs.org/unbzip2-stream/-/unbzip2-stream-1.4.3.tgz"
+ integrity sha512-mlExGW4w71ebDJviH16lQLtZS32VKqsSfk80GCfUlwT/4/hNRFsoscrF/c++9xinkMzECL1uL9DDwXqFWkruPg==
+ dependencies:
+ buffer "^5.2.1"
+ through "^2.3.8"
+
undici-types@~6.19.2:
version "6.19.8"
resolved "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz"
@@ -6234,7 +8702,12 @@ uri-js@^4.2.2:
dependencies:
punycode "^2.1.0"
-util-deprecate@^1.0.1, util-deprecate@~1.0.1:
+use-sync-external-store@^1.5.0:
+ version "1.6.0"
+ resolved "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz"
+ integrity sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==
+
+util-deprecate@^1.0.1, util-deprecate@^1.0.2, util-deprecate@~1.0.1:
version "1.0.2"
resolved "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz"
integrity sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==
@@ -6249,6 +8722,11 @@ uuid@^11.0.3:
resolved "https://registry.npmjs.org/uuid/-/uuid-11.1.0.tgz"
integrity sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==
+uuid@^11.1.0:
+ version "11.1.0"
+ resolved "https://registry.npmjs.org/uuid/-/uuid-11.1.0.tgz"
+ integrity sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==
+
uuid@^8.3.2:
version "8.3.2"
resolved "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz"
@@ -6264,12 +8742,37 @@ v8-compile-cache-lib@^3.0.1:
resolved "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz"
integrity sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==
-vscode-languageserver-textdocument@^1.0.12:
+vscode-jsonrpc@8.2.0:
+ version "8.2.0"
+ resolved "https://registry.npmjs.org/vscode-jsonrpc/-/vscode-jsonrpc-8.2.0.tgz"
+ integrity sha512-C+r0eKJUIfiDIfwJhria30+TYWPtuHJXHtI7J0YlOmKAo7ogxP20T0zxB7HZQIFhIyvoBPwWskjxrvAtfjyZfA==
+
+vscode-languageserver-protocol@3.17.5:
+ version "3.17.5"
+ resolved "https://registry.npmjs.org/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.17.5.tgz"
+ integrity sha512-mb1bvRJN8SVznADSGWM9u/b07H7Ecg0I3OgXDuLdn307rl/J3A9YD6/eYOssqhecL27hK1IPZAsaqh00i/Jljg==
+ dependencies:
+ vscode-jsonrpc "8.2.0"
+ vscode-languageserver-types "3.17.5"
+
+vscode-languageserver-textdocument@^1.0.12, vscode-languageserver-textdocument@~1.0.11:
version "1.0.12"
resolved "https://registry.npmjs.org/vscode-languageserver-textdocument/-/vscode-languageserver-textdocument-1.0.12.tgz"
integrity sha512-cxWNPesCnQCcMPeenjKKsOCKQZ/L6Tv19DTRIGuLWe32lyzWhihGVJ/rcckZXJxfdKCFvRLS3fpBIsV/ZGX4zA==
-vscode-uri@^3.1.0:
+vscode-languageserver-types@3.17.5:
+ version "3.17.5"
+ resolved "https://registry.npmjs.org/vscode-languageserver-types/-/vscode-languageserver-types-3.17.5.tgz"
+ integrity sha512-Ld1VelNuX9pdF39h2Hgaeb5hEZM2Z3jUrrMgWQAu82jMtZp7p3vJT3BzToKtZI7NgQssZje5o0zryOrhQvzQAg==
+
+vscode-languageserver@~9.0.1:
+ version "9.0.1"
+ resolved "https://registry.npmjs.org/vscode-languageserver/-/vscode-languageserver-9.0.1.tgz"
+ integrity sha512-woByF3PDpkHFUreUa7Hos7+pUWdeWMXRd26+ZX2A8cFx6v/JPTtd4/uN0/jB6XQHYaOlHbio03NTHCqrgG5n7g==
+ dependencies:
+ vscode-languageserver-protocol "3.17.5"
+
+vscode-uri@^3.1.0, vscode-uri@~3.1.0:
version "3.1.0"
resolved "https://registry.npmjs.org/vscode-uri/-/vscode-uri-3.1.0.tgz"
integrity sha512-/BpdSx+yCQGnCvecbyXdxHDkuk55/G3xwnC0GqY4gmQ3j+A+g8kzzgB4Nk/SINjqn6+waqw3EgbVF2QKExkRxQ==
@@ -6324,7 +8827,24 @@ workerpool@^9.2.0:
resolved "https://registry.npmjs.org/workerpool/-/workerpool-9.3.3.tgz"
integrity sha512-slxCaKbYjEdFT/o2rH9xS1hf4uRDch1w7Uo+apxhZ+sf/1d9e0ZVkn42kPNGP2dgjIx6YFvSevj0zHvbWe2jdw==
-"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0", wrap-ansi@^7.0.0:
+"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0":
+ version "7.0.0"
+ resolved "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz"
+ integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==
+ dependencies:
+ ansi-styles "^4.0.0"
+ string-width "^4.1.0"
+ strip-ansi "^6.0.0"
+
+wrap-ansi@^3.0.1:
+ version "3.0.1"
+ resolved "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-3.0.1.tgz"
+ integrity sha512-iXR3tDXpbnTpzjKSylUJRkLuOrEC7hwEB221cgn6wtF8wpmz28puFXAEfPT5zrjM3wahygB//VuWEr1vTkDcNQ==
+ dependencies:
+ string-width "^2.1.1"
+ strip-ansi "^4.0.0"
+
+wrap-ansi@^7.0.0:
version "7.0.0"
resolved "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz"
integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==
@@ -6347,16 +8867,21 @@ wrappy@1:
resolved "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz"
integrity sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==
-ws@8.17.1:
- version "8.17.1"
- resolved "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz"
- integrity sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==
-
ws@^7.4.6:
version "7.5.10"
resolved "https://registry.npmjs.org/ws/-/ws-7.5.10.tgz"
integrity sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ==
+ws@^8.18.0:
+ version "8.19.0"
+ resolved "https://registry.npmjs.org/ws/-/ws-8.19.0.tgz"
+ integrity sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg==
+
+ws@8.17.1:
+ version "8.17.1"
+ resolved "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz"
+ integrity sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==
+
xdg-basedir@^5.1.0:
version "5.1.0"
resolved "https://registry.npmjs.org/xdg-basedir/-/xdg-basedir-5.1.0.tgz"
@@ -6372,7 +8897,7 @@ y18n@^5.0.5:
resolved "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz"
integrity sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==
-yaml@^2.8.0:
+yaml@^2.4.2, yaml@^2.8.0:
version "2.8.1"
resolved "https://registry.npmjs.org/yaml/-/yaml-2.8.1.tgz"
integrity sha512-lcYcMxX2PO9XMGvAJkJ3OsNMw+/7FKes7/hgerGUYWIoWu5j/+YQqcZr5JnPZWzOsEBgMbSbiSTn/dv/69Mkpw==
@@ -6423,6 +8948,14 @@ yargs@^17.7.2:
y18n "^5.0.5"
yargs-parser "^21.1.1"
+yauzl@^2.10.0:
+ version "2.10.0"
+ resolved "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz"
+ integrity sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==
+ dependencies:
+ buffer-crc32 "~0.2.3"
+ fd-slicer "~1.1.0"
+
yn@3.1.1:
version "3.1.1"
resolved "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz"
@@ -6437,3 +8970,8 @@ zksync-ethers@^6.15.0, zksync-ethers@^6.20.1:
version "6.20.1"
resolved "https://registry.npmjs.org/zksync-ethers/-/zksync-ethers-6.20.1.tgz"
integrity sha512-dq/a5mCfBwBvj123I5v9kP8xTmvTOTN+itnbei2ang0jZnLBAF2n4xeQTsiFOXweyE2XrYPMPHDY2oeVsCl0bA==
+
+zod@3.23.8:
+ version "3.23.8"
+ resolved "https://registry.npmjs.org/zod/-/zod-3.23.8.tgz"
+ integrity sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g==