diff --git a/.changeset/README.md b/.changeset/README.md
new file mode 100644
index 0000000..e5b6d8d
--- /dev/null
+++ b/.changeset/README.md
@@ -0,0 +1,8 @@
+# Changesets
+
+Hello and welcome! This folder has been automatically generated by `@changesets/cli`, a build tool that works
+with multi-package repos, or single-package repos to help you version and publish your code. You can
+find the full documentation for it [in our repository](https://github.com/changesets/changesets)
+
+We have a quick list of common questions to get you started engaging with this project in
+[our documentation](https://github.com/changesets/changesets/blob/main/docs/common-questions.md)
diff --git a/.changeset/config.json b/.changeset/config.json
new file mode 100644
index 0000000..ad6f18a
--- /dev/null
+++ b/.changeset/config.json
@@ -0,0 +1,11 @@
+{
+ "$schema": "https://unpkg.com/@changesets/config@3.1.2/schema.json",
+ "changelog": "@changesets/cli/changelog",
+ "commit": false,
+ "fixed": [],
+ "linked": [],
+ "access": "restricted",
+ "baseBranch": "main",
+ "updateInternalDependencies": "patch",
+ "ignore": []
+}
diff --git a/.env.example b/.env.example
index 9ffa0a9..eacb3e5 100644
--- a/.env.example
+++ b/.env.example
@@ -1,23 +1,81 @@
-# Private keys for each chain type
-EVM_PRIVATE_KEY=0x...
-TVM_PRIVATE_KEY=...
-SVM_PRIVATE_KEY=...
-
-# RPC endpoints (optional - defaults provided)
-EVM_RPC_URL=https://eth-mainnet.g.alchemy.com/v2/...
-TVM_RPC_URL=https://api.trongrid.io
-SVM_RPC_URL=https://api.mainnet-beta.solana.com
-
-# Quote Service Configuration (optional)
-# Use solver-v2 endpoint (takes precedence over quote service)
-SOLVER_URL=https://solver.example.com
-
-# Use preprod quote service (only applies if SOLVER_URL not set)
-# QUOTES_API_URL=any_value
-# QUOTES_PREPROD=any_value
-
-# Portal contract addresses per chain (optional - defaults provided)
-PORTAL_ADDRESS_ETH=0x...
-PORTAL_ADDRESS_OPTIMISM=0x...
-PORTAL_ADDRESS_TRON=...
-PORTAL_ADDRESS_SOLANA=...
\ No newline at end of file
+# =============================================================================
+# Routes CLI — Environment Configuration
+# =============================================================================
+# Copy this file to .env and fill in the values for your setup.
+#
+# REQUIRED variables must be set before publishing to the corresponding chain.
+# OPTIONAL variables override built-in defaults; omitting them is safe.
+#
+# Private key format summary:
+# EVM — 0x followed by 64 hexadecimal characters (66 chars total)
+# TVM — 64 hexadecimal characters WITHOUT the 0x prefix (64 chars)
+# SVM — base58 string, JSON array [1,2,...], or comma-separated bytes
+# =============================================================================
+
+
+# =============================================================================
+# REQUIRED: Private Keys
+# Set the key for each chain type you intend to publish intents on.
+# =============================================================================
+
+# EVM chains (Ethereum, Base, Optimism, Arbitrum, Polygon, BSC, Ronin, etc.)
+# Format : 0x + 64 hexadecimal characters (66 characters total)
+# Example: 0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80
+EVM_PRIVATE_KEY=
+
+# TVM chain (Tron mainnet / Tron Shasta testnet)
+# Format : 64 hexadecimal characters WITHOUT the 0x prefix (64 characters)
+# NOTE : Do NOT use your TRX wallet address (starts with T); use the raw hex key
+# Example: ac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80
+TVM_PRIVATE_KEY=
+
+# SVM chain (Solana mainnet / Solana Devnet)
+# Three accepted formats (choose one):
+#
+# Base58 string (default export from Phantom, Solana CLI `solana-keygen`):
+# SVM_PRIVATE_KEY=5K5K5K...base58EncodedKey
+#
+# JSON byte array (Solana CLI keypair file format):
+# SVM_PRIVATE_KEY=[1,2,3,4,...,64]
+#
+# Comma-separated byte values:
+# SVM_PRIVATE_KEY=1,2,3,4,...,64
+SVM_PRIVATE_KEY=
+
+
+# =============================================================================
+# OPTIONAL: RPC Endpoint Overrides
+# Each chain has a built-in default RPC URL. Set these to use a private
+# or rate-limit-free endpoint (Alchemy, Infura, QuickNode, etc.).
+# =============================================================================
+
+# EVM chains — override per chain using EVM_RPC_URL_{CHAIN_ID}:
+# EVM_RPC_URL_1=https://eth-mainnet.g.alchemy.com/v2/YOUR_KEY # Ethereum
+# EVM_RPC_URL_10=https://opt-mainnet.g.alchemy.com/v2/YOUR_KEY # Optimism
+# EVM_RPC_URL_8453=https://base-mainnet.g.alchemy.com/v2/YOUR_KEY # Base
+# EVM_RPC_URL_42161=https://arb-mainnet.g.alchemy.com/v2/YOUR_KEY # Arbitrum
+
+# Tron — default: https://api.trongrid.io
+# TVM_RPC_URL=https://api.trongrid.io
+
+# Solana — default: https://api.mainnet-beta.solana.com
+# SVM_RPC_URL=https://api.mainnet-beta.solana.com
+
+
+# =============================================================================
+# OPTIONAL: Quote Service Selection
+# Controls which pricing endpoint is used for route quotes.
+#
+# Priority order (highest first):
+# 1. SOLVER_URL — solver-v2 API at {SOLVER_URL}/api/v2/quote/reverse
+# 2. QUOTES_PREPROD — preprod service at https://quotes-preprod.eco.com/...
+# 3. (default) — production service at https://quotes.eco.com/...
+#
+# Unset all three to use the default production service.
+# =============================================================================
+
+# Solver v2 endpoint (takes precedence over all other quote service settings)
+# SOLVER_URL=https://your-solver.example.com
+
+# Set to any non-empty value to force the preprod quote service (ignored if SOLVER_URL is set)
+# QUOTES_PREPROD=true
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
new file mode 100644
index 0000000..e389172
--- /dev/null
+++ b/.github/workflows/ci.yml
@@ -0,0 +1,93 @@
+name: CI
+on:
+ push:
+ branches: [main]
+ tags:
+ - 'v*'
+ pull_request:
+ branches: [main]
+jobs:
+ quality:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+ - uses: pnpm/action-setup@v4
+ - uses: actions/setup-node@v4
+ with:
+ node-version-file: '.nvmrc'
+ cache: 'pnpm'
+ - run: pnpm install --frozen-lockfile
+ - run: pnpm lint
+ - run: pnpm typecheck
+ - run: pnpm test:coverage
+ - run: pnpm build
+ - run: pnpm audit --audit-level=moderate
+ e2e:
+ runs-on: ubuntu-latest
+ needs: quality
+ env:
+ FORK_RPC_URL: ${{ secrets.BASE_RPC_URL }}
+ FORK_BLOCK_NUMBER: '28000000'
+ steps:
+ - uses: actions/checkout@v4
+ - uses: pnpm/action-setup@v4
+ - uses: actions/setup-node@v4
+ with:
+ node-version-file: '.nvmrc'
+ cache: 'pnpm'
+ - run: pnpm install --frozen-lockfile
+ - run: pnpm build
+ - name: Run E2E tests (Anvil managed by Jest global setup/teardown)
+ run: pnpm test:e2e:ci
+ release:
+ runs-on: ubuntu-latest
+ needs: quality
+ if: startsWith(github.ref, 'refs/tags/v')
+ permissions:
+ contents: write
+ steps:
+ - uses: actions/checkout@v4
+ - uses: pnpm/action-setup@v4
+ - uses: actions/setup-node@v4
+ with:
+ node-version-file: '.nvmrc'
+ cache: 'pnpm'
+ registry-url: 'https://registry.npmjs.org'
+ - run: pnpm install --frozen-lockfile
+ - run: pnpm build
+ - name: Publish to npm
+ run: pnpm publish --no-git-checks
+ env:
+ NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
+ - name: Create GitHub Release
+ uses: softprops/action-gh-release@v2
+ with:
+ generate_release_notes: true
+ docs:
+ runs-on: ubuntu-latest
+ needs: quality
+ if: startsWith(github.ref, 'refs/tags/v')
+ permissions:
+ contents: read
+ pages: write
+ id-token: write
+ environment:
+ name: github-pages
+ url: ${{ steps.deployment.outputs.page_url }}
+ steps:
+ - uses: actions/checkout@v4
+ - uses: pnpm/action-setup@v4
+ - uses: actions/setup-node@v4
+ with:
+ node-version-file: '.nvmrc'
+ cache: 'pnpm'
+ - run: pnpm install --frozen-lockfile
+ - name: Generate API docs
+ run: pnpm docs
+ - uses: actions/configure-pages@v4
+ - uses: actions/upload-pages-artifact@v3
+ with:
+ path: docs/api
+ - name: Deploy to GitHub Pages
+ id: deployment
+ uses: actions/deploy-pages@v4
diff --git a/.github/workflows/security.yml b/.github/workflows/security.yml
new file mode 100644
index 0000000..e2c4b14
--- /dev/null
+++ b/.github/workflows/security.yml
@@ -0,0 +1,30 @@
+name: Security Scan
+on:
+ schedule:
+ - cron: '0 2 * * *'
+ push:
+ branches: [main]
+jobs:
+ audit:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+ - uses: pnpm/action-setup@v4
+ - uses: actions/setup-node@v4
+ with:
+ node-version-file: '.nvmrc'
+ cache: 'pnpm'
+ - run: pnpm install --frozen-lockfile
+ - run: pnpm audit --audit-level=high
+ secrets:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+ with:
+ fetch-depth: 0
+ - uses: trufflesecurity/trufflehog@main
+ with:
+ path: ./
+ base: ${{ github.event.repository.default_branch }}
+ head: HEAD
+ extra_args: --debug --only-verified
diff --git a/.gitignore b/.gitignore
index 92eb06c..a72d361 100644
--- a/.gitignore
+++ b/.gitignore
@@ -3,6 +3,10 @@ node_modules/
# Build output
dist/
+bundle/
+
+# Generated docs
+docs/api/
# Environment files
.env
@@ -33,3 +37,4 @@ coverage/
CLAUDE.md
thoughts/
+.worktrees/
diff --git a/.husky/pre-commit b/.husky/pre-commit
index 2312dc5..e98fb4d 100644
--- a/.husky/pre-commit
+++ b/.husky/pre-commit
@@ -1 +1,2 @@
npx lint-staged
+pnpm typecheck
diff --git a/.node-version b/.node-version
new file mode 100644
index 0000000..3c03207
--- /dev/null
+++ b/.node-version
@@ -0,0 +1 @@
+18
diff --git a/.nvmrc b/.nvmrc
new file mode 100644
index 0000000..3c03207
--- /dev/null
+++ b/.nvmrc
@@ -0,0 +1 @@
+18
diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md
new file mode 100644
index 0000000..d5461bd
--- /dev/null
+++ b/ARCHITECTURE.md
@@ -0,0 +1,1123 @@
+# Routes CLI — Architecture Document
+
+> Generated 2026-02-20. Intended as the foundation for the upcoming architecture improvement initiative.
+> For a developer how-to guide on adding new chains, see the "Quick Reference" at the bottom.
+
+---
+
+## Table of Contents
+
+1. [Overview](#1-overview)
+2. [High-Level Architecture](#2-high-level-architecture)
+3. [Core Type System](#3-core-type-system)
+4. [Universal Address System](#4-universal-address-system)
+5. [Configuration Layer](#5-configuration-layer)
+6. [Intent Construction](#6-intent-construction)
+7. [Blockchain Publisher Layer](#7-blockchain-publisher-layer)
+8. [CLI Layer](#8-cli-layer)
+9. [Security Architecture](#9-security-architecture)
+10. [Data & Control Flows](#10-data--control-flows)
+11. [Module Dependency Graph](#11-module-dependency-graph)
+12. [Build System](#12-build-system)
+13. [Supported Chains & Tokens](#13-supported-chains--tokens)
+14. [Known Issues & Improvement Opportunities](#14-known-issues--improvement-opportunities)
+15. [Quick Reference: Adding a New Chain](#15-quick-reference-adding-a-new-chain)
+
+---
+
+## 1. Overview
+
+Routes CLI is a command-line tool for publishing **cross-chain intents** on EVM, TVM (Tron), and SVM (Solana) blockchains. Built by Eco Protocol, it lets users specify a **reward** on a source chain in exchange for a solver executing a **route** on a destination chain.
+
+**Core Concepts:**
+- **Intent** = Route (what to do on destination) + Reward (what to pay on source)
+- **Route** = A series of smart contract calls to execute on the destination chain
+- **Reward** = Tokens/native currency locked on the source chain as solver incentive
+- **Universal Address** = 32-byte chain-agnostic address format used throughout all internal logic
+- **Publisher** = Chain-specific class responsible for broadcasting the intent transaction
+
+---
+
+## 2. High-Level Architecture
+
+```
+┌─────────────────────────────────────────────────────────────────────┐
+│ CLI LAYER │
+│ src/index.ts → commands/ → cli/prompts/ → utils/logger.ts │
+└───────────────────────────┬─────────────────────────────────────────┘
+ │
+ ▼
+┌─────────────────────────────────────────────────────────────────────┐
+│ INTENT SERVICE LAYER │
+│ src/core/services/intent-service.ts │
+│ ├── Quote fetching (src/core/utils/quote.ts) │
+│ └── Manual fallback route building │
+└───────────────────────────┬─────────────────────────────────────────┘
+ │
+ ┌─────────────┴──────────────┐
+ ▼ ▼
+┌─────────────────────┐ ┌────────────────────────────────────────┐
+│ ENCODING LAYER │ │ CONFIGURATION LAYER │
+│ portal-encoder.ts │ │ config/chains.ts config/tokens.ts │
+│ intent-converter │ │ config/env.ts chain-registry.ts │
+└─────────────────────┘ └────────────────────────────────────────┘
+ │
+ ▼
+┌─────────────────────────────────────────────────────────────────────┐
+│ PUBLISHER LAYER │
+│ BasePublisher (abstract) │
+│ ├── EVMPublisher → viem (PublicClient + WalletClient) │
+│ ├── TVMPublisher → TronWeb │
+│ └── SVMPublisher → @solana/web3.js + Anchor │
+└─────────────────────────────────────────────────────────────────────┘
+ │
+ ▼
+┌─────────────────────────────────────────────────────────────────────┐
+│ ADDRESS SYSTEM │
+│ AddressNormalizer ── UniversalAddress ── ChainRegistry │
+│ (normalize / denormalize at all chain boundaries) │
+└─────────────────────────────────────────────────────────────────────┘
+```
+
+**Key principle:** All internal data flows in `UniversalAddress` format. Denormalization to chain-native formats happens *only* inside publisher classes and *only* just before blockchain calls.
+
+---
+
+## 3. Core Type System
+
+### ChainType Enum
+
+```typescript
+// src/core/interfaces/intent.ts
+enum ChainType {
+ EVM = 'EVM', // Ethereum-compatible chains
+ TVM = 'TVM', // Tron Virtual Machine
+ SVM = 'SVM', // Solana Virtual Machine
+}
+```
+
+### Intent Interface
+
+The central data structure of the system. **All addresses stored as UniversalAddress.**
+
+```typescript
+interface Intent {
+ intentHash?: Hex; // Computed after creation
+ destination: bigint; // Destination chain ID
+ sourceChainId: bigint; // Source chain ID
+
+ route: {
+ salt: Hex; // Random 32-byte replay protection
+ deadline: bigint; // Unix seconds — route execution deadline
+ portal: UniversalAddress; // Portal contract on destination chain
+ nativeAmount: bigint; // Native token for route execution
+ tokens: Array<{ amount: bigint; token: UniversalAddress }>;
+ calls: Array<{
+ data: Hex;
+ target: UniversalAddress;
+ value: bigint;
+ }>;
+ };
+
+ reward: {
+ deadline: bigint; // Unix seconds — reward claiming deadline
+ creator: UniversalAddress;
+ prover: UniversalAddress; // Authorized prover/solver
+ nativeAmount: bigint;
+ tokens: Array<{ amount: bigint; token: UniversalAddress }>;
+ };
+}
+```
+
+### Blockchain Address Types
+
+```typescript
+type EvmAddress = Address; // viem Address (0x + 40 hex)
+type TronAddress = `T${string}`; // Base58, starts with 'T'
+type SvmAddress = string & { _brand: 'SvmAddress' }; // Base58 Solana pubkey
+type BlockchainAddress = EvmAddress | TronAddress | SvmAddress;
+```
+
+### Configuration Types
+
+```typescript
+interface ChainConfig {
+ id: bigint;
+ name: string;
+ env: 'production' | 'development';
+ type: ChainType;
+ rpcUrl: string;
+ portalAddress?: UniversalAddress;
+ proverAddress?: UniversalAddress;
+ nativeCurrency: { name: string; symbol: string; decimals: number };
+}
+
+interface TokenConfig {
+ symbol: string;
+ name: string;
+ decimals: number;
+ addresses: Record; // key = chainId.toString()
+}
+```
+
+### Publisher Result Types
+
+```typescript
+interface PublishResult {
+ success: boolean;
+ transactionHash?: string;
+ intentHash?: string;
+ error?: string;
+ vaultAddress?: string; // EVM vault address created on publish
+ decodedData?: unknown; // SVM program output
+}
+
+interface ValidationResult {
+ valid: boolean;
+ errors: string[];
+}
+```
+
+### Error Types
+
+```typescript
+enum ErrorCode {
+ INVALID_ADDRESS = 'INVALID_ADDRESS',
+ INVALID_PRIVATE_KEY = 'INVALID_PRIVATE_KEY',
+ INSUFFICIENT_BALANCE = 'INSUFFICIENT_BALANCE',
+ UNSUPPORTED_CHAIN = 'UNSUPPORTED_CHAIN',
+ NETWORK_ERROR = 'NETWORK_ERROR',
+ TRANSACTION_FAILED = 'TRANSACTION_FAILED',
+ CONFIGURATION_ERROR = 'CONFIGURATION_ERROR',
+ QUOTE_SERVICE_ERROR = 'QUOTE_SERVICE_ERROR',
+}
+
+class RoutesCliError extends Error {
+ constructor(
+ public readonly code: ErrorCode,
+ message: string,
+ public readonly isUserError: boolean = false,
+ public readonly cause?: unknown
+ )
+ // Factory methods: invalidAddress(), invalidPrivateKey(),
+ // insufficientBalance(), unsupportedChain(), etc.
+}
+```
+
+### Validation Schemas (Zod)
+
+| Schema | Pattern |
+|--------|---------|
+| `EvmAddressSchema` | `/^0x[a-fA-F0-9]{40}$/` |
+| `UniversalAddressSchema` | `/^0x[a-fA-F0-9]{64}$/` |
+| `TvmAddressSchema` | Base58 `T[34 chars]` or hex `0x41[40 hex]` |
+| `SvmAddressSchema` | Base58, 32–44 chars |
+| `EvmPrivateKeySchema` | `/^0x[a-fA-F0-9]{64}$/` |
+| `TvmPrivateKeySchema` | `/^[a-fA-F0-9]{64}$/` (no `0x`) |
+| `TokenAmountSchema` | Positive decimal string |
+| `ChainIdSchema` | Positive bigint |
+
+---
+
+## 4. Universal Address System
+
+### Design Rationale
+
+The intent system is *cross-chain*. A single `Intent` struct contains addresses from multiple chains (e.g. the creator on Ethereum, the portal on Solana). A single 32-byte address format eliminates `switch (chainType)` statements throughout the codebase and makes the `Intent` interface chain-agnostic.
+
+### Format
+
+```
+UniversalAddress = "0x" + 64 hex characters (32 bytes)
+
+EVM (20 bytes, zero-padded):
+ 0x000000000000000000000000742d35cc6634c0532925a3b8d65c32c2b3f6de1b
+ ^^^^^^^^^^^^^^^^^^^^^^^^ 12 zero bytes ^^^^^^^^^^^^^^^^^^^^^^^^ 20-byte EVM addr
+
+TVM (21 bytes, Tron 0x41 prefix, padded):
+ 0x0000000000000000000000004166b86ac24bd89bf2e8c33a3a6c4b63d5c4acef
+
+SVM (32 bytes, no padding):
+ 0xc69a84e3e1abff0111b65bc2daa7a8b6b2a0cec08d6c6d2c1b2f0e4cb3de5f7
+```
+
+### Normalization Rules
+
+| Chain | Native Format | Universal Encoding |
+|-------|-------------|-------------------|
+| EVM | `0x` + 40 hex (checksummed) | Left-pad to 32 bytes with zeros |
+| TVM | Base58 `T...` | Convert to hex `0x41...`, pad to 32 bytes |
+| SVM | Base58 (32-byte pubkey) | Raw 32-byte pubkey as hex |
+
+### AddressNormalizer API (`src/core/utils/address-normalizer.ts`)
+
+```typescript
+// Normalize: chain-native → UniversalAddress
+AddressNormalizer.normalize(address, chainType) // generic dispatch
+AddressNormalizer.normalizeEvm(evmAddress)
+AddressNormalizer.normalizeTvm(tronAddress)
+AddressNormalizer.normalizeSvm(solanaAddress | PublicKey)
+
+// Denormalize: UniversalAddress → chain-native
+AddressNormalizer.denormalize(universal, chainType) // generic, type-safe return
+AddressNormalizer.denormalizeToEvm(universal) → EvmAddress
+AddressNormalizer.denormalizeToTvm(universal) → TronAddress
+AddressNormalizer.denormalizeToSvm(universal) → SvmAddress
+```
+
+### Address Flow Rule (Critical)
+
+```
+User Input Internal Logic Blockchain / Display
+(chain-native) → (UniversalAddress) → (chain-native)
+ normalize() denormalize()
+```
+
+**Where to denormalize:**
+- Inside publisher `publish()` — before any RPC or contract call
+- Before displaying addresses to users in CLI output
+- When calling external APIs that expect chain-native formats
+
+**Where NOT to denormalize:**
+- In the `Intent` struct
+- In `chains.ts` / `tokens.ts` configuration
+- When passing addresses between internal functions
+
+### ChainHandler & ChainRegistry
+
+Each chain type has a self-registering handler module:
+
+```typescript
+interface ChainHandler {
+ readonly chainType: ChainType;
+ validateAddress(address: string): boolean;
+ normalize(address: string): UniversalAddress;
+ denormalize(address: UniversalAddress): BlockchainAddress;
+ getAddressFormat(): string;
+}
+
+// Singleton, populated by side-effect imports in src/index.ts:
+export const chainRegistry = new ChainRegistry();
+```
+
+> **⚠️ Import order is critical.** `chains.ts` and `tokens.ts` call `AddressNormalizer.normalize()`
+> at module load time. If chain handlers are not registered first the process throws on startup.
+> The three handler imports in `src/index.ts` must always precede all other `@/` imports.
+
+---
+
+## 5. Configuration Layer
+
+### Chain Configuration (`src/config/chains.ts`)
+
+Chains are partitioned by `env`:
+- **`production`** — loaded by default
+- **`development`** — loaded when `NODE_CHAINS_ENV=development`
+
+**Portal address env overrides:** `PORTAL_ADDRESS_ETH`, `PORTAL_ADDRESS_OPTIMISM`, `PORTAL_ADDRESS_BASE`, `PORTAL_ADDRESS_TRON`, `PORTAL_ADDRESS_SOLANA`
+
+### Token Configuration (`src/config/tokens.ts`)
+
+Tokens have addresses per chain, keyed by `chainId.toString()`. All addresses are auto-normalized to `UniversalAddress` at module load via `AddressNormalizer.normalize()`.
+
+### Environment Configuration (`src/config/env.ts`)
+
+All variables validated with Zod at startup. Unknown or malformed variables throw `RoutesCliError.configurationError`.
+
+```
+EVM_PRIVATE_KEY 0x + 64 hex chars (required for EVM)
+TVM_PRIVATE_KEY 64 hex chars, no 0x (required for TVM)
+SVM_PRIVATE_KEY Base58 | [1,2,...] | 1,2,... (required for SVM)
+
+EVM_RPC_URL optional override
+TVM_RPC_URL default: https://api.trongrid.io
+TVM_RPC_URL_2 default: https://tron.publicnode.com (fallback)
+SVM_RPC_URL default: https://api.mainnet-beta.solana.com
+SVM_RPC_URL_2 default: https://solana.publicnode.com (fallback)
+SOLVER_URL optional; enables solver-v2 quote endpoint
+QUOTES_API_URL optional; enables preprod quote endpoint
+NODE_CHAINS_ENV 'production' (default) | 'development'
+DEBUG optional; enables verbose logging + stack traces
+```
+
+### Persistent CLI Config (`~/.eco-routes/config.json`)
+
+Managed by the `config` command. Supports named profiles.
+
+```json
+{
+ "defaultSourceChain": "base",
+ "defaultDestinationChain": "optimism",
+ "defaultPrivateKeys": { "EVM": "...", "TVM": "...", "SVM": "..." },
+ "rpcUrls": { "base": "https://..." },
+ "profiles": { "mainnet": {}, "testnet": {} },
+ "currentProfile": "mainnet"
+}
+```
+
+---
+
+## 6. Intent Construction
+
+### IntentService (`src/core/services/intent-service.ts`)
+
+Central orchestrator for building intents. Coordinates quote fetching, manual fallback, user confirmation, and final reward/route assembly.
+
+```typescript
+interface IntentConfig {
+ sourceChain: ChainConfig;
+ destChain: ChainConfig;
+ creator: UniversalAddress;
+ recipient: UniversalAddress;
+ rewardToken: { address: BlockchainAddress; decimals: number; symbol?: string };
+ rewardAmount: bigint;
+ rewardAmountStr: string;
+ routeToken: { address: BlockchainAddress; decimals: number; symbol?: string };
+}
+
+interface BuildIntentResult {
+ reward: Intent['reward'];
+ encodedRoute: Hex;
+ sourcePortal: UniversalAddress;
+}
+```
+
+#### buildIntent() Flow
+
+```
+IntentService.buildIntent(config: IntentConfig)
+ │
+ ├─► Phase 1: getQuoteOrFallback()
+ │ ├─ Calls quote service (see §6.1 below)
+ │ └─ On failure: manual portal/prover prompts + manual route encoding
+ │
+ ├─► Phase 2: Construct reward object
+ │ └─ { deadline, creator, prover (UniversalAddress), nativeAmount: 0, tokens }
+ │
+ ├─► Phase 3: Display "📋 Intent Summary" table
+ │
+ ├─► Phase 4: User confirmation prompt (default: true)
+ │ └─ Throws "Publication cancelled by user" if denied
+ │
+ └─► Return: { reward, encodedRoute, sourcePortal }
+```
+
+### Quote Service (`src/core/utils/quote.ts`)
+
+**Endpoint selection (priority order):**
+1. `SOLVER_URL` → `{SOLVER_URL}/api/v2/quote/reverse`
+2. `QUOTES_API_URL` or `QUOTES_PREPROD` → `https://quotes-preprod.eco.com/api/v3/quotes/single`
+3. Default → `https://quotes.eco.com/api/v3/quotes/single`
+
+**Request:**
+```typescript
+{
+ dAppID: 'eco-routes-cli',
+ quoteRequest: {
+ sourceChainID, sourceToken, destinationChainID, destinationToken,
+ sourceAmount, funder, recipient // all in chain-native format
+ }
+}
+```
+
+**Response (both formats normalized internally):**
+```typescript
+{
+ quoteResponse: { encodedRoute, deadline, destinationAmount, estimatedFulfillTimeSec?, ... },
+ contracts: { sourcePortal: Address, prover: Address, destinationPortal: Address }
+}
+```
+
+### PortalEncoder (`src/core/utils/portal-encoder.ts`)
+
+Encodes Route and Reward structs for the target chain. Handles address denormalization internally.
+
+| Chain | Encoding | Library |
+|-------|----------|---------|
+| EVM | ABI encoding | viem `encodeAbiParameters` |
+| TVM | ABI encoding (same as EVM) | viem `encodeAbiParameters` |
+| SVM | Borsh serialization | `portalBorshCoder` |
+
+```typescript
+PortalEncoder.encode(route | reward, chainType): Hex
+PortalEncoder.decode(data, chainType, 'route' | 'reward'): Route | Reward
+PortalEncoder.isRoute(data): data is Route // type guard
+```
+
+### IntentConverter (`src/core/utils/intent-converter.ts`)
+
+Converts the universal-address-based `Intent` to EVM-native format before ABI encoding:
+
+```typescript
+toEVMIntent(intent: Intent): { route: EVMRoute; reward: EVMReward; ... }
+toRewardEVMIntent(reward: Intent['reward']): EVMReward
+toRouteEVMIntent(route: Intent['route']): EVMRoute
+```
+
+---
+
+## 7. Blockchain Publisher Layer
+
+### Class Hierarchy
+
+```
+BasePublisher (abstract)
+├── EVMPublisher viem PublicClient + WalletClient
+├── TVMPublisher TronWeb
+└── SVMPublisher @solana/web3.js Connection + Anchor Program
+```
+
+### BasePublisher Contract
+
+```typescript
+abstract class BasePublisher {
+ constructor(rpcUrl: string)
+
+ abstract publish(
+ source: bigint, destination: bigint,
+ reward: Intent['reward'], encodedRoute: string,
+ keyHandle: KeyHandle,
+ portalAddress?: UniversalAddress, proverAddress?: UniversalAddress
+ ): Promise
+
+ abstract getBalance(address: string, chainId?: bigint): Promise
+ abstract validate(reward: Intent['reward'], senderAddress: string): Promise
+
+ protected handleError(error: unknown): PublishResult
+ protected runSafely(fn: () => Promise): Promise
+ protected runPreflightChecks(sourceChainId: bigint): void // validates allowlist
+}
+```
+
+**Factory function:**
+```typescript
+createPublisher(chainType: ChainType, rpcUrl: string): BasePublisher
+```
+
+Each publisher accepts an optional client factory constructor parameter for testability:
+```typescript
+new EVMPublisher(rpcUrl, mockEvmClientFactory)
+new TVMPublisher(rpcUrl, mockTvmClientFactory)
+new SVMPublisher(rpcUrl, mockSvmClientFactory)
+```
+
+### EVMPublisher
+
+**Tech:** viem
+
+**Client strategy:**
+- Cached `PublicClient` for all reads (balance checks, allowances, validation)
+- Fresh `WalletClient` created per `publish()` call for signing
+
+**Publish sequence:**
+1. Preflight check (chain ID in allowlist)
+2. Derive account from `KeyHandle` via `privateKeyToAccount(key)`
+3. For each reward token: check balance → check allowance → `approve(portal, maxUint256)` if needed (wait 2 confirmations)
+4. Denormalize all addresses (UniversalAddress → checksummed EVM)
+5. `encodeFunctionData("publishAndFund", [destination, encodedRoute, evmReward, false])`
+6. `walletClient.sendTransaction({ to: portalAddress, data, value: nativeAmount })`
+7. Wait for receipt; parse `IntentPublished` event logs
+8. Return `{ success, transactionHash, intentHash }`
+
+**Contract functions called on Portal:**
+```solidity
+function publishAndFund(uint64 destination, bytes route, Reward reward, bool allowPartial)
+ external payable returns (bytes32 intentHash, address vault)
+```
+
+**Contract functions called on ERC-20:**
+```solidity
+function balanceOf(address account) view returns (uint256)
+function allowance(address owner, address spender) view returns (uint256)
+function approve(address spender, uint256 amount) returns (bool)
+```
+
+### TVMPublisher
+
+**Tech:** TronWeb
+
+**Key security invariant:** Private key is set on the TronWeb instance immediately before use and always cleared in a `finally` block.
+
+**Publish sequence:**
+1. Preflight check
+2. `keyHandle.use(key => { tronWeb.setPrivateKey(key); ... })` — key captured synchronously
+3. `try {` For each reward token: TRC-20 `approve(portal, amount)` → poll confirmation (20 × 4s)
+4. Denormalize addresses (UniversalAddress → Base58 Tron)
+5. Call Portal `publishAndFund(dest, encodedRoute, tvmReward, false)` with TRX `callValue`
+6. Compute `intentHash` locally via `PortalHashUtils`
+7. `} finally { tronWeb.setPrivateKey('') }`
+
+**TVM invariant:** At least one reward token is required (Tron Portal does not support native-only rewards).
+
+**Transaction confirmation polling:** 20 attempts × 4-second interval. Checks `txInfo.blockNumber && receipt.result === 'SUCCESS'`.
+
+### SVMPublisher
+
+**Tech:** `@solana/web3.js` + `@coral-xyz/anchor`
+
+**Private key formats supported:**
+1. JSON byte array: `[1, 2, 3, ...]`
+2. Comma-separated: `1, 2, 3, ...`
+3. Base58 string (default Solana/Phantom export format)
+
+**Publish sequence:**
+1. Preflight check
+2. Parse `Keypair` from `KeyHandle`
+3. Derive portal `PublicKey` from chain config
+4. Calculate `intentHash` and `routeHash` via `PortalHashUtils`
+5. `setupAnchorProgram(connection, context)` → Anchor `Program` instance
+6. `buildFundingTransaction()`:
+ - Derive vault PDA: `["vault", intentHashBytes]`
+ - Derive associated token accounts (funder ATA + vault ATA per token)
+ - `program.methods.fund({ destination, routeHash, reward, allowPartial })`
+ - Set `{ vault, payer, funder }` accounts + remaining token accounts
+7. `sendAndConfirmTransaction()`: `skipPreflight: false`, `maxRetries: 3`, poll 30× at 1s intervals until `'confirmed'`
+8. Return `{ success, transactionHash, intentHash }`
+
+**PDA Derivation:**
+```
+vault PDA: ["vault", intentHash bytes]
+proof PDA: ["proof", intentHash bytes, proverAddress bytes]
+withdrawn_marker PDA: ["withdrawn_marker", intentHash bytes]
+```
+
+**Connection config:**
+```typescript
+{
+ commitment: 'confirmed',
+ disableRetryOnRateLimit: true,
+ confirmTransactionInitialTimeout: 60000,
+}
+```
+
+### Portal Contract ABIs (`src/commons/abis/`)
+
+Key Solidity signatures:
+
+```solidity
+// Portal contract
+function publishAndFund(
+ uint64 destination,
+ bytes memory route,
+ Reward memory reward,
+ bool allowPartial
+) external payable returns (bytes32 intentHash, address vault)
+
+function fund(
+ uint64 destination,
+ bytes32 routeHash,
+ Reward memory reward,
+ bool allowPartial
+) external payable returns (bytes32 intentHash)
+
+event IntentPublished(
+ bytes32 indexed intentHash,
+ uint64 destination,
+ bytes route,
+ address indexed creator,
+ address indexed prover,
+ uint64 rewardDeadline,
+ uint256 rewardNativeAmount,
+ TokenAmount[] rewardTokens
+)
+
+// Reward struct
+struct Reward {
+ uint64 deadline;
+ address creator;
+ address prover;
+ uint256 nativeAmount;
+ TokenAmount[] tokens;
+}
+struct TokenAmount { address token; uint256 amount; }
+```
+
+---
+
+## 8. CLI Layer
+
+### Entry Point (`src/index.ts`)
+
+**Import order is critical and must not be changed:**
+
+```typescript
+// MUST come first — populates chainRegistry before chains.ts/tokens.ts load:
+import '@/blockchain/evm/evm-chain-handler';
+import '@/blockchain/tvm/tvm-chain-handler';
+import '@/blockchain/svm/svm-chain-handler';
+
+// Only then can config files be imported:
+import { listChains } from '@/config/chains';
+import { listTokens } from '@/config/tokens';
+```
+
+**Startup sequence:**
+1. Node >= 18 version check (exits with code 1 if failed)
+2. `setupGlobalErrorHandlers()` — uncaught exceptions, unhandled rejections, SIGTERM/SIGINT
+3. `ConfigService.fromEnvironment()` — loads and validates `.env`
+4. Register all chain IDs in `chainRegistry` (security allowlist)
+5. Create Commander program, register all commands
+6. `program.parse(argv)`
+
+### Commands
+
+#### `publish` — Main Command
+
+```
+routes-cli publish [options]
+
+Options:
+ -s, --source Source chain name or ID
+ -d, --destination Destination chain name or ID
+ -k, --private-key Override env private key
+ -r, --rpc Override RPC endpoint
+ --recipient Recipient on destination chain
+ --dry-run Validate only, do not broadcast
+```
+
+**Interactive publish flow:**
+
+```
+1. "🎨 Interactive Intent Publishing"
+2. PROMPT: Select source chain (list of all chains)
+3. PROMPT: Select destination chain (all except source)
+4. SECTION: "📏 Route Configuration (Destination Chain)"
+5. PROMPT: Select route token (tokens on dest chain, or custom address+decimals)
+6. SECTION: "💰 Reward Configuration (Source Chain)"
+7. PROMPT: Select reward token
+8. PROMPT: Enter reward amount → parseUnits(str, decimals) → bigint
+9. SECTION: "👤 Recipient Configuration"
+10. PROMPT: Enter recipient address → validate → normalize to UniversalAddress
+11. DERIVE: keyHandle.use(rawKey => ({
+ senderNative: getWalletAddress(chainType, rawKey),
+ publishKeyHandle: new KeyHandle(rawKey)
+ }))
+12. BUILD: IntentService.buildIntent()
+ ├── getQuoteOrFallback() with spinner
+ ├── Display "📋 Intent Summary" table
+ └── CONFIRM: "Publish this intent?" (default: true)
+13. CHECK: --dry-run → log warning and exit
+14. CREATE: createPublisher(sourceChain.type, rpcUrl)
+15. SPINNER: "Publishing intent to blockchain..."
+16. CALL: publisher.publish(source, dest, reward, encodedRoute, keyHandle, portal)
+17. DISPLAY: Transaction result table (hash, intent hash, vault address)
+```
+
+#### `status` — Check Intent Status
+
+```
+routes-cli status -c [--watch] [--json] [--verbose]
+```
+
+Queries Portal contract for `IntentFulfilled` events. Currently **EVM-only**. Watch mode polls every 10 seconds.
+
+#### `chains` — List Chains
+
+Inline command. Displays table: Name, ID, Type, Native Currency.
+
+#### `tokens` — List Tokens
+
+Inline command. Displays table: Symbol, Name, Decimals, Available Chains.
+
+#### `config` — Manage CLI Configuration
+
+Subcommands: `list`, `set [-i]`, `get `, `unset `, `reset [--force]`
+Profile management: `profile create|switch|delete|list`
+
+### Logger (`src/utils/logger.ts`)
+
+Singleton `logger` instance wrapping `ora` (spinners) and `cli-table3` (tables).
+
+**Spinner lifecycle:** `spinner(text)` → `succeed/fail/warn/info(text?)`
+
+**Display methods:**
+
+| Method | Color | Prefix |
+|--------|-------|--------|
+| `success(msg)` | Green | ✅ |
+| `error(msg)` | Red | ❌ |
+| `warning(msg)` | Yellow | ⚠️ |
+| `log(msg)` | Gray | — |
+| `title(msg)` | Bold blue | — |
+| `section(msg)` | Blue | — |
+
+**Table methods:** `displayTable(headers, rows)`, `displayTransactionResult(result)`, `displayIntentSummary(summary)`, `displayKeyValue(data, title?)`
+
+### Prompts (`src/cli/prompts/intent-prompts.ts`)
+
+All prompts use `inquirer`. Types used: `list`, `input`, `confirm`, `password`.
+
+| Prompt | Returns |
+|--------|---------|
+| `selectSourceChain` | `ChainConfig` |
+| `selectDestinationChain` | `ChainConfig` |
+| `selectToken` | `{ address, decimals, symbol? }` |
+| `configureReward` | `{ token, amount: bigint, amountStr }` |
+| `selectRecipient` | `UniversalAddress` |
+| Intent confirmation | `boolean` (default `true`) |
+| Destructive operations | `boolean` (default `false`) |
+
+### Error Handling (`src/utils/error-handler.ts`)
+
+**Error class hierarchy:**
+
+```
+RoutesCliError (code: ErrorCode, isUserError: boolean) — primary error type
+CliError generic CLI errors
+NetworkError ECONNREFUSED, ENOTFOUND, etc.
+ValidationError input validation failures
+ConfigurationError config issues
+BlockchainError chain operation failures
+```
+
+**Retry wrapper:**
+```typescript
+withRetry(fn, maxRetries=3, delayMs=1000)
+// Retries only: NetworkError, ECONNREFUSED, ETIMEDOUT, ENOTFOUND
+// Backoff: delayMs × 1.5 per attempt
+```
+
+---
+
+## 9. Security Architecture
+
+### KeyHandle — Private Key Zeroization
+
+```typescript
+// src/core/security/key-manager.ts
+class KeyHandle {
+ private buffer: Buffer; // mutable; can be zeroed
+
+ use(fn: (key: string) => T): T {
+ try { return fn(this.buffer.toString('utf8')); }
+ finally { this.buffer.fill(0); } // always zeroed, even on throw
+ }
+}
+```
+
+**Async limitation:** `use()` is synchronous. The buffer is zeroed immediately after `fn` returns — before any `await`. For async publisher flows, callers must derive all synchronous key material (account, address) inside `use()` and create a second `KeyHandle` for the async publisher:
+
+```typescript
+// src/commands/publish.ts
+const { senderNative, publishKeyHandle } = keyHandle.use(rawKey => ({
+ senderNative: getWalletAddress(sourceChain.type, rawKey),
+ publishKeyHandle: new KeyHandle(rawKey), // second handle for publisher
+}));
+```
+
+### Chain ID Allowlist
+
+All chain IDs from `CHAIN_CONFIGS` are registered at startup. `BasePublisher.runPreflightChecks()` verifies the source chain ID is allowlisted before any transaction is sent. Publishing to an unrecognized chain ID throws immediately.
+
+### TVM Key Clearing
+
+TronWeb requires the private key on the global instance object. `TVMPublisher` enforces a strict try-finally pattern:
+```typescript
+this.tronWeb.setPrivateKey(key);
+try { /* all TronWeb operations */ }
+finally { this.tronWeb.setPrivateKey(''); } // always cleared
+```
+
+### Address & Input Validation
+
+All user-supplied addresses are validated with Zod schemas before normalization. Invalid inputs throw `RoutesCliError.invalidAddress()` with a chain-specific format hint.
+
+---
+
+## 10. Data & Control Flows
+
+### Address Flow
+
+```
+User types: "0x742d35Cc6634C0532925a3b8D65C32c2b3f6dE1b"
+ │ normalize(addr, ChainType.EVM)
+ ▼
+Internal: "0x000000000000000000000000742d35cc6634c0532925a3b8d65c32c2b3f6de1b"
+ (UniversalAddress stored in Intent, passed between functions)
+ │ │
+ │ at blockchain boundary │ at display boundary
+ ▼ ▼
+ EVMPublisher.publish() logger.displayIntentSummary()
+ TVMPublisher.publish() status command output
+ SVMPublisher.publish()
+ PortalEncoder.encode()
+```
+
+### Publish Transaction Data Flow
+
+```
+CLI Prompts (chain names, token, amount, recipient)
+ │
+ └─► IntentService.buildIntent()
+ │
+ ├─► getQuote() → encodedRoute (hex), sourcePortal, proverAddress, deadline
+ │ (addresses returned in chain-native format; normalized before storing)
+ │
+ └─► reward = {
+ deadline, prover (UniversalAddress), creator (UniversalAddress),
+ nativeAmount: 0n, tokens: [{ token: UniversalAddress, amount: bigint }]
+ }
+ │
+ └─► publisher.publish(source, dest, reward, encodedRoute, keyHandle, portal)
+ │
+ ├─ EVM:
+ │ denormalize addresses in reward
+ │ viem encodeFunctionData("publishAndFund", [...])
+ │ walletClient.sendTransaction({ data, value })
+ │
+ ├─ TVM:
+ │ denormalize addresses in reward
+ │ tronWeb.contract.publishAndFund(...).send({ callValue })
+ │
+ └─ SVM:
+ setupAnchorProgram()
+ buildFundingTransaction() [vault PDA + ATAs]
+ sendAndConfirmTransaction()
+```
+
+---
+
+## 11. Module Dependency Graph
+
+Layers are strictly one-directional. Lower layers must never import from higher layers.
+
+```
+┌─────────────────────────────────────────────────────────────────┐
+│ Layer 4 — Commands (src/commands/, src/cli/, src/index.ts) │
+│ May import from: all layers below │
+└──────────────────────────────────┬──────────────────────────────┘
+ │
+┌──────────────────────────────────▼──────────────────────────────┐
+│ Layer 3 — Blockchain (src/blockchain/) │
+│ evm-publisher, tvm-publisher, svm-publisher │
+│ base-publisher, publisher-factory │
+│ evm/, tvm/, svm/ (client factories, chain handlers, PDAs) │
+│ May import from: core/, config/, commons/ │
+└──────────────────────────────────┬──────────────────────────────┘
+ │
+┌──────────────────────────────────▼──────────────────────────────┐
+│ Layer 2 — Config (src/config/) │
+│ chains.ts, tokens.ts, env.ts, config-service.ts │
+│ May import from: core/, commons/ │
+└──────────────────────────────────┬──────────────────────────────┘
+ │
+┌──────────────────────────────────▼──────────────────────────────┐
+│ Layer 1 — Core (src/core/) │
+│ interfaces/, types/, errors/, validation/ │
+│ utils/: address-normalizer, portal-encoder, intent-converter │
+│ quote, chain-detector │
+│ chain/: chain-handler.interface, chain-registry │
+│ security/: key-manager │
+│ services/: intent-service │
+│ May import from: commons/ only │
+└──────────────────────────────────┬──────────────────────────────┘
+ │
+┌──────────────────────────────────▼──────────────────────────────┐
+│ Layer 0 — Commons (src/commons/) │
+│ abis/, utils/error-handler, utils/serialize │
+│ types/portal-idl.* │
+│ No internal @/ imports │
+└─────────────────────────────────────────────────────────────────┘
+```
+
+**Cross-cutting (importable from any layer):**
+- `src/utils/logger.ts` — singleton logger
+- `src/core/errors/` — `RoutesCliError`, `ErrorCode`
+
+---
+
+## 12. Build System
+
+### TypeScript Configuration
+
+```
+target: ES2021
+module: CommonJS
+outDir: ./dist
+strict: true (all strict checks: noImplicitAny, strictNullChecks, noUnusedLocals, etc.)
+paths: @/* → src/*
+exclude: node_modules, dist, src/scripts, tests
+```
+
+### Scripts
+
+```bash
+pnpm build # tsc
+pnpm dev # tsx + tsconfig-paths (production chains)
+pnpm dev:testnet # NODE_CHAINS_ENV=development tsx
+pnpm start # node dist/index.js (production)
+pnpm clean # rm -rf dist
+pnpm test # jest (all)
+pnpm test:unit # tests/core | config | blockchain
+pnpm test:integration # tests/integration
+pnpm test:coverage # jest --coverage
+pnpm test:e2e # jest.e2e.config.ts
+pnpm typecheck # tsc --noEmit
+pnpm lint # eslint src tests
+pnpm format # prettier
+pnpm docs # typedoc → GitHub Pages
+```
+
+### Key Dependencies
+
+| Category | Package | Version | Purpose |
+|----------|---------|---------|---------|
+| EVM | `viem` | ^2.40.1 | EVM client, ABI encoding, address utils |
+| Tron | `tronweb` | ^6.2.0 | Tron client + address conversion |
+| Solana | `@solana/web3.js` | ^1.91.8 | Solana client |
+| Solana | `@solana/spl-token` | ^0.4.14 | SPL token accounts |
+| Solana | `@coral-xyz/anchor` | ^0.32.1 | Anchor IDL + program interaction |
+| CLI | `commander` | ^12.1.0 | Argument parsing |
+| CLI | `inquirer` | ^9.3.7 | Interactive prompts |
+| CLI | `ora` | ^8.2.0 | Spinners |
+| CLI | `chalk` | ^4.1.2 | Terminal colors |
+| CLI | `cli-table3` | ^0.6.5 | Formatted tables |
+| Validation | `zod` | ^4.3.6 | Schema validation |
+| Config | `dotenv` | ^16.4.5 | `.env` loading |
+| Dev | `tsx` | ^4.20.5 | TypeScript execution in dev |
+| Dev | `jest` | ^30.2.0 | Test runner |
+| Dev | `husky + lint-staged` | latest | Pre-commit hooks |
+| Dev | `typedoc` | ^0.28.17 | API documentation |
+| Dev | `@changesets/cli` | latest | Changelog management |
+
+---
+
+## 13. Supported Chains & Tokens
+
+### Production Chains
+
+| Name | ID | Type | Portal | RPC Default |
+|------|----|------|--------|-------------|
+| Ethereum | 1 | EVM | — | viem default |
+| Optimism | 10 | EVM | — | https://mainnet.optimism.io |
+| BSC | 56 | EVM | — | viem default |
+| Base | 8453 | EVM | `0x399Dbd5...` | https://mainnet.base.org |
+| Arbitrum | 42161 | EVM | — | viem default |
+| Polygon | 137 | EVM | — | viem default |
+| Ronin | 2020 | EVM | — | viem default |
+| Sonic | 146 | EVM | — | viem default |
+| Hyper EVM | 999 | EVM | — | viem default |
+| Tron | 728126428 | TVM | — | https://api.trongrid.io |
+| Solana | 1399811149 | SVM | — | https://api.mainnet-beta.solana.com |
+
+### Development / Testnet Chains
+
+| Name | ID | Type | Portal | Prover |
+|------|----|------|--------|--------|
+| Base Sepolia | 84532 | EVM | `0x06EFdb68...` | `0x9523b6c0...` |
+| Optimism Sepolia | 11155420 | EVM | `0x06EFdb68...` | `0x9523b6c0...` |
+| Plasma Testnet | 9746 | EVM | `0x06EFdb68...` | `0x9523b6c0...` |
+| Sepolia | 11155111 | EVM | `0x06EFdb68...` | `0x9523b6c0...` |
+| Tron Shasta | 2494104990 | TVM | — | — |
+| Solana Devnet | 1399811150 | SVM | — | — |
+
+### Configured Tokens
+
+| Symbol | Decimals | Chains |
+|--------|----------|--------|
+| USDC | 6 | ETH, OP, Base, Polygon, Arbitrum, HyperEVM, Ronin, Sonic, Base Sepolia, OP Sepolia, Plasma, Sepolia, Solana mainnet/devnet |
+| USDT | 6 | ETH, OP, Base, Tron mainnet/shasta, HyperEVM, Solana mainnet |
+| bUSDC | 18 | BSC |
+| bUSDT | 18 | BSC |
+
+---
+
+## 14. Known Issues & Improvement Opportunities
+
+Structural issues observed during architecture research. Intended as input for the improvement initiative.
+
+### 1. Import Order Side-Effect Dependency
+**File:** `src/index.ts`
+Chain handlers must be imported before config files due to `chainRegistry` side-effects at module load time. This is fragile and breaks silently if auto-sort tools reorder the imports.
+**Opportunity:** Explicit `initializeChainHandlers()` function called before any config access.
+
+### 2. KeyHandle Async Limitation
+**Files:** `src/core/security/key-manager.ts`, `src/commands/publish.ts`
+`KeyHandle.use()` zeroes the buffer synchronously after `fn()` returns. For async publisher flows a second `KeyHandle` must be constructed from the raw key, partially defeating the zeroization guarantee.
+**Opportunity:** An async-aware `KeyHandle` that defers zeroization until `dispose()` is explicitly called.
+
+### 3. Intent Persistence Not Implemented
+CLAUDE.md and the existing ARCHITECTURE.md describe intent storage to `~/.routes-cli/intents.json` but this is not implemented. Refunds require manual record-keeping.
+**Opportunity:** Implement the local intent store as described.
+
+### 4. IntentService Mixes Concerns
+`src/core/services/intent-service.ts` combines quote fetching, UI prompts, reward construction, and route encoding in a single class.
+**Opportunity:** Separate into `QuoteService` (pure data), `IntentBuilder` (pure construction), and `PublishOrchestrator` (UX + confirmation flow).
+
+### 5. No Standalone IntentBuilder
+CLAUDE.md references `src/builders/intent-builder.ts` with a fluent builder pattern, but the file does not exist. All intent construction is coupled to CLI prompts.
+**Opportunity:** Implement a prompt-free `IntentBuilder` to enable programmatic SDK usage.
+
+### 6. Missing Portal Addresses on Most Production Chains
+Only Base (mainnet) and the four testnet chains have portal addresses configured. All other production chains depend entirely on the quote service to supply the portal address.
+**Opportunity:** Populate portal and prover addresses for all chains where Eco Protocol is deployed.
+
+### 7. Status Command is EVM-Only
+Intent status (`IntentFulfilled` event query) only works on EVM chains.
+**Opportunity:** Add `checkStatus()` to `BasePublisher` (or a separate `StatusChecker` interface) for TVM and SVM.
+
+### 8. RPC Fallback is TVM/SVM-Only
+TVM and SVM have configurable fallback RPC URLs; EVM has none.
+**Opportunity:** Extend fallback RPC strategy uniformly to EVM chains.
+
+### 9. No Tests Despite Full Test Infrastructure
+Jest, ts-jest, E2E config, and test scripts are all set up but no test files exist. Core utilities (AddressNormalizer, PortalEncoder, IntentService) are untested.
+**Opportunity:** Start with unit tests for `AddressNormalizer` and `PortalEncoder`; add publisher integration tests using the factory mock pattern.
+
+### 10. Hardcoded dAppID in Quote Requests
+`dAppID: 'eco-routes-cli'` is hardcoded in `quote.ts`.
+**Opportunity:** Make configurable for SDK/programmatic usage.
+
+### 11. TVMPublisher Singleton TronWeb State
+TronWeb holds private key state on a shared instance. Concurrent use of `TVMPublisher` would be unsafe.
+**Opportunity:** Create TronWeb per-publish-call or add a concurrency guard.
+
+### 12. Manual Route Deadline is Hardcoded
+The quote fallback path sets route deadline to `now + 2 hours` with no way to configure it.
+**Opportunity:** Expose as a CLI flag or configurable default.
+
+---
+
+## 15. Quick Reference: Adding a New Chain
+
+Complete checklist for adding `ChainType.XVM` support:
+
+```
+1. src/core/interfaces/intent.ts
+ Add: XVM = 'XVM' to ChainType enum
+
+2. src/blockchain/xvm/xvm-chain-handler.ts (new file)
+ Implement ChainHandler interface
+ Bottom: chainRegistry.register(new XvmChainHandler())
+
+3. src/core/utils/address-normalizer.ts (optional helpers)
+ Add: static normalizeXvm(addr: XvmAddress): UniversalAddress
+ Add: static denormalizeToXvm(addr: UniversalAddress): XvmAddress
+
+4. src/blockchain/xvm/xvm-client-factory.ts (new file)
+ Define XvmClientFactory interface
+ Export: DefaultXvmClientFactory
+
+5. src/blockchain/xvm-publisher.ts (new file)
+ Extend BasePublisher
+ Implement publish(), getBalance(), validate() — all with `override` keyword
+ Denormalize addresses inside publish() before any RPC call
+
+6. src/blockchain/publisher-factory.ts
+ Add: case ChainType.XVM: return new XvmPublisher(rpcUrl, options?.xvmClientFactory)
+
+7. src/config/chains.ts
+ Add XVM chain configs with portalAddress in UniversalAddress format
+
+8. src/config/tokens.ts
+ Add token addresses for XVM chains (in UniversalAddress format)
+
+9. src/index.ts (BEFORE all other @/ imports!)
+ Add: import '@/blockchain/xvm/xvm-chain-handler'
+
+10. tests/__mocks__/xvm-client-factory.mock.ts
+ Create mock factory for integration tests
+```
+
+After step 9, `AddressNormalizer.normalize(addr, ChainType.XVM)` and
+`AddressNormalizer.denormalize(addr, ChainType.XVM)` work automatically everywhere.
diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
index 0000000..4e823d9
--- /dev/null
+++ b/CHANGELOG.md
@@ -0,0 +1,36 @@
+# Changelog
+
+All notable changes to this project will be documented in this file.
+
+This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html) and uses [Conventional Commits](https://www.conventionalcommits.org/) for commit messages. The changelog is managed with [Changesets](https://github.com/changesets/changesets).
+
+---
+
+## 1.0.0 (initial release)
+
+### Features
+
+- **Multi-chain intent publishing** — publish cross-chain intents on EVM, TVM (Tron), and SVM (Solana) with a single unified CLI.
+- **Universal Address System** — 32-byte chain-agnostic address format enabling consistent cross-chain address handling internally; chain-native formats displayed to users.
+- **Interactive publishing flow** — guided prompts for chain selection, token configuration, quote fetching, and deadline calculation.
+- **Multi-format private key support** — EVM (0x-prefixed hex), TVM (hex without 0x), SVM (base58, byte array, or comma-separated).
+- **Quote integration** — real-time route quotes for optimal intent pricing and path finding.
+- **Intent refund system** — locally tracked intents (via `~/.routes-cli/intents.json`) with refund eligibility checking.
+- **Rich CLI experience** — colored output, progress spinners (ora), formatted tables (cli-table3), and interactive prompts (inquirer).
+
+### Supported Chains
+
+- Ethereum Mainnet (EVM)
+- Base Mainnet (EVM)
+- Optimism (EVM)
+- Arbitrum One (EVM)
+- Tron Mainnet (TVM)
+- Solana Mainnet (SVM)
+
+### Architecture Highlights
+
+- Publisher abstraction (`BasePublisher`) with concrete implementations for EVM, TVM, and SVM.
+- Typed error hierarchy (`RoutesCliError`) with machine-readable error codes.
+- Runtime environment validation with Zod.
+- Dependency injection in all publisher classes for testability.
+- Chain plugin registry for self-registering chain handlers.
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
new file mode 100644
index 0000000..f6dc79d
--- /dev/null
+++ b/CONTRIBUTING.md
@@ -0,0 +1,268 @@
+# Contributing to Routes CLI
+
+Thank you for your interest in contributing to Routes CLI! This guide will help you get started.
+
+---
+
+## 1. Development Setup
+
+### Prerequisites
+
+- **Node.js** >= 18.0.0
+- **pnpm** (preferred package manager)
+- Git
+
+### Clone and install
+
+```bash
+git clone https://github.com/eco/routes-cli.git
+cd routes-cli
+pnpm install
+```
+
+### Environment configuration
+
+Copy the example environment file and fill in your private keys:
+
+```bash
+cp .env.example .env
+```
+
+Required variables in `.env`:
+
+```
+EVM_PRIVATE_KEY=0x<64-hex-chars> # EVM chains (Ethereum, Base, Optimism, …)
+TVM_PRIVATE_KEY=<64-hex-chars> # Tron (no 0x prefix)
+SVM_PRIVATE_KEY= # Solana (base58, byte array, or comma-separated)
+```
+
+> **Security note:** Never commit your `.env` file. It is already listed in `.gitignore`.
+> Use dedicated test accounts with minimal funds — never your main wallet.
+
+### Run in development mode
+
+```bash
+pnpm dev publish # Interactive intent publishing
+pnpm dev chains # List supported chains
+pnpm dev tokens # List configured tokens
+```
+
+### Build
+
+```bash
+pnpm build # Compiles TypeScript to dist/
+pnpm clean # Removes dist/
+```
+
+---
+
+## 2. Branch Naming Convention
+
+| Prefix | When to use |
+|--------|-------------|
+| `feat/` | New feature or capability |
+| `fix/` | Bug fix |
+| `docs/` | Documentation only changes |
+| `refactor/` | Code restructuring with no behaviour change |
+| `test/` | Adding or improving tests |
+
+**Examples:**
+
+```
+feat/add-polygon-chain
+fix/tvm-token-approval-loop
+docs/update-contributing-guide
+refactor/extract-quote-service
+test/evm-publisher-unit-tests
+```
+
+Branch names should be lowercase and use hyphens, not underscores or spaces.
+
+---
+
+## 3. Commit Message Format
+
+Routes CLI follows the [Conventional Commits](https://www.conventionalcommits.org/) specification.
+
+```
+():
+
+[optional body — explain the *why*, not the *what*]
+```
+
+### Types
+
+| Type | Use for |
+|------|---------|
+| `feat` | A new feature |
+| `fix` | A bug fix |
+| `chore` | Build scripts, dependency updates, tooling |
+| `docs` | Documentation changes only |
+| `refactor` | Restructuring code without changing behaviour |
+| `test` | Adding or fixing tests |
+| `perf` | Performance improvements |
+
+### Scope (optional)
+
+Use the affected module or file area: `evm-publisher`, `config`, `svm`, `cli`, `address-normalizer`, etc.
+
+### Examples
+
+```
+feat(config): add Polygon chain support
+fix(tvm-publisher): loop over all reward tokens for approval
+docs(readme): add Solana private key format examples
+test(address-normalizer): add round-trip tests for all chain types
+chore(deps): bump viem to 2.x
+```
+
+---
+
+## 4. Pull Request Checklist
+
+Before opening a PR, verify all of the following:
+
+- [ ] **Tests pass** — `pnpm test` exits with zero errors
+- [ ] **TypeScript compiles** — `pnpm build` succeeds with no type errors
+- [ ] **Lint passes** — `pnpm lint` reports zero errors
+- [ ] **No regressions** — existing tests are not deleted or weakened
+- [ ] **Docs updated** — README, ARCHITECTURE.md, or inline JSDoc updated where relevant
+- [ ] **Commit messages** follow Conventional Commits format
+- [ ] **`.env` not committed** — double-check `git status` before pushing
+
+For new features, also ensure:
+
+- [ ] A test is added that would fail without the change
+- [ ] Edge cases and error paths are covered
+- [ ] The new chain/token/feature is documented in relevant config files
+
+---
+
+## 5. Testing Guide
+
+### Running tests
+
+```bash
+pnpm test # Run all unit + integration tests
+pnpm test --watch # Watch mode for active development
+pnpm test # Run tests matching a file/name pattern
+pnpm test:e2e # Run E2E tests (requires Docker and BASE_RPC_URL)
+```
+
+### Test structure
+
+```
+tests/
+├── unit/ # Pure unit tests (no I/O)
+│ ├── address-normalizer.test.ts
+│ ├── chain-detector.test.ts
+│ ├── intent-converter.test.ts
+│ └── portal-encoder.test.ts
+├── blockchain/ # Publisher tests with mocked clients
+│ └── evm-publisher.integration.test.ts
+├── integration/ # Full-pipeline integration tests
+│ └── intent-publishing.test.ts
+├── config/ # Config loading integration tests
+│ ├── chains.test.ts
+│ └── tokens.test.ts
+├── e2e/ # End-to-end tests against Anvil fork
+│ └── evm-publish.e2e.test.ts
+└── __mocks__/ # Shared mock factories
+```
+
+### Writing new tests
+
+1. **Unit tests** go in `tests/unit/` — mock all I/O, test one function at a time.
+2. **Integration tests** go in `tests/integration/` or `tests/blockchain/` — use injected mock clients via the factory pattern (see `PublisherFactory` and `tests/__mocks__/`).
+3. **E2E tests** go in `tests/e2e/` — use a real Anvil fork; add your test to the existing file or create a new `*.e2e.test.ts`.
+
+**Key conventions:**
+
+- Use `beforeEach(() => jest.clearAllMocks())` to isolate per-test mock state.
+- Prefer `mockResolvedValueOnce` over `mockResolvedValue` to catch unexpected extra calls.
+- Use `expect.objectContaining(...)` for partial assertions on large objects.
+- Fixtures should use real, well-known addresses (e.g. vitalik.eth, USDC contract) rather than made-up values.
+- Universal Addresses (32-byte `0x` + 64-hex) must be used in all test fixtures except where testing chain-native formats.
+
+### Test configuration files
+
+- `jest.config.ts` — unit + integration tests (excludes `tests/e2e/`)
+- `jest.e2e.config.ts` — E2E tests only (no viem mock, longer timeout, single worker)
+
+---
+
+## 6. Code Review Process
+
+### Submitting a PR
+
+1. Open a PR against the `main` branch.
+2. Fill in the PR description: what changed, why, and how to test it.
+3. Link any related issues.
+4. Ensure all CI checks pass before requesting review.
+
+### Review timeline
+
+- Initial review within **2 business days** for small PRs (< 200 lines changed).
+- Larger or architectural PRs may take longer; consider splitting into smaller PRs.
+- If you have not received a review after 3 business days, ping the maintainer on the PR.
+
+### What reviewers look for
+
+- Correctness and edge-case coverage.
+- Adherence to the Universal Address pattern (normalize on input, denormalize only at boundaries).
+- No global state mutation (see `ConfigService` pattern in `src/config/config-service.ts`).
+- Publisher classes receive Universal Addresses and denormalize internally before blockchain calls.
+- New chains/tokens follow existing patterns in `src/config/chains.ts` and `src/config/tokens.ts`.
+- Tests that actually exercise the new or fixed code path.
+
+### Addressing review feedback
+
+- Push new commits to the same branch; do not force-push unless explicitly asked.
+- Mark conversations as resolved after addressing them.
+- If you disagree with feedback, explain your reasoning — discussion is welcome.
+
+---
+
+## 7. Release Process
+
+Routes CLI uses [Changesets](https://github.com/changesets/changesets) to manage versioning and `CHANGELOG.md` updates.
+
+### For contributors — describe your change
+
+When your PR includes a user-facing change, add a changeset file:
+
+```bash
+pnpm changeset
+```
+
+The CLI will prompt you to:
+1. Select the bump type — `major` (breaking), `minor` (new feature), or `patch` (bug fix).
+2. Write a short summary of the change for the CHANGELOG.
+
+This creates a `.changeset/.md` file. **Commit this file with your PR.**
+
+> **When to skip changesets:** Pure documentation, test, or CI changes that have no impact on
+> CLI behaviour or the published package do not need a changeset file.
+
+### For maintainers — cutting a release
+
+1. Merge all PRs for the release into `main`. Each PR should include its `.changeset/*.md` file.
+2. Run the changeset version command to consume the changeset files and bump `package.json`:
+ ```bash
+ pnpm changeset version
+ ```
+ This updates `package.json`, aggregates all changeset summaries into `CHANGELOG.md`, and removes the consumed `.changeset/*.md` files.
+3. Review the diff — confirm `package.json` version and `CHANGELOG.md` look correct.
+4. Commit and push:
+ ```bash
+ git add package.json CHANGELOG.md pnpm-lock.yaml
+ git commit -m "chore(release): v$(node -p 'require(\"./package.json\").version')"
+ git push origin main
+ ```
+5. Tag the release — CI triggers the `release` job on tag push:
+ ```bash
+ VERSION=$(node -p 'require("./package.json").version')
+ git tag "v$VERSION"
+ git push origin "v$VERSION"
+ ```
+6. CI will publish to npm (`NPM_TOKEN` secret required) and create a GitHub Release automatically.
diff --git a/README.md b/README.md
index 92806ed..d18fe5b 100644
--- a/README.md
+++ b/README.md
@@ -1,320 +1,172 @@
-# Routes CLI - Intent Publisher
-
-[](https://opensource.org/licenses/MIT)
-[](https://nodejs.org/)
-
-A powerful command-line interface for creating and publishing cross-chain intents on EVM, TVM (Tron), and SVM (Solana) blockchains. Built by Eco Protocol for seamless multi-chain interactions.
-
-## 🌟 Key Features
-
-- **🌍 Multi-chain Support**: Seamlessly publish intents across EVM, Tron (TVM), and Solana (SVM) chains
-- **💸 Quote Integration**: Real-time route quotes for optimal pricing and path finding
-- **🎯 Interactive Wizards**: Intuitive step-by-step guides for intent creation and publishing
-- **🔐 Secure Key Management**: Environment-based private key storage with multi-format support
-- **📊 Rich CLI Experience**: Beautiful tables, spinners, and colored output for better UX
-- **⚡ Smart Defaults**: Automatic deadline calculation and intelligent configuration
-- **🔌 Extensible Architecture**: Easy integration of new chains and tokens
-- **📦 Standalone Operation**: Zero external service dependencies
-
-## 📋 Prerequisites
-
-- Node.js >= 18.0.0
-- pnpm (recommended) or npm
-- Private keys for the chains you want to use
-
-## 📦 Installation
-
-### Clone and Build
+
+
+
-```bash
-git clone https://github.com/eco/routes-cli.git
-cd routes-cli
-pnpm install
-pnpm build
-```
+Routes CLI
+Send assets across chains with a single command.
-### Global Installation (Optional)
+
+
+
+
-```bash
-pnpm link
-# Now you can use 'eco-routes-cli' globally
-```
+Routes CLI is a terminal tool for publishing **cross-chain intents** built on the [Eco Routes Protocol](https://github.com/eco/eco-routes/). You lock a reward on your source chain; competitive solvers race to deliver the result on your destination chain. Supports EVM (Ethereum, Base, Optimism, and more), and Solana.
-## Configuration
+---
-1. Copy the example environment file:
-```bash
-cp .env.example .env
-```
+## See It In Action
-2. Add your private keys to `.env`:
-```env
-# Private keys for each chain type
-EVM_PRIVATE_KEY=0x...
-TVM_PRIVATE_KEY=...
-SVM_PRIVATE_KEY=...
-
-# Optional: Custom RPC endpoints
-EVM_RPC_URL=https://eth-mainnet.g.alchemy.com/v2/...
-TVM_RPC_URL=https://api.trongrid.io
-SVM_RPC_URL=https://api.mainnet-beta.solana.com
-
-# Optional: Portal contract addresses
-PORTAL_ADDRESS_ETH=0x...
-PORTAL_ADDRESS_OPTIMISM=0x...
-PORTAL_ADDRESS_TRON=T...
-PORTAL_ADDRESS_SOLANA=...
```
+$ eco-routes-cli publish --private-key 0xYOUR_PRIVATE_KEY
-## 🚀 Quick Start
+? Select source chain: Base
+? Select destination chain: Optimism
-1. **Set up your environment:**
- ```bash
- cp .env.example .env
- # Edit .env with your private keys
- ```
+? Route token: USDC
+? Route amount: 100
-2. **Publish your first intent:**
- ```bash
- pnpm dev publish
- # Follow the interactive prompts
- ```
+? Reward token: USDC
+? Reward amount: 101
-3. **View available chains:**
- ```bash
- pnpm dev chains
- ```
+┌─────────────────────────────────────────────┐
+│ Intent Summary │
+├──────────────────┬──────────────────────────┤
+│ Source chain │ Base (8453) │
+│ Destination │ Optimism (10) │
+│ Route │ 100 USDC → Optimism │
+│ Reward │ 101 USDC on Base │
+│ Route deadline │ 2026-02-26 14:00 UTC │
+│ Reward deadline │ 2026-02-26 15:00 UTC │
+└──────────────────┴──────────────────────────┘
-## 📖 Usage Guide
+? Confirm and publish intent? Yes
-### 🎯 Interactive Publishing (Recommended)
-
-The simplest and most user-friendly way to publish intents:
-
-```bash
-pnpm dev publish
+⠋ Publishing intent...
+✔ Intent published — tx: 0xabc123...def456
```
-#### Publishing Flow
-
-1. **🔗 Chain Selection**
- - Select source chain (where rewards come from)
- - Select destination chain (where route executes)
- - Automatic quote fetching for optimal routing
-
-2. **💰 Token Configuration**
- - **Route Token**: Choose destination chain token (native or ERC20/TRC20/SPL)
- - **Route Amount**: Specify amount to transfer on destination
- - **Reward Token**: Choose source chain token for prover reward
- - **Reward Amount**: Specify reward amount for proof submission
-
-3. **⚙️ Automatic Configuration**
- - Creator address derived from your wallet
- - Prover address from chain configuration
- - Portal address from destination chain
- - Smart deadline calculation:
- - Route deadline: 2 hours from now
- - Reward deadline: 3 hours from now
-
-4. **✅ Review & Confirm**
- - Display complete intent details
- - Show estimated gas costs
- - Confirm before blockchain submission
+---
-### 🔄 Semi-Interactive Publishing
+## Quick Start
-Specify chains via command line, configure tokens interactively:
+**1. Install globally**
```bash
-# Mainnet examples
-pnpm dev publish --source ethereum --destination optimism
-pnpm dev publish --source tron --destination base
-pnpm dev publish --source solana --destination ethereum
-
-# Testnet examples
-pnpm dev publish --source base-sepolia --destination optimism-sepolia
-pnpm dev publish --source tron-shasta --destination base-sepolia
+npm i -g eco-routes-cli
```
-### ⚙️ Command Options
+**2. Publish your first EVM intent**
-| Option | Alias | Description | Example |
-|--------|-------|-------------|----------|
-| `--source` | `-s` | Source chain name or ID | `ethereum`, `1` |
-| `--destination` | `-d` | Destination chain name or ID | `optimism`, `10` |
-| `--verbose` | `-v` | Show detailed output | |
-
-### 📊 Information Commands
-
-#### List Supported Chains
```bash
-pnpm dev chains
-# Output: Formatted table with chain names, IDs, types, and native currencies
+eco-routes-cli publish --private-key 0xYOUR_PRIVATE_KEY
```
-#### List Configured Tokens
-```bash
-pnpm dev tokens
-# Output: Table showing token symbols, names, decimals, and chain availability
-```
+Follow the prompts to select source chain (e.g. Base), destination chain (e.g. Optimism), token, and amounts. Done.
-## Intent Structure
-
-An intent consists of two main parts:
-
-### Route
-- `salt`: Random 32-byte hex value
-- `deadline`: Unix timestamp for route expiration
-- `portal`: Portal contract address on destination chain
-- `nativeAmount`: Native token amount to transfer
-- `tokens`: Array of token transfers
-- `calls`: Array of contract calls to execute
-
-### Reward
-- `deadline`: Unix timestamp for reward claim deadline
-- `creator`: Address that created the intent
-- `prover`: Prover contract address
-- `nativeAmount`: Native token reward amount
-- `tokens`: Array of token rewards
-
-## 🔧 Customization & Extension
-
-## Adding New Tokens
-
-Edit `src/config/tokens.ts`:
-
-```typescript
-export const TOKEN_CONFIGS: Record = {
- // ... existing tokens
-
- MYTOKEN: {
- symbol: 'MTK',
- name: 'My Token',
- decimals: 18,
- addresses: {
- ethereum: AddressNormalizer.normalize('0x...', ChainType.EVM),
- optimism: AddressNormalizer.normalize('0x...', ChainType.EVM),
- },
- },
-};
-```
+> Tip: pass `--source` and `--destination` to skip chain selection prompts.
-## 🛠️ Development
+Prefer not to pass keys inline? Copy `.env.example` to `.env` and set `EVM_PRIVATE_KEY` (or `SVM_PRIVATE_KEY` for Solana) — the CLI will pick it up automatically.
-### Available Scripts
+---
-| Script | Description |
-|--------|-------------|
-| `pnpm build` | Compile TypeScript to JavaScript |
-| `pnpm dev ` | Run in development mode with ts-node |
-| `pnpm start ` | Run compiled version |
-| `pnpm clean` | Remove build artifacts |
+## What Is an Intent?
-### Project Structure
+An intent describes **what you want to happen on the destination chain** and **what reward you're offering** to whoever makes it happen. Solvers — independent actors monitoring the protocol — race to fulfill your intent and claim the reward. The faster the solver, the better the deal for everyone.
-```
-routes-cli/
-├── src/
-│ ├── blockchain/ # Chain-specific implementations
-│ ├── builders/ # Intent builder patterns
-│ ├── commands/ # CLI command implementations
-│ ├── config/ # Chain and token configurations
-│ ├── core/ # Core types and utilities
-│ ├── scripts/ # Standalone scripts
-│ └── utils/ # Helper utilities
-├── dist/ # Compiled output
-├── .env.example # Environment template
-└── package.json # Project dependencies
-```
+This is powered by the [Eco Routes Protocol](https://github.com/eco/eco-routes/). For the full picture of how intents are encoded, published, and proven, see [ARCHITECTURE.md](./ARCHITECTURE.md).
-## 🏗️ Architecture
+---
-### Core Concepts
+## Command Reference
+
+| Command | Description |
+|---------|-------------|
+| `eco-routes-cli publish` | Interactive intent publishing wizard |
+| `eco-routes-cli publish --source --destination ` | Skip chain selection prompts |
+| `eco-routes-cli chains` | List all supported chains |
+| `eco-routes-cli tokens` | List all configured tokens |
+
+**`publish` flags:**
+
+| Flag | Alias | Description |
+|------|-------|-------------|
+| `--source` | `-s` | Source chain name or ID |
+| `--destination` | `-d` | Destination chain name or ID |
+| `--private-key` | `-k` | EVM private key (overrides `EVM_PRIVATE_KEY` env) |
+| `--private-key-svm` | | SVM private key (overrides `SVM_PRIVATE_KEY` env) |
+| `--recipient` | | Recipient address on the destination chain |
+| `--portal-address` | | Portal contract address on the source chain |
+| `--prover-address` | | Prover contract address on the source chain |
+| `--rpc` | `-r` | RPC URL override for the source chain |
+| `--dry-run` | | Validate and preview without broadcasting |
+| `--watch` | `-w` | Watch for fulfillment after publishing |
+
+**Private key formats:**
+
+| Chain | Format | Example |
+|-------|--------|---------|
+| EVM | `0x` + 64 hex chars | `0xac09...ff80` |
+| Tron | 64 hex chars, no `0x` | `ac09...ff80` |
+| Solana | Base58 | `5Jd7F...` |
+| Solana | JSON byte array | `[1,2,3,...]` |
+| Solana | Comma-separated bytes | `1,2,3,...` |
-- **UniversalAddress**: Chain-agnostic 32-byte address representation enabling cross-chain compatibility
-- **PortalEncoder**: Specialized encoder for intent data across different blockchain types
-- **AddressNormalizer**: Bidirectional converter between chain-native and universal address formats
-- **IntentBuilder**: Fluent builder pattern for constructing complex intents programmatically
-- **ChainTypeDetector**: Automatic chain type detection from configuration
-- **Quote System**: Integration with routing protocols for optimal path finding
+---
-### Design Principles
+## Configuration Reference
-1. **Chain Abstraction**: Uniform interface across different blockchain types
-2. **Type Safety**: Full TypeScript support with strict typing
-3. **Modularity**: Pluggable architecture for easy extension
-4. **User Experience**: Interactive wizards with rich CLI feedback
+Copy `.env.example` to `.env`. All variables except the private keys are optional.
-## 🔑 Private Key Formats
+| Variable | Required | Description |
+|----------|----------|-------------|
+| `EVM_PRIVATE_KEY` | For EVM chains | EVM wallet private key (`0x…`) |
+| `SVM_PRIVATE_KEY` | For Solana | Solana wallet key (base58, array, or bytes) |
+| `EVM_RPC_URL` | No | Override RPC for all EVM chains |
+| `SVM_RPC_URL` | No | Override Solana RPC (default: mainnet-beta) |
+| `SOLVER_URL` | No | Use a custom solver endpoint for quotes |
+| `QUOTES_PREPROD` | No | Force preprod quote service (set to `true`) |
+| `PORTAL_ADDRESS_ETH` | No | Override Ethereum portal contract |
+| `PORTAL_ADDRESS_BASE` | No | Override Base portal contract |
+| `PORTAL_ADDRESS_OPTIMISM` | No | Override Optimism portal contract |
+| `PORTAL_ADDRESS_SOLANA` | No | Override Solana portal contract |
-| Chain Type | Format | Example |
-|------------|--------|----------|
-| **EVM** | Hex with prefix | `0x1234...` (64 hex chars) |
-| **Tron** | Hex without prefix | `1234...` (64 hex chars) |
-| **Solana** | Base58 | `5Jd7F...` |
-| | Byte array | `[1,2,3,...]` |
-| | Comma-separated | `1,2,3,...` |
+See `.env.example` for the complete list of portal address overrides.
-## 🚨 Troubleshooting
+---
-### Common Issues
+## Troubleshooting
-| Issue | Solution |
-|-------|----------|
-| `Invalid address format` | Check address matches chain type requirements |
-| `Insufficient balance` | Ensure wallet has enough tokens and gas |
-| `Chain not found` | Verify chain name/ID in supported list |
-| `RPC timeout` | Check network connection or use custom RPC |
-| `Private key error` | Verify key format matches chain type |
-| `Quote unavailable` | Check source/destination pair compatibility |
+| Issue | Fix |
+|-------|-----|
+| `Private key error` | Check the format matches the chain type — see Private key formats above |
+| `Chain not found` | Run `eco-routes-cli chains` to verify the exact chain name or ID |
+| `Insufficient balance` | Ensure your wallet has the reward token plus gas on the source chain |
+| `Quote unavailable` | Not all chain pairs have live routes yet — try a different pair |
+| `RPC timeout` | Set a custom RPC endpoint via `EVM_RPC_URL` / `TVM_RPC_URL` / `SVM_RPC_URL` |
-### Debug Mode
+Enable verbose output for more detail:
```bash
-# Enable verbose logging
-export DEBUG=eco-routes-cli:*
-pnpm dev publish --verbose
+DEBUG=* eco-routes-cli publish
```
-## 🔒 Security Best Practices
-
-1. **Never commit `.env` files** - Add to `.gitignore`
-2. **Use environment variables** - Don't hardcode private keys
-3. **Hardware wallets recommended** - For production environments
-4. **Validate before publishing** - Use `--dry-run` flag
-5. **Audit intent details** - Review all parameters before confirmation
-6. **Secure RPC endpoints** - Use authenticated endpoints when possible
-7. **Rotate keys regularly** - Especially for automated systems
-
-## 🤝 Contributing
-
-Contributions are welcome! Please follow these steps:
-
-1. Fork the repository
-2. Create a feature branch (`git checkout -b feature/amazing-feature`)
-3. Commit your changes (`git commit -m 'feat: add amazing feature'`)
-4. Push to the branch (`git push origin feature/amazing-feature`)
-5. Open a Pull Request
-
-## 📄 License
+---
-This project is licensed under the MIT License.
+## Going Deeper
-## 🙏 Acknowledgments
+- [ARCHITECTURE.md](./ARCHITECTURE.md) — How intents work, the Universal Address system, publisher internals
+- [CONTRIBUTING.md](./CONTRIBUTING.md) — Dev setup, adding chains and tokens, PR process
+- [GitHub Issues](https://github.com/eco/routes-cli/issues) — Bug reports and feature requests
-- Built with [Viem](https://viem.sh/) for EVM interactions
-- [TronWeb](https://tronweb.network/) for Tron support
-- [Solana Web3.js](https://solana-labs.github.io/solana-web3.js/) for Solana integration
-- [Commander.js](https://github.com/tj/commander.js/) for CLI framework
-- [Inquirer.js](https://github.com/SBoudrias/Inquirer.js/) for interactive prompts
+---
-## 📞 Support
+## License
-- **Documentation**: [Full API Reference](https://docs.eco.org/routes-cli)
-- **Issues**: [GitHub Issues](https://github.com/eco-protocol/routes-cli/issues)
+MIT © [Eco Protocol](https://eco.com)
----
+Built with [viem](https://viem.sh/), and [@solana/web3.js](https://solana-labs.github.io/solana-web3.js/).
+Powered by the [Eco Routes Protocol](https://github.com/eco/eco-routes/).
- Made with ❤️ by Eco Protocol
-
\ No newline at end of file
+
+
diff --git a/SECURITY.md b/SECURITY.md
new file mode 100644
index 0000000..207f126
--- /dev/null
+++ b/SECURITY.md
@@ -0,0 +1,204 @@
+# Security Policy
+
+## Supported Versions
+
+The following versions of eco-routes-cli receive security patches:
+
+| Version | Supported |
+|---------|--------------------|
+| 1.x | ✅ Active support |
+| < 1.0 | ❌ End of life |
+
+Security fixes are released as patch versions (e.g. `1.0.1`) and announced via
+[GitHub Releases](https://github.com/eco-protocol/routes-cli/releases).
+
+---
+
+## Reporting a Vulnerability
+
+**Do not open a public GitHub issue for security vulnerabilities.**
+
+### Option A — GitHub Security Advisory (preferred)
+
+Use GitHub's private disclosure flow:
+
+1. Go to the repository on GitHub
+2. Click **Security** → **Advisories** → **Report a vulnerability**
+3. Fill in the vulnerability details (description, affected versions, reproduction steps)
+4. Submit — the maintainers will respond within **5 business days**
+
+### Option B — Email
+
+Send a report to the Eco Protocol security team:
+
+```
+security@eco.org
+```
+
+Include:
+- A description of the vulnerability
+- Steps to reproduce
+- Potential impact assessment
+- Any suggested mitigations
+
+You will receive an acknowledgement within **2 business days**.
+
+### What to expect
+
+- We will confirm receipt and begin investigation within 5 business days
+- We aim to release a patch within 30 days for critical vulnerabilities
+- You will be credited in the release notes unless you prefer otherwise
+- We do not offer a bug bounty program at this time
+
+---
+
+## Security Model
+
+### Private Key Handling
+
+eco-routes-cli requires private keys to sign blockchain transactions. Here is exactly how
+keys are handled at each stage:
+
+1. **Load** — Private keys are read from environment variables (`EVM_PRIVATE_KEY`, etc.)
+ into process memory when the CLI starts. They are never written to disk by the application.
+
+2. **Pass** — The key string is passed as a function argument to the relevant publisher
+ (`EvmPublisher`, `TvmPublisher`, or `SvmPublisher`). It is not stored in any global or
+ class-level field between calls.
+
+3. **Sign** — The key is handed to the chain-specific library (`viem`, `TronWeb`, or
+ `@solana/web3.js`) to sign the transaction. For TVM, the key is loaded into the `TronWeb`
+ instance immediately before signing and cleared from the instance immediately after (via a
+ `finally` block calling `this.tronWeb.setPrivateKey('')`).
+
+4. **Discard** — After signing, the function scope ends and the string is eligible for garbage
+ collection. No copy is retained.
+
+**Important limitations:** JavaScript strings are immutable — the runtime may retain a copy in
+memory until the garbage collector runs. For high-security deployments, run the CLI in a
+dedicated process and terminate it immediately after use.
+
+### What is never persisted
+
+- Private keys are **never** written to `~/.routes-cli/` or any other disk location
+- The local intent store (`~/.routes-cli/intents.json`) records intent metadata only
+ (hashes, chain IDs, reward amounts) — never private keys or wallet addresses derived from them
+- Log output never includes private key material
+
+### RPC Endpoints
+
+The CLI connects to RPC endpoints to submit transactions. By default:
+
+- **EVM**: Public RPC (configurable via `EVM_RPC_URL`)
+- **TVM**: `https://api.trongrid.io` (configurable via `TVM_RPC_URL`)
+- **SVM**: `https://api.mainnet-beta.solana.com` (configurable via `SVM_RPC_URL`)
+
+Use a private RPC endpoint (`EVM_RPC_URL`, etc.) if you are concerned about transaction
+metadata leaking to public node operators.
+
+---
+
+## Private Key Format Reference
+
+### EVM Chains (Ethereum, Optimism, Base, Arbitrum, etc.)
+
+```
+Format: 0x followed by exactly 64 hexadecimal characters
+Example: 0xabc123...def456 (0x + 64 hex chars = 66 chars total)
+Regex: ^0x[a-fA-F0-9]{64}$
+```
+
+Set in `.env`:
+```bash
+EVM_PRIVATE_KEY=0x
+```
+
+### TVM (Tron)
+
+```
+Format: Exactly 64 hexadecimal characters — NO 0x prefix
+Example: abc123...def456 (64 hex chars, no prefix)
+Regex: ^[a-fA-F0-9]{64}$
+```
+
+Set in `.env`:
+```bash
+TVM_PRIVATE_KEY=
+```
+
+Note: This is the raw private key, not the WIF-encoded format used by some Tron wallets.
+
+### SVM (Solana)
+
+Solana private keys can be provided in any of three formats:
+
+**Base58 encoded keypair** (58 characters, standard export from Phantom / Solflare):
+```bash
+SVM_PRIVATE_KEY=5Kb8kLf9zgWQnogidDA76MzPL6TsZZY36hWXMssSzNydYXYB...
+```
+
+**Byte array** (JSON array of 64 numbers, standard format from `solana-keygen`):
+```bash
+SVM_PRIVATE_KEY=[12,34,56,...,255] # 64 comma-separated numbers inside brackets
+```
+
+**Comma-separated bytes** (same as array, without brackets):
+```bash
+SVM_PRIVATE_KEY=12,34,56,...,255 # 64 comma-separated numbers
+```
+
+---
+
+## Best Practices for Users
+
+### Use dedicated keys
+
+Never use a personal wallet key with this CLI. Create a dedicated wallet that holds only
+the tokens needed for publishing:
+
+```bash
+# EVM: create a fresh key with cast
+cast wallet new
+
+# Solana: create a fresh keypair
+solana-keygen new --outfile ~/.config/solana/routes-cli.json
+```
+
+### Keep .env out of version control
+
+Confirm your `.env` file is ignored before committing:
+
+```bash
+git check-ignore -v .env # should print: .gitignore:N:.env
+```
+
+If it is not ignored, add it:
+```bash
+echo '.env' >> .gitignore
+```
+
+### Use a hardware wallet for large amounts
+
+For production use with significant token amounts, consider a hardware wallet integration.
+The CLI currently accepts software keys only — hardware wallet support is on the roadmap.
+
+### Rotate keys after any suspected exposure
+
+If you believe a key has been exposed (e.g., accidentally committed, shown in a log):
+
+1. Move all tokens off the compromised wallet immediately
+2. Generate a new key
+3. Update your `.env` with the new key
+4. If the key was in git history, follow the instructions in [TASK-001](IMPROVEMENT_PLAN.md)
+ to rewrite history and notify collaborators to re-clone
+
+### Keep dependencies up to date
+
+Run `pnpm audit` regularly to check for known vulnerabilities in dependencies:
+
+```bash
+pnpm audit --audit-level=high
+```
+
+The CI pipeline runs `pnpm audit` on every push to `main` and on a daily schedule via
+`.github/workflows/security.yml`.
diff --git a/docs/plans/2026-02-20-architecture-improvement-design.md b/docs/plans/2026-02-20-architecture-improvement-design.md
new file mode 100644
index 0000000..950e1d1
--- /dev/null
+++ b/docs/plans/2026-02-20-architecture-improvement-design.md
@@ -0,0 +1,462 @@
+# Architecture Improvement Design
+
+**Date:** 2026-02-20
+**Scope:** Transformative redesign — all layers, CLI only (no SDK surface)
+**Approach:** Service Container via NestJS standalone + `nestjs-commander`
+
+---
+
+## Background
+
+The current architecture was researched and documented in `ARCHITECTURE.md`. It identified 12 structural issues across four dimensions:
+
+- **Correctness / reliability** — missing intent persistence, EVM-only status, missing portal addresses on most production chains
+- **Developer experience** — side-effect import initialization, monolithic `IntentService`, missing `IntentBuilder`, hardcoded values
+- **Security** — `KeyHandle` async race condition, TronWeb singleton state, no EVM RPC fallback
+- **Extensibility** — chain type logic scattered across directories, no clean boundary for adding new chains
+
+This design resolves all 12 issues through a transformative restructure using NestJS as the dependency injection container.
+
+---
+
+## Decisions
+
+| Question | Decision |
+|----------|----------|
+| Scope | Transformative — clean break, not incremental |
+| Output | CLI only — no public SDK surface |
+| DI Framework | NestJS standalone (`createApplicationContext`) + `nestjs-commander` |
+| Prompt library | Keep `inquirer` — wrapped in injectable `PromptService` |
+| Test coverage | Not a deliverable of this effort — design for testability, write tests later |
+| Phasing | Comprehensive — single design spec, implemented as one effort |
+
+---
+
+## Module Structure
+
+The application decomposes into focused NestJS modules. `CoreModule` was deliberately excluded — types, errors, and security utilities are plain TypeScript in `shared/` and imported directly.
+
+```
+AppModule
+ imports: [ConfigModule, BlockchainModule, IntentModule,
+ QuoteModule, StatusModule, CliModule]
+
+ConfigModule (global) — env validation, typed config access, token definitions
+BlockchainModule (global) — everything chain-related: registry, handlers, publishers,
+ encoding, address normalization, chains config, RPC fallback
+IntentModule — IntentBuilder (pure), IntentStorage (persistence)
+QuoteModule — QuoteService (network, no I/O side effects)
+StatusModule — StatusService (all chain types)
+CliModule (leaf) — commands, PromptService, DisplayService
+```
+
+**`ConfigModule` and `BlockchainModule` are `@Global()`** — available everywhere without explicit import in every module.
+
+**`CliModule` is the only leaf** — it imports everything but nothing imports it. CLI concerns can never leak into business logic.
+
+---
+
+## Directory Structure
+
+```
+src/
+├── main.ts # bootstrap() only
+├── app.module.ts # root AppModule
+│
+├── shared/ # plain TypeScript — no NestJS module
+│ ├── types/
+│ │ ├── intent.interface.ts # Intent, Route, Reward interfaces
+│ │ ├── universal-address.ts # UniversalAddress branded type + helpers
+│ │ └── blockchain-addresses.ts # EvmAddress, TronAddress, SvmAddress
+│ ├── security/
+│ │ └── key-handle.ts # KeyHandle with sync use() + async useAsync()
+│ └── errors/
+│ └── routes-cli-error.ts # RoutesCliError + ErrorCode enum
+│
+├── config/ # ConfigModule (global)
+│ ├── config.module.ts
+│ ├── config.service.ts # typed getters: getRpcUrl(), getDeadlineOffset(), etc.
+│ ├── tokens.config.ts # token definitions (USDC, USDT, bUSDC, bUSDT)
+│ └── validation/
+│ └── env.schema.ts # Zod schemas for all env vars
+│
+├── blockchain/ # BlockchainModule (global)
+│ ├── blockchain.module.ts
+│ ├── address-normalizer.service.ts # normalize() / denormalize() — injectable
+│ ├── chain-registry.service.ts # explicit bootstrap(), isRegistered()
+│ ├── chain-handler.interface.ts # ChainHandler interface
+│ ├── chains.config.ts # all chain definitions (prod + dev)
+│ ├── chains.service.ts # getChainById(), listChains(), resolveChain()
+│ ├── base.publisher.ts # abstract BasePublisher
+│ ├── publisher-factory.service.ts # createPublisher(chainType, rpcUrl)
+│ ├── rpc.service.ts # withFallback(primary, secondary) — all chain types
+│ ├── encoding/
+│ │ ├── portal-encoder.service.ts # ABI (EVM/TVM) + Borsh (SVM) encoding
+│ │ └── intent-converter.service.ts # UniversalAddress → chain-native conversion
+│ ├── abis/
+│ │ ├── portal.abi.ts
+│ │ └── erc20.abi.ts
+│ ├── evm/ # all EVM logic together
+│ │ ├── evm-chain-handler.ts # EVM address normalize/denormalize
+│ │ ├── evm.publisher.ts # viem PublicClient + WalletClient
+│ │ └── evm-client-factory.ts # injectable factory for testability
+│ ├── tvm/ # all TVM logic together
+│ │ ├── tvm-chain-handler.ts # Tron address normalize/denormalize
+│ │ ├── tvm.publisher.ts # per-call TronWeb instantiation
+│ │ └── tvm-client-factory.ts
+│ └── svm/ # all SVM logic together
+│ ├── svm-chain-handler.ts # Solana address normalize/denormalize
+│ ├── svm.publisher.ts # @solana/web3.js + Anchor
+│ ├── svm-client-factory.ts
+│ ├── pda-manager.ts # vault, proof, withdrawn_marker PDA derivation
+│ ├── transaction-builder.ts # buildFundingTransaction()
+│ └── solana-client.ts # setupAnchorProgram(), connection config
+│
+├── intent/ # IntentModule
+│ ├── intent.module.ts
+│ ├── intent-builder.service.ts # pure data assembly — no I/O, no prompts
+│ └── intent-storage.service.ts # ~/.routes-cli/intents.json persistence
+│
+├── quote/ # QuoteModule
+│ ├── quote.module.ts
+│ └── quote.service.ts # endpoint selection, request, response normalization
+│
+├── status/ # StatusModule
+│ ├── status.module.ts
+│ └── status.service.ts # status checking for EVM + TVM + SVM
+│
+├── cli/ # CliModule (leaf)
+│ ├── cli.module.ts
+│ ├── services/
+│ │ ├── prompt.service.ts # injectable inquirer wrapper
+│ │ └── display.service.ts # injectable ora + cli-table3 wrapper
+│ └── commands/
+│ ├── publish.command.ts # @Command('publish')
+│ ├── status.command.ts # @Command('status')
+│ ├── config.command.ts # @Command('config')
+│ ├── chains.command.ts # @Command('chains')
+│ └── tokens.command.ts # @Command('tokens')
+│
+└── commons/
+ └── utils/ # shared pure utilities (no NestJS)
+```
+
+---
+
+## Service Designs
+
+### `ConfigService` (ConfigModule)
+
+Wraps `@nestjs/config` with Zod validation. All values are typed — no raw `process.env` access outside this service.
+
+```typescript
+@Injectable()
+class ConfigService {
+ getEvmPrivateKey(): Hex | undefined
+ getTvmPrivateKey(): string | undefined
+ getSvmPrivateKey(): string | undefined
+ getRpcUrl(chainType: ChainType, variant: 'primary' | 'fallback'): string
+ getQuoteEndpoint(): string // selects SOLVER_URL, preprod, or production
+ getDeadlineOffsetSeconds(): number // default: 9000 (2.5h), was hardcoded
+ getDappId(): string // default: 'eco-routes-cli', was hardcoded
+ isProdEnvironment(): boolean
+}
+```
+
+### `ChainRegistryService` (BlockchainModule)
+
+Explicit initialization — no side-effect imports. Bootstrapped inside `BlockchainModule.onModuleInit()`.
+
+```typescript
+@Injectable()
+class ChainRegistryService implements OnModuleInit {
+ onModuleInit() {
+ this.bootstrap([
+ new EvmChainHandler(),
+ new TvmChainHandler(),
+ new SvmChainHandler(),
+ ]);
+ }
+
+ bootstrap(handlers: ChainHandler[]): void
+ get(chainType: ChainType): ChainHandler
+ isRegistered(chainId: bigint): boolean
+ registerChainId(chainId: bigint): void
+}
+```
+
+### `AddressNormalizerService` (BlockchainModule)
+
+Same API as the current static `AddressNormalizer` class, now injectable. Delegates to `ChainRegistryService`.
+
+```typescript
+@Injectable()
+class AddressNormalizerService {
+ normalize(address: BlockchainAddress, chainType: ChainType): UniversalAddress
+ denormalize(address: UniversalAddress, chainType: ChainType): BlockchainAddress
+ denormalizeToEvm(address: UniversalAddress): EvmAddress
+ denormalizeToTvm(address: UniversalAddress): TronAddress
+ denormalizeToSvm(address: UniversalAddress): SvmAddress
+}
+```
+
+### `RpcService` (BlockchainModule)
+
+Uniform RPC fallback for all chain types — fixes the current EVM gap.
+
+```typescript
+@Injectable()
+class RpcService {
+ getUrl(chain: ChainConfig): string // applies withFallback(primary, secondary)
+ withFallback(primary: () => Promise, fallback: () => Promise): Promise
+}
+```
+
+### `QuoteService` (QuoteModule)
+
+Pure network concern — extracted from the current monolithic `IntentService`. No prompts, no intent assembly.
+
+```typescript
+@Injectable()
+class QuoteService {
+ async getQuote(params: QuoteRequest): Promise
+ // Normalizes both v3 wrapped and solver-v2 array response formats
+ // Throws QuoteServiceError on failure — caller decides fallback behavior
+}
+```
+
+### `IntentBuilder` (IntentModule)
+
+Pure data assembly — no I/O, no network calls, no prompts. Takes explicit inputs, returns immutable data.
+
+```typescript
+@Injectable()
+class IntentBuilder {
+ buildReward(params: RewardParams): Intent['reward']
+ buildManualRoute(params: ManualRouteParams): Intent['route']
+ buildFromQuote(params: QuoteRouteParams): Intent['route']
+}
+```
+
+### `IntentStorage` (IntentModule)
+
+Implements the missing `~/.routes-cli/intents.json` persistence.
+
+```typescript
+@Injectable()
+class IntentStorage {
+ async save(intent: Intent, result: PublishResult): Promise
+ async findByHash(intentHash: string): Promise
+ async listAll(): Promise
+ async markRefunded(intentHash: string): Promise
+}
+```
+
+### `StatusService` (StatusModule)
+
+Replaces the EVM-only `status` command. Routes to the correct publisher based on chain type.
+
+```typescript
+@Injectable()
+class StatusService {
+ async getStatus(intentHash: string, chain: ChainConfig): Promise
+ async watch(intentHash: string, chain: ChainConfig, onUpdate: (s: IntentStatus) => void): Promise
+}
+```
+
+### `PromptService` (CliModule)
+
+Injectable wrapper around inquirer. All prompt logic centralized here.
+
+```typescript
+@Injectable()
+class PromptService {
+ async selectChain(chains: ChainConfig[], message: string): Promise
+ async selectToken(tokens: TokenConfig[], label: string): Promise
+ async inputAmount(symbol: string): Promise<{ raw: string; parsed: bigint }>
+ async inputAddress(chain: ChainConfig, label: string): Promise
+ async confirmPublish(summary: IntentSummary): Promise
+ async inputManualRoute(chain: ChainConfig): Promise
+ async confirm(message: string, defaultValue?: boolean): Promise
+}
+```
+
+### `DisplayService` (CliModule)
+
+Injectable wrapper around `ora` and `cli-table3`.
+
+```typescript
+@Injectable()
+class DisplayService {
+ spinner(text: string): void
+ succeed(text?: string): void
+ fail(text?: string): void
+ displayIntentSummary(summary: IntentSummary): void
+ displayTransactionResult(result: PublishResult): void
+ displayTable(headers: string[], rows: string[][]): void
+ displayChains(chains: ChainConfig[]): void
+ displayTokens(tokens: TokenConfig[]): void
+}
+```
+
+---
+
+## Security Changes
+
+### AsyncKeyHandle — fixes async zeroization race
+
+```typescript
+class KeyHandle {
+ private buffer: Buffer;
+
+ // Sync variant — kept for synchronous derivations
+ use(fn: (key: string) => T): T {
+ try { return fn(this.buffer.toString('utf8')); }
+ finally { this.buffer.fill(0); }
+ }
+
+ // Async variant — zeroes buffer only after the promise resolves/rejects
+ async useAsync(fn: (key: string) => Promise): Promise {
+ try { return await fn(this.buffer.toString('utf8')); }
+ finally { this.buffer.fill(0); }
+ }
+}
+```
+
+Publishers call `keyHandle.useAsync()` directly. No second `KeyHandle` construction needed.
+
+### TronWeb per-call instantiation — fixes singleton concurrency risk
+
+```typescript
+// tvm.publisher.ts
+async publish(..., keyHandle: KeyHandle): Promise {
+ return keyHandle.useAsync(async (key) => {
+ const tronWeb = this.tvmClientFactory.create(this.rpcUrl, key);
+ return this.executePublish(tronWeb, ...);
+ // tronWeb scoped to this call — no shared state, no key clearing needed
+ });
+}
+```
+
+### Chain ID allowlist via `ChainRegistryService`
+
+All chain IDs registered during `BlockchainModule.onModuleInit()`. `BasePublisher.runPreflightChecks()` injects `ChainRegistryService` and calls `isRegistered(sourceChainId)`.
+
+---
+
+## Data Flow — Publish Intent
+
+```
+nestjs-commander parses args + options
+ │
+ ▼
+PublishCommand.run(params, options)
+ │
+ ├─► PromptService.selectChain() → ChainsService.listChains()
+ ├─► PromptService.selectChain() → destination chain
+ ├─► PromptService.selectToken() → route token on dest
+ ├─► PromptService.selectToken() → reward token on source
+ ├─► PromptService.inputAmount() → reward amount as bigint
+ ├─► PromptService.inputAddress() → recipient → normalize
+ │
+ ├─► KeyHandle.useAsync(key => {
+ │ senderAddress = getWalletAddress(chainType, key)
+ │ return publisher.publish(..., key) ← full async publish inside useAsync
+ │ })
+ │
+ ├─► QuoteService.getQuote(params)
+ │ └─► on failure: PromptService.inputManualRoute()
+ │
+ ├─► IntentBuilder.buildReward(params) ← pure, no I/O
+ │
+ ├─► DisplayService.displayIntentSummary()
+ ├─► PromptService.confirmPublish()
+ │
+ ├─► RpcService.getUrl(sourceChain) ← withFallback applied here
+ ├─► PublisherFactory.create(chainType, url)
+ │
+ ├─► publisher.publish(source, dest, reward, encodedRoute, keyHandle, portal)
+ │
+ ├─► IntentStorage.save(intent, result) ← always persisted
+ │
+ └─► DisplayService.displayTransactionResult(result)
+```
+
+---
+
+## Module Dependency Graph
+
+```
+ConfigModule (global)
+ exports: [ConfigService]
+ imports: []
+
+BlockchainModule (global)
+ exports: [AddressNormalizerService, ChainsService, ChainRegistryService,
+ PublisherFactory, RpcService]
+ imports: [ConfigModule]
+
+QuoteModule
+ exports: [QuoteService]
+ imports: [ConfigModule, BlockchainModule]
+
+IntentModule
+ exports: [IntentBuilder, IntentStorage]
+ imports: [BlockchainModule]
+
+StatusModule
+ exports: [StatusService]
+ imports: [BlockchainModule]
+
+CliModule ← leaf, nothing imports this
+ exports: []
+ imports: [BlockchainModule, ConfigModule, IntentModule,
+ QuoteModule, StatusModule]
+```
+
+---
+
+## Issues Resolved
+
+| # | Issue | Resolution |
+|---|-------|------------|
+| 1 | Side-effect import order dependency | `ChainRegistryService.onModuleInit()` bootstrap |
+| 2 | KeyHandle async race condition | `useAsync()` awaits before zeroing |
+| 3 | No intent persistence | `IntentStorage` service |
+| 4 | IntentService mixes concerns | Dissolved into `IntentBuilder` + `QuoteService` + `PublishCommand` |
+| 5 | No standalone IntentBuilder | `IntentBuilder` as pure injectable service |
+| 6 | Missing portal addresses on production chains | Populate in `chains.config.ts` as part of implementation |
+| 7 | Status command EVM-only | `StatusService` routes to correct publisher by chain type |
+| 8 | RPC fallback EVM-only | `RpcService.withFallback()` uniform across all chain types |
+| 9 | No tests | Not a deliverable — designed for testability via injection |
+| 10 | Hardcoded dAppID | `ConfigService.getDappId()` |
+| 11 | TronWeb singleton state | Per-call `TronWeb` instantiation in `TvmPublisher` |
+| 12 | Hardcoded route deadline | `ConfigService.getDeadlineOffsetSeconds()` |
+
+---
+
+## New Dependencies Required
+
+| Package | Purpose |
+|---------|---------|
+| `@nestjs/core` | NestJS IoC container |
+| `@nestjs/common` | Decorators (`@Injectable`, `@Module`, etc.) |
+| `@nestjs/config` | Environment config with validation |
+| `nestjs-commander` | CLI command decorators (`@Command`, `@Option`) |
+| `reflect-metadata` | Required by NestJS decorators (may already be present) |
+
+**Packages to remove:**
+| Package | Replaced by |
+|---------|-------------|
+| `commander` | `nestjs-commander` |
+
+---
+
+## Implementation Notes
+
+1. `tsconfig.json` already has `experimentalDecorators: true` and `emitDecoratorMetadata: true` — NestJS decorator support is ready
+2. `nestjs-commander` wraps Commander internally — migration from Commander to `nestjs-commander` is straightforward
+3. All existing inquirer prompts migrate verbatim into `PromptService` methods
+4. All existing `ora` / `cli-table3` calls migrate verbatim into `DisplayService` methods
+5. Chain type subdirectories (`evm/`, `tvm/`, `svm/`) already exist in current codebase — files move, not rewrite
+6. `shared/` replaces the need for `src/core/` as a NestJS module — same files, no NestJS wiring
diff --git a/docs/plans/2026-02-20-architecture-improvement.md b/docs/plans/2026-02-20-architecture-improvement.md
new file mode 100644
index 0000000..89d7d96
--- /dev/null
+++ b/docs/plans/2026-02-20-architecture-improvement.md
@@ -0,0 +1,1978 @@
+# Architecture Improvement Implementation Plan
+
+> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
+
+**Goal:** Transform the routes-cli codebase into a clean NestJS-based architecture that resolves all 12 structural issues identified in ARCHITECTURE.md.
+
+**Architecture:** NestJS standalone application using `nestjs-commander` for CLI commands. All business logic lives in focused injectable services organized into domain modules (`BlockchainModule`, `ConfigModule`, `IntentModule`, `QuoteModule`, `StatusModule`). The `CliModule` is the leaf — it consumes all services but nothing imports it. Plain TypeScript in `shared/` replaces the old `CoreModule`.
+
+**Tech Stack:** NestJS (`@nestjs/core`, `@nestjs/common`, `@nestjs/config`), `nestjs-commander`, `inquirer`, `ora`, `cli-table3`, `viem`, `tronweb`, `@solana/web3.js`, `@coral-xyz/anchor`, `zod`
+
+**Design doc:** `docs/plans/2026-02-20-architecture-improvement-design.md`
+
+---
+
+## Phase 1: Foundation & Dependencies
+
+### Task 1: Install NestJS dependencies
+
+**Files:**
+- Modify: `package.json`
+
+**Step 1: Install new dependencies**
+
+```bash
+pnpm add @nestjs/core @nestjs/common @nestjs/config nestjs-commander reflect-metadata
+```
+
+**Step 2: Remove commander (replaced by nestjs-commander)**
+
+```bash
+pnpm remove commander
+```
+
+**Step 3: Verify install**
+
+```bash
+pnpm typecheck
+```
+Expected: No errors (nothing uses commander yet at the type level).
+
+**Step 4: Commit**
+
+```bash
+git add package.json pnpm-lock.yaml
+git commit -m "chore(deps): add nestjs + nestjs-commander, remove commander"
+```
+
+---
+
+### Task 2: Create shared/types/
+
+These are pure TypeScript types — no NestJS, no side effects. Migrated from `src/core/types/` and `src/core/interfaces/`.
+
+**Files:**
+- Create: `src/shared/types/universal-address.ts`
+- Create: `src/shared/types/blockchain-addresses.ts`
+- Create: `src/shared/types/intent.interface.ts`
+
+**Step 1: Create `src/shared/types/universal-address.ts`**
+
+Copy verbatim from `src/core/types/universal-address.ts`. No changes needed.
+
+**Step 2: Create `src/shared/types/blockchain-addresses.ts`**
+
+Copy verbatim from `src/core/types/blockchain-addresses.ts`. No changes needed.
+
+**Step 3: Create `src/shared/types/intent.interface.ts`**
+
+Copy verbatim from `src/core/interfaces/intent.ts`. No changes needed.
+
+**Step 4: Create barrel `src/shared/types/index.ts`**
+
+```typescript
+export * from './universal-address';
+export * from './blockchain-addresses';
+export * from './intent.interface';
+```
+
+**Step 5: Verify**
+
+```bash
+pnpm typecheck
+```
+
+**Step 6: Commit**
+
+```bash
+git add src/shared/
+git commit -m "refactor: add shared/types (migrated from core/types + core/interfaces)"
+```
+
+---
+
+### Task 3: Create shared/security/key-handle.ts
+
+Adds `useAsync()` — the critical fix for the async key zeroization race condition.
+
+**Files:**
+- Create: `src/shared/security/key-handle.ts`
+
+**Step 1: Write `src/shared/security/key-handle.ts`**
+
+```typescript
+/**
+ * A single-use wrapper around a private key string.
+ *
+ * Calling use() or useAsync() passes the key to a function and immediately
+ * zeroizes the internal buffer in a finally block, regardless of success or failure.
+ *
+ * use() — synchronous; buffer zeroed after fn() returns
+ * useAsync() — async-safe; buffer zeroed after the returned Promise settles
+ */
+export class KeyHandle {
+ private buffer: Buffer;
+
+ constructor(key: string) {
+ this.buffer = Buffer.from(key, 'utf8');
+ }
+
+ /**
+ * Synchronous variant. Use for deriving wallet addresses or other
+ * synchronous key operations. Buffer is zeroed before any async work begins.
+ */
+ use(fn: (key: string) => T): T {
+ try {
+ return fn(this.buffer.toString('utf8'));
+ } finally {
+ this.buffer.fill(0);
+ }
+ }
+
+ /**
+ * Async-safe variant. Buffer is zeroed only after the promise resolves or rejects.
+ * Use this when the key needs to survive through async operations (e.g. publisher.publish).
+ */
+ async useAsync(fn: (key: string) => Promise): Promise {
+ try {
+ return await fn(this.buffer.toString('utf8'));
+ } finally {
+ this.buffer.fill(0);
+ }
+ }
+}
+```
+
+**Step 2: Create barrel `src/shared/security/index.ts`**
+
+```typescript
+export * from './key-handle';
+```
+
+**Step 3: Verify**
+
+```bash
+pnpm typecheck
+```
+
+**Step 4: Commit**
+
+```bash
+git add src/shared/security/
+git commit -m "feat(security): add async-safe KeyHandle.useAsync()"
+```
+
+---
+
+### Task 4: Create shared/errors/
+
+**Files:**
+- Create: `src/shared/errors/routes-cli-error.ts`
+
+**Step 1: Write `src/shared/errors/routes-cli-error.ts`**
+
+Copy verbatim from `src/core/errors/errors.ts`. No changes needed.
+
+**Step 2: Create barrel `src/shared/errors/index.ts`**
+
+```typescript
+export * from './routes-cli-error';
+```
+
+**Step 3: Create top-level `src/shared/index.ts`**
+
+```typescript
+export * from './types';
+export * from './security';
+export * from './errors';
+```
+
+**Step 4: Verify**
+
+```bash
+pnpm typecheck
+```
+
+**Step 5: Commit**
+
+```bash
+git add src/shared/errors/ src/shared/index.ts
+git commit -m "refactor: add shared/errors (migrated from core/errors)"
+```
+
+---
+
+## Phase 2: ConfigModule
+
+### Task 5: Create config/validation/env.schema.ts
+
+**Files:**
+- Create: `src/config/validation/env.schema.ts`
+
+**Step 1: Write the file**
+
+```typescript
+import { z } from 'zod';
+
+export const EnvSchema = z.object({
+ EVM_PRIVATE_KEY: z.string().regex(/^0x[a-fA-F0-9]{64}$/).optional(),
+ TVM_PRIVATE_KEY: z.string().regex(/^[a-fA-F0-9]{64}$/).optional(),
+ SVM_PRIVATE_KEY: z.string().min(1).optional(),
+
+ EVM_RPC_URL: z.string().url().optional(),
+ TVM_RPC_URL: z.string().url().default('https://api.trongrid.io'),
+ TVM_RPC_URL_2: z.string().url().default('https://tron.publicnode.com'),
+ SVM_RPC_URL: z.string().url().default('https://api.mainnet-beta.solana.com'),
+ SVM_RPC_URL_2: z.string().url().default('https://solana.publicnode.com'),
+
+ SOLVER_URL: z.string().url().optional(),
+ QUOTES_API_URL: z.string().optional(),
+ QUOTES_PREPROD: z.string().optional(),
+
+ NODE_CHAINS_ENV: z.enum(['production', 'development']).default('production'),
+ DEBUG: z.string().optional(),
+
+ DAPP_ID: z.string().default('eco-routes-cli'),
+ DEADLINE_OFFSET_SECONDS: z.coerce.number().positive().default(9000),
+});
+
+export type EnvConfig = z.infer;
+```
+
+**Step 2: Verify**
+
+```bash
+pnpm typecheck
+```
+
+**Step 3: Commit**
+
+```bash
+git add src/config/validation/
+git commit -m "feat(config): add Zod env validation schema with configurable dAppID + deadline"
+```
+
+---
+
+### Task 6: Create config/config.service.ts
+
+**Files:**
+- Create: `src/config/config.service.ts`
+
+**Step 1: Write the file**
+
+```typescript
+import { Injectable } from '@nestjs/common';
+import { ConfigService as NestConfigService } from '@nestjs/config';
+import { Hex } from 'viem';
+import { ChainType } from '@/shared/types';
+
+@Injectable()
+export class ConfigService {
+ constructor(private readonly config: NestConfigService) {}
+
+ getEvmPrivateKey(): Hex | undefined {
+ return this.config.get('EVM_PRIVATE_KEY');
+ }
+
+ getTvmPrivateKey(): string | undefined {
+ return this.config.get('TVM_PRIVATE_KEY');
+ }
+
+ getSvmPrivateKey(): string | undefined {
+ return this.config.get('SVM_PRIVATE_KEY');
+ }
+
+ getRpcUrl(chainType: ChainType, variant: 'primary' | 'fallback' = 'primary'): string | undefined {
+ const map: Record> = {
+ [ChainType.EVM]: {
+ primary: this.config.get('EVM_RPC_URL') ?? '',
+ fallback: '', // EVM fallback not configured via env — handled per-chain
+ },
+ [ChainType.TVM]: {
+ primary: this.config.get('TVM_RPC_URL') ?? 'https://api.trongrid.io',
+ fallback: this.config.get('TVM_RPC_URL_2') ?? 'https://tron.publicnode.com',
+ },
+ [ChainType.SVM]: {
+ primary: this.config.get('SVM_RPC_URL') ?? 'https://api.mainnet-beta.solana.com',
+ fallback: this.config.get('SVM_RPC_URL_2') ?? 'https://solana.publicnode.com',
+ },
+ };
+ return map[chainType][variant] || undefined;
+ }
+
+ getQuoteEndpoint(): { url: string; type: 'solver-v2' | 'preprod' | 'production' } {
+ const solverUrl = this.config.get('SOLVER_URL');
+ if (solverUrl) {
+ return { url: `${solverUrl}/api/v2/quote/reverse`, type: 'solver-v2' };
+ }
+ if (this.config.get('QUOTES_API_URL') || this.config.get('QUOTES_PREPROD')) {
+ return { url: 'https://quotes-preprod.eco.com/api/v3/quotes/single', type: 'preprod' };
+ }
+ return { url: 'https://quotes.eco.com/api/v3/quotes/single', type: 'production' };
+ }
+
+ getDeadlineOffsetSeconds(): number {
+ return this.config.get('DEADLINE_OFFSET_SECONDS') ?? 9000;
+ }
+
+ getDappId(): string {
+ return this.config.get('DAPP_ID') ?? 'eco-routes-cli';
+ }
+
+ getChainsEnv(): 'production' | 'development' {
+ return this.config.get<'production' | 'development'>('NODE_CHAINS_ENV') ?? 'production';
+ }
+
+ isDebug(): boolean {
+ return !!this.config.get('DEBUG');
+ }
+}
+```
+
+**Step 2: Verify**
+
+```bash
+pnpm typecheck
+```
+
+**Step 3: Commit**
+
+```bash
+git add src/config/config.service.ts
+git commit -m "feat(config): add typed ConfigService with all env getters"
+```
+
+---
+
+### Task 7: Create config/tokens.config.ts
+
+**Files:**
+- Create: `src/config/tokens.config.ts`
+
+**Step 1: Write the file**
+
+Copy `src/config/tokens.ts` to `src/config/tokens.config.ts`. Update all imports from `@/core/` to `@/shared/`. No logic changes.
+
+**Step 2: Create config/config.module.ts**
+
+```typescript
+import { Module, Global } from '@nestjs/common';
+import { ConfigModule as NestConfigModule } from '@nestjs/config';
+import { ConfigService } from './config.service';
+import { EnvSchema } from './validation/env.schema';
+
+@Global()
+@Module({
+ imports: [
+ NestConfigModule.forRoot({
+ isGlobal: true,
+ validate: (config) => EnvSchema.parse(config),
+ }),
+ ],
+ providers: [ConfigService],
+ exports: [ConfigService],
+})
+export class ConfigModule {}
+```
+
+**Step 3: Verify**
+
+```bash
+pnpm typecheck
+```
+
+**Step 4: Commit**
+
+```bash
+git add src/config/
+git commit -m "feat(config): add ConfigModule with global Zod-validated config"
+```
+
+---
+
+## Phase 3: BlockchainModule
+
+### Task 8: Create chain-handler.interface.ts and chain-registry.service.ts
+
+**Files:**
+- Create: `src/blockchain/chain-handler.interface.ts`
+- Create: `src/blockchain/chain-registry.service.ts`
+
+**Step 1: Create `src/blockchain/chain-handler.interface.ts`**
+
+Copy verbatim from `src/core/chain/chain-handler.interface.ts`. Update imports to use `@/shared/`.
+
+**Step 2: Create `src/blockchain/chain-registry.service.ts`**
+
+```typescript
+import { Injectable, OnModuleInit } from '@nestjs/common';
+import { ChainType } from '@/shared/types';
+import { RoutesCliError } from '@/shared/errors';
+import { ChainHandler } from './chain-handler.interface';
+import { EvmChainHandler } from './evm/evm-chain-handler';
+import { TvmChainHandler } from './tvm/tvm-chain-handler';
+import { SvmChainHandler } from './svm/svm-chain-handler';
+
+@Injectable()
+export class ChainRegistryService implements OnModuleInit {
+ private readonly handlers = new Map();
+ private readonly registeredChainIds = new Set();
+
+ onModuleInit(): void {
+ this.bootstrap([
+ new EvmChainHandler(),
+ new TvmChainHandler(),
+ new SvmChainHandler(),
+ ]);
+ }
+
+ bootstrap(handlers: ChainHandler[]): void {
+ for (const handler of handlers) {
+ this.handlers.set(handler.chainType, handler);
+ }
+ }
+
+ get(chainType: ChainType): ChainHandler {
+ const handler = this.handlers.get(chainType);
+ if (!handler) throw RoutesCliError.unsupportedChain(chainType);
+ return handler;
+ }
+
+ getAll(): ChainHandler[] {
+ return [...this.handlers.values()];
+ }
+
+ registerChainId(chainId: bigint): void {
+ this.registeredChainIds.add(chainId);
+ }
+
+ isRegistered(chainId: bigint): boolean {
+ return this.registeredChainIds.has(chainId);
+ }
+}
+```
+
+**Step 3: Verify**
+
+```bash
+pnpm typecheck
+```
+
+**Step 4: Commit**
+
+```bash
+git add src/blockchain/chain-handler.interface.ts src/blockchain/chain-registry.service.ts
+git commit -m "feat(blockchain): add ChainRegistryService with explicit onModuleInit bootstrap"
+```
+
+---
+
+### Task 9: Migrate EVM, TVM, SVM chain handlers
+
+**Files:**
+- Create: `src/blockchain/evm/evm-chain-handler.ts`
+- Create: `src/blockchain/tvm/tvm-chain-handler.ts`
+- Create: `src/blockchain/svm/svm-chain-handler.ts`
+
+**Step 1:** Copy each handler from `src/blockchain/evm/evm-chain-handler.ts`, `src/blockchain/tvm/tvm-chain-handler.ts`, `src/blockchain/svm/svm-chain-handler.ts` (current location).
+
+Update each to:
+- Remove the `chainRegistry.register(new XxxChainHandler())` self-registration line at the bottom (registration now happens in `ChainRegistryService.onModuleInit()`)
+- Update all imports to use `@/shared/` and `@/blockchain/chain-handler.interface`
+
+**Step 2: Verify**
+
+```bash
+pnpm typecheck
+```
+
+**Step 3: Commit**
+
+```bash
+git add src/blockchain/evm/evm-chain-handler.ts src/blockchain/tvm/tvm-chain-handler.ts src/blockchain/svm/svm-chain-handler.ts
+git commit -m "refactor(blockchain): migrate chain handlers, remove self-registration side effects"
+```
+
+---
+
+### Task 10: Create address-normalizer.service.ts
+
+**Files:**
+- Create: `src/blockchain/address-normalizer.service.ts`
+
+**Step 1: Write the file**
+
+```typescript
+import { Injectable } from '@nestjs/common';
+import { ChainType, UniversalAddress, BlockchainAddress, EvmAddress, TronAddress, SvmAddress } from '@/shared/types';
+import { ChainRegistryService } from './chain-registry.service';
+
+@Injectable()
+export class AddressNormalizerService {
+ constructor(private readonly registry: ChainRegistryService) {}
+
+ normalize(address: BlockchainAddress, chainType: ChainType): UniversalAddress {
+ return this.registry.get(chainType).normalize(address as string);
+ }
+
+ denormalize(address: UniversalAddress, chainType: ChainType): BlockchainAddress {
+ return this.registry.get(chainType).denormalize(address);
+ }
+
+ denormalizeToEvm(address: UniversalAddress): EvmAddress {
+ return this.registry.get(ChainType.EVM).denormalize(address) as EvmAddress;
+ }
+
+ denormalizeToTvm(address: UniversalAddress): TronAddress {
+ return this.registry.get(ChainType.TVM).denormalize(address) as TronAddress;
+ }
+
+ denormalizeToSvm(address: UniversalAddress): SvmAddress {
+ return this.registry.get(ChainType.SVM).denormalize(address) as SvmAddress;
+ }
+}
+```
+
+**Step 2: Verify**
+
+```bash
+pnpm typecheck
+```
+
+**Step 3: Commit**
+
+```bash
+git add src/blockchain/address-normalizer.service.ts
+git commit -m "feat(blockchain): add injectable AddressNormalizerService"
+```
+
+---
+
+### Task 11: Create chains.config.ts and chains.service.ts
+
+**Files:**
+- Create: `src/blockchain/chains.config.ts`
+- Create: `src/blockchain/chains.service.ts`
+
+**Step 1: Create `src/blockchain/chains.config.ts`**
+
+Copy `src/config/chains.ts` to `src/blockchain/chains.config.ts`. Update imports to use `@/shared/` and `@/blockchain/address-normalizer.service`. Remove any calls to the old static `AddressNormalizer` — chain config will store raw strings and normalize lazily via `ChainsService`, OR normalize at construction time passing the service.
+
+> Note: Because config is loaded at module init time and `AddressNormalizerService` requires `ChainRegistryService` to be initialized first, address normalization in chains config must happen in `ChainsService.onModuleInit()`, not at file load time.
+
+Update `chains.config.ts` to export raw chain definitions with addresses as plain strings (pre-normalization), using a new type:
+
+```typescript
+export interface RawChainConfig {
+ id: bigint;
+ name: string;
+ env: 'production' | 'development';
+ type: ChainType;
+ rpcUrl: string;
+ portalAddress?: string; // raw string, normalized by ChainsService
+ proverAddress?: string;
+ nativeCurrency: { name: string; symbol: string; decimals: number };
+}
+```
+
+**Step 2: Create `src/blockchain/chains.service.ts`**
+
+```typescript
+import { Injectable, OnModuleInit } from '@nestjs/common';
+import { ConfigService } from '@/config/config.service';
+import { ChainConfig, ChainType, UniversalAddress } from '@/shared/types';
+import { RoutesCliError } from '@/shared/errors';
+import { RAW_CHAIN_CONFIGS, RawChainConfig } from './chains.config';
+import { AddressNormalizerService } from './address-normalizer.service';
+import { ChainRegistryService } from './chain-registry.service';
+
+@Injectable()
+export class ChainsService implements OnModuleInit {
+ private chains: ChainConfig[] = [];
+
+ constructor(
+ private readonly config: ConfigService,
+ private readonly normalizer: AddressNormalizerService,
+ private readonly registry: ChainRegistryService,
+ ) {}
+
+ onModuleInit(): void {
+ const env = this.config.getChainsEnv();
+ this.chains = RAW_CHAIN_CONFIGS
+ .filter(c => c.env === env || c.env === 'production')
+ .map(c => this.normalizeChain(c));
+
+ // Register all chain IDs in the allowlist
+ for (const chain of this.chains) {
+ this.registry.registerChainId(chain.id);
+ }
+ }
+
+ private normalizeChain(raw: RawChainConfig): ChainConfig {
+ return {
+ ...raw,
+ portalAddress: raw.portalAddress
+ ? this.normalizer.normalize(raw.portalAddress as any, raw.type)
+ : undefined,
+ proverAddress: raw.proverAddress
+ ? this.normalizer.normalize(raw.proverAddress as any, raw.type)
+ : undefined,
+ };
+ }
+
+ listChains(): ChainConfig[] {
+ return this.chains;
+ }
+
+ getChainById(id: bigint): ChainConfig {
+ const chain = this.chains.find(c => c.id === id);
+ if (!chain) throw RoutesCliError.unsupportedChain(id);
+ return chain;
+ }
+
+ getChainByName(name: string): ChainConfig {
+ const chain = this.chains.find(c => c.name.toLowerCase() === name.toLowerCase());
+ if (!chain) throw RoutesCliError.unsupportedChain(name);
+ return chain;
+ }
+
+ resolveChain(nameOrId: string): ChainConfig {
+ const asId = BigInt(nameOrId);
+ if (asId) return this.getChainById(asId);
+ return this.getChainByName(nameOrId);
+ }
+}
+```
+
+**Step 3: Verify**
+
+```bash
+pnpm typecheck
+```
+
+**Step 4: Commit**
+
+```bash
+git add src/blockchain/chains.config.ts src/blockchain/chains.service.ts
+git commit -m "feat(blockchain): add ChainsService with lazy normalization in onModuleInit"
+```
+
+---
+
+### Task 12: Create rpc.service.ts
+
+**Files:**
+- Create: `src/blockchain/rpc.service.ts`
+
+**Step 1: Write the file**
+
+```typescript
+import { Injectable } from '@nestjs/common';
+import { ConfigService } from '@/config/config.service';
+import { ChainConfig, ChainType } from '@/shared/types';
+
+@Injectable()
+export class RpcService {
+ constructor(private readonly config: ConfigService) {}
+
+ getUrl(chain: ChainConfig): string {
+ // Chain-specific RPC overrides env override default
+ const envOverride = this.config.getRpcUrl(chain.type, 'primary');
+ return envOverride || chain.rpcUrl;
+ }
+
+ getFallbackUrl(chain: ChainConfig): string | undefined {
+ return this.config.getRpcUrl(chain.type, 'fallback') || undefined;
+ }
+
+ async withFallback(
+ primary: () => Promise,
+ fallback: () => Promise,
+ ): Promise {
+ try {
+ return await primary();
+ } catch {
+ return fallback();
+ }
+ }
+}
+```
+
+**Step 2: Verify**
+
+```bash
+pnpm typecheck
+```
+
+**Step 3: Commit**
+
+```bash
+git add src/blockchain/rpc.service.ts
+git commit -m "feat(blockchain): add RpcService with uniform withFallback() for all chain types"
+```
+
+---
+
+### Task 13: Migrate publishers
+
+**Files:**
+- Create: `src/blockchain/base.publisher.ts`
+- Modify: `src/blockchain/evm/evm.publisher.ts`
+- Modify: `src/blockchain/tvm/tvm.publisher.ts`
+- Modify: `src/blockchain/svm/svm.publisher.ts`
+
+**Step 1: Create `src/blockchain/base.publisher.ts`**
+
+Copy from `src/blockchain/base-publisher.ts`. Add `@Injectable()` decorator. Update imports to `@/shared/`. Change constructor signature to inject `ChainRegistryService`:
+
+```typescript
+import { Injectable } from '@nestjs/common';
+import { ChainRegistryService } from './chain-registry.service';
+
+@Injectable()
+export abstract class BasePublisher {
+ constructor(
+ protected readonly rpcUrl: string,
+ protected readonly registry: ChainRegistryService,
+ ) {}
+
+ protected runPreflightChecks(sourceChainId: bigint): void {
+ if (!this.registry.isRegistered(sourceChainId)) {
+ throw RoutesCliError.unsupportedChain(sourceChainId);
+ }
+ }
+
+ // ... rest of abstract methods unchanged
+}
+```
+
+**Step 2: Update `src/blockchain/evm/evm.publisher.ts`**
+
+- Add `@Injectable()` decorator
+- Update `publish()` to use `keyHandle.useAsync()` instead of `keyHandle.use()` + second KeyHandle construction
+- Inject `ChainRegistryService` via constructor
+- Update all imports to `@/shared/`
+
+Key change in publish():
+```typescript
+// Before (current code):
+const { senderAccount } = keyHandle.use(rawKey => ({
+ senderAccount: privateKeyToAccount(rawKey as Hex),
+}));
+const publishKeyHandle = new KeyHandle(rawKey); // this is wrong - rawKey not in scope
+
+// After:
+return keyHandle.useAsync(async (rawKey) => {
+ const senderAccount = privateKeyToAccount(rawKey as Hex);
+ // ... all async publisher logic here, key alive for the duration
+});
+```
+
+**Step 3: Update `src/blockchain/tvm/tvm.publisher.ts`**
+
+- Add `@Injectable()` decorator
+- Change from singleton TronWeb to per-call instantiation:
+
+```typescript
+// Before: single this.tronWeb instance, key set/clear pattern
+// After:
+return keyHandle.useAsync(async (rawKey) => {
+ const tronWeb = this.factory.create(this.rpcUrl, rawKey);
+ // tronWeb scoped to this call, no finally needed
+ return this.executePublish(tronWeb, ...);
+});
+```
+
+**Step 4: Update `src/blockchain/svm/svm.publisher.ts`**
+
+- Add `@Injectable()` decorator
+- Update to use `keyHandle.useAsync()`
+- Update imports to `@/shared/`
+
+**Step 5: Verify**
+
+```bash
+pnpm typecheck
+```
+
+**Step 6: Commit**
+
+```bash
+git add src/blockchain/base.publisher.ts src/blockchain/evm/evm.publisher.ts src/blockchain/tvm/tvm.publisher.ts src/blockchain/svm/svm.publisher.ts
+git commit -m "feat(blockchain): migrate publishers to injectable NestJS services with useAsync()"
+```
+
+---
+
+### Task 14: Migrate SVM helpers
+
+**Files:**
+- Modify: `src/blockchain/svm/pda-manager.ts`
+- Modify: `src/blockchain/svm/transaction-builder.ts`
+- Modify: `src/blockchain/svm/solana-client.ts`
+
+**Step 1:** Copy these three files from their current locations. Update imports to `@/shared/`.
+
+No logic changes required — these are already pure functions / utility modules.
+
+**Step 2: Verify**
+
+```bash
+pnpm typecheck
+```
+
+**Step 3: Commit**
+
+```bash
+git add src/blockchain/svm/
+git commit -m "refactor(blockchain/svm): migrate SVM helpers to new structure"
+```
+
+---
+
+### Task 15: Migrate client factories
+
+**Files:**
+- Create: `src/blockchain/evm/evm-client-factory.ts`
+- Create: `src/blockchain/tvm/tvm-client-factory.ts`
+- Create: `src/blockchain/svm/svm-client-factory.ts`
+
+**Step 1:** Copy each from current `src/blockchain/evm/`, `tvm/`, `svm/` locations. Update imports to `@/shared/`. No logic changes.
+
+**Step 2: Verify**
+
+```bash
+pnpm typecheck
+```
+
+**Step 3: Commit**
+
+```bash
+git add src/blockchain/evm/evm-client-factory.ts src/blockchain/tvm/tvm-client-factory.ts src/blockchain/svm/svm-client-factory.ts
+git commit -m "refactor(blockchain): migrate client factories to co-located chain dirs"
+```
+
+---
+
+### Task 16: Create publisher-factory.service.ts
+
+**Files:**
+- Create: `src/blockchain/publisher-factory.service.ts`
+
+**Step 1: Write the file**
+
+```typescript
+import { Injectable } from '@nestjs/common';
+import { ChainType } from '@/shared/types';
+import { BasePublisher } from './base.publisher';
+import { EvmPublisher } from './evm/evm.publisher';
+import { TvmPublisher } from './tvm/tvm.publisher';
+import { SvmPublisher } from './svm/svm.publisher';
+import { ChainRegistryService } from './chain-registry.service';
+import { RpcService } from './rpc.service';
+import { ChainConfig } from '@/shared/types';
+
+@Injectable()
+export class PublisherFactory {
+ constructor(
+ private readonly registry: ChainRegistryService,
+ private readonly rpcService: RpcService,
+ ) {}
+
+ create(chain: ChainConfig): BasePublisher {
+ const rpcUrl = this.rpcService.getUrl(chain);
+ switch (chain.type) {
+ case ChainType.EVM:
+ return new EvmPublisher(rpcUrl, this.registry);
+ case ChainType.TVM:
+ return new TvmPublisher(rpcUrl, this.registry);
+ case ChainType.SVM:
+ return new SvmPublisher(rpcUrl, this.registry);
+ default:
+ throw new Error(`Unsupported chain type: ${chain.type}`);
+ }
+ }
+}
+```
+
+**Step 2: Verify**
+
+```bash
+pnpm typecheck
+```
+
+**Step 3: Commit**
+
+```bash
+git add src/blockchain/publisher-factory.service.ts
+git commit -m "feat(blockchain): add PublisherFactory as injectable NestJS service"
+```
+
+---
+
+### Task 17: Migrate encoding services
+
+**Files:**
+- Create: `src/blockchain/encoding/portal-encoder.service.ts`
+- Create: `src/blockchain/encoding/intent-converter.service.ts`
+
+**Step 1:** Copy from `src/core/utils/portal-encoder.ts` and `src/core/utils/intent-converter.ts`.
+
+- Add `@Injectable()` decorator to each
+- Convert static methods to instance methods
+- Inject `AddressNormalizerService` instead of using static `AddressNormalizer`
+- Update all imports to `@/shared/`
+
+**Step 2: Verify**
+
+```bash
+pnpm typecheck
+```
+
+**Step 3: Commit**
+
+```bash
+git add src/blockchain/encoding/
+git commit -m "feat(blockchain): migrate PortalEncoder and IntentConverter to injectable services"
+```
+
+---
+
+### Task 18: Create blockchain.module.ts
+
+**Files:**
+- Create: `src/blockchain/blockchain.module.ts`
+
+**Step 1: Write the file**
+
+```typescript
+import { Global, Module } from '@nestjs/common';
+import { ChainRegistryService } from './chain-registry.service';
+import { AddressNormalizerService } from './address-normalizer.service';
+import { ChainsService } from './chains.service';
+import { RpcService } from './rpc.service';
+import { PublisherFactory } from './publisher-factory.service';
+import { PortalEncoderService } from './encoding/portal-encoder.service';
+import { IntentConverterService } from './encoding/intent-converter.service';
+
+@Global()
+@Module({
+ providers: [
+ ChainRegistryService,
+ AddressNormalizerService,
+ ChainsService,
+ RpcService,
+ PublisherFactory,
+ PortalEncoderService,
+ IntentConverterService,
+ ],
+ exports: [
+ ChainRegistryService,
+ AddressNormalizerService,
+ ChainsService,
+ RpcService,
+ PublisherFactory,
+ PortalEncoderService,
+ IntentConverterService,
+ ],
+})
+export class BlockchainModule {}
+```
+
+**Step 2: Verify**
+
+```bash
+pnpm typecheck
+```
+
+**Step 3: Commit**
+
+```bash
+git add src/blockchain/blockchain.module.ts
+git commit -m "feat(blockchain): assemble BlockchainModule (global)"
+```
+
+---
+
+## Phase 4: QuoteModule
+
+### Task 19: Create quote/quote.service.ts
+
+**Files:**
+- Create: `src/quote/quote.service.ts`
+
+**Step 1:** Copy logic from `src/core/utils/quote.ts`. Convert to `@Injectable()` class. Replace hardcoded `'eco-routes-cli'` and endpoint selection with `ConfigService` injected via constructor:
+
+```typescript
+@Injectable()
+export class QuoteService {
+ constructor(private readonly config: ConfigService) {}
+
+ async getQuote(params: QuoteRequest): Promise {
+ const { url, type } = this.config.getQuoteEndpoint();
+ const dAppID = this.config.getDappId();
+ // ... rest of logic unchanged, uses url + dAppID from config
+ }
+}
+```
+
+**Step 2: Create `src/quote/quote.module.ts`**
+
+```typescript
+import { Module } from '@nestjs/common';
+import { QuoteService } from './quote.service';
+
+@Module({
+ providers: [QuoteService],
+ exports: [QuoteService],
+})
+export class QuoteModule {}
+```
+
+**Step 3: Verify**
+
+```bash
+pnpm typecheck
+```
+
+**Step 4: Commit**
+
+```bash
+git add src/quote/
+git commit -m "feat(quote): add injectable QuoteService with configurable endpoint + dAppID"
+```
+
+---
+
+## Phase 5: IntentModule
+
+### Task 20: Create intent/intent-builder.service.ts
+
+**Files:**
+- Create: `src/intent/intent-builder.service.ts`
+
+**Step 1: Write the file**
+
+Extract the pure intent/reward assembly logic from the current `IntentService`. No prompts, no network calls.
+
+```typescript
+import { Injectable } from '@nestjs/common';
+import { Hex } from 'viem';
+import { Intent, UniversalAddress, ChainConfig } from '@/shared/types';
+import { AddressNormalizerService } from '@/blockchain/address-normalizer.service';
+import { PortalEncoderService } from '@/blockchain/encoding/portal-encoder.service';
+import { ConfigService } from '@/config/config.service';
+
+export interface RewardParams {
+ sourceChain: ChainConfig;
+ creator: UniversalAddress;
+ prover: UniversalAddress;
+ rewardToken: UniversalAddress;
+ rewardAmount: bigint;
+ deadline?: bigint;
+}
+
+export interface ManualRouteParams {
+ destChain: ChainConfig;
+ recipient: UniversalAddress;
+ routeToken: UniversalAddress;
+ routeAmount: bigint;
+ portal: UniversalAddress;
+ deadline?: bigint;
+}
+
+@Injectable()
+export class IntentBuilder {
+ constructor(
+ private readonly config: ConfigService,
+ private readonly encoder: PortalEncoderService,
+ private readonly normalizer: AddressNormalizerService,
+ ) {}
+
+ buildReward(params: RewardParams): Intent['reward'] {
+ const deadlineOffset = BigInt(this.config.getDeadlineOffsetSeconds());
+ const deadline = params.deadline ?? BigInt(Math.floor(Date.now() / 1000)) + deadlineOffset;
+ return {
+ deadline,
+ creator: params.creator,
+ prover: params.prover,
+ nativeAmount: 0n,
+ tokens: [{ token: params.rewardToken, amount: params.rewardAmount }],
+ };
+ }
+
+ buildManualRoute(params: ManualRouteParams): { encodedRoute: Hex; route: Intent['route'] } {
+ const deadlineOffset = BigInt(this.config.getDeadlineOffsetSeconds());
+ const deadline = params.deadline ?? BigInt(Math.floor(Date.now() / 1000)) + deadlineOffset;
+ const salt = this.generateSalt();
+
+ // Build ERC-20 transfer call to recipient
+ const transferData = this.encoder.encodeErc20Transfer(params.recipient, params.routeAmount);
+ const route: Intent['route'] = {
+ salt,
+ deadline,
+ portal: params.portal,
+ nativeAmount: 0n,
+ tokens: [{ token: params.routeToken, amount: params.routeAmount }],
+ calls: [{ target: params.routeToken, data: transferData, value: 0n }],
+ };
+
+ const encodedRoute = this.encoder.encode(route, params.destChain.type);
+ return { encodedRoute, route };
+ }
+
+ private generateSalt(): Hex {
+ const bytes = new Uint8Array(32);
+ crypto.getRandomValues(bytes);
+ return `0x${Buffer.from(bytes).toString('hex')}` as Hex;
+ }
+}
+```
+
+**Step 2: Verify**
+
+```bash
+pnpm typecheck
+```
+
+**Step 3: Commit**
+
+```bash
+git add src/intent/intent-builder.service.ts
+git commit -m "feat(intent): add pure IntentBuilder service — no I/O, no prompts"
+```
+
+---
+
+### Task 21: Create intent/intent-storage.service.ts
+
+**Files:**
+- Create: `src/intent/intent-storage.service.ts`
+
+**Step 1: Write the file**
+
+```typescript
+import { Injectable } from '@nestjs/common';
+import * as fs from 'fs/promises';
+import * as path from 'path';
+import * as os from 'os';
+import { Intent } from '@/shared/types';
+import { PublishResult } from '@/blockchain/base.publisher';
+
+export interface StoredIntent {
+ intentHash: string;
+ sourceChainId: string;
+ destChainId: string;
+ reward: unknown;
+ routeHash: string;
+ publishedAt: number;
+ refundedAt: number | null;
+ transactionHash: string;
+}
+
+@Injectable()
+export class IntentStorage {
+ private readonly storePath = path.join(os.homedir(), '.routes-cli', 'intents.json');
+
+ async save(intent: Intent, result: PublishResult): Promise {
+ const intents = await this.readAll();
+ const entry: StoredIntent = {
+ intentHash: result.intentHash ?? '',
+ sourceChainId: intent.sourceChainId.toString(),
+ destChainId: intent.destination.toString(),
+ reward: intent.reward,
+ routeHash: '',
+ publishedAt: Math.floor(Date.now() / 1000),
+ refundedAt: null,
+ transactionHash: result.transactionHash ?? '',
+ };
+ intents.push(entry);
+ await this.writeAll(intents);
+ }
+
+ async findByHash(intentHash: string): Promise {
+ const intents = await this.readAll();
+ return intents.find(i => i.intentHash === intentHash) ?? null;
+ }
+
+ async listAll(): Promise {
+ return this.readAll();
+ }
+
+ async markRefunded(intentHash: string): Promise {
+ const intents = await this.readAll();
+ const entry = intents.find(i => i.intentHash === intentHash);
+ if (entry) {
+ entry.refundedAt = Math.floor(Date.now() / 1000);
+ await this.writeAll(intents);
+ }
+ }
+
+ private async readAll(): Promise {
+ try {
+ const raw = await fs.readFile(this.storePath, 'utf8');
+ return JSON.parse(raw, (_, v) => typeof v === 'string' && /^\d+n$/.test(v) ? BigInt(v.slice(0, -1)) : v);
+ } catch {
+ return [];
+ }
+ }
+
+ private async writeAll(intents: StoredIntent[]): Promise {
+ await fs.mkdir(path.dirname(this.storePath), { recursive: true });
+ await fs.writeFile(this.storePath, JSON.stringify(intents, (_, v) => typeof v === 'bigint' ? `${v}n` : v, 2));
+ }
+}
+```
+
+**Step 2: Create `src/intent/intent.module.ts`**
+
+```typescript
+import { Module } from '@nestjs/common';
+import { IntentBuilder } from './intent-builder.service';
+import { IntentStorage } from './intent-storage.service';
+
+@Module({
+ providers: [IntentBuilder, IntentStorage],
+ exports: [IntentBuilder, IntentStorage],
+})
+export class IntentModule {}
+```
+
+**Step 3: Verify**
+
+```bash
+pnpm typecheck
+```
+
+**Step 4: Commit**
+
+```bash
+git add src/intent/
+git commit -m "feat(intent): add IntentBuilder + IntentStorage services and IntentModule"
+```
+
+---
+
+## Phase 6: StatusModule
+
+### Task 22: Create status/status.service.ts
+
+**Files:**
+- Create: `src/status/status.service.ts`
+- Create: `src/status/status.module.ts`
+
+**Step 1: Write `src/status/status.service.ts`**
+
+Extract the EVM status logic from `src/commands/status.ts`. Add TVM and SVM status checking via the respective publishers' new `checkStatus()` method on `BasePublisher`.
+
+```typescript
+import { Injectable } from '@nestjs/common';
+import { ChainConfig, ChainType } from '@/shared/types';
+import { PublisherFactory } from '@/blockchain/publisher-factory.service';
+
+export interface IntentStatus {
+ fulfilled: boolean;
+ solver?: string;
+ fulfillmentTxHash?: string;
+ blockNumber?: bigint;
+ timestamp?: number;
+}
+
+@Injectable()
+export class StatusService {
+ constructor(private readonly publisherFactory: PublisherFactory) {}
+
+ async getStatus(intentHash: string, chain: ChainConfig): Promise {
+ const publisher = this.publisherFactory.create(chain);
+ return publisher.getStatus(intentHash, chain.id);
+ }
+
+ async watch(
+ intentHash: string,
+ chain: ChainConfig,
+ onUpdate: (status: IntentStatus) => void,
+ intervalMs = 10_000,
+ ): Promise {
+ let last: IntentStatus | null = null;
+ while (true) {
+ const status = await this.getStatus(intentHash, chain);
+ if (!last || status.fulfilled !== last.fulfilled) {
+ onUpdate(status);
+ last = status;
+ }
+ if (status.fulfilled) break;
+ await new Promise(r => setTimeout(r, intervalMs));
+ }
+ }
+}
+```
+
+**Step 2: Add `getStatus()` abstract method to `BasePublisher`**
+
+```typescript
+abstract getStatus(intentHash: string, chainId: bigint): Promise;
+```
+
+Implement in EVM publisher (extract from current `status.ts`). Add stub implementations to TVM and SVM publishers that throw `'Not yet implemented'` — this makes TVM/SVM status a tracked gap without blocking the rest.
+
+**Step 3: Create `src/status/status.module.ts`**
+
+```typescript
+import { Module } from '@nestjs/common';
+import { StatusService } from './status.service';
+
+@Module({
+ providers: [StatusService],
+ exports: [StatusService],
+})
+export class StatusModule {}
+```
+
+**Step 4: Verify**
+
+```bash
+pnpm typecheck
+```
+
+**Step 5: Commit**
+
+```bash
+git add src/status/
+git commit -m "feat(status): add StatusService routing status checks by chain type"
+```
+
+---
+
+## Phase 7: CliModule
+
+### Task 23: Create cli/services/prompt.service.ts
+
+**Files:**
+- Create: `src/cli/services/prompt.service.ts`
+
+**Step 1:** Extract all inquirer calls from the current `src/cli/prompts/intent-prompts.ts` and `src/commands/publish.ts` into a single injectable class. Methods map 1:1 to existing prompt logic — no UX changes.
+
+```typescript
+import { Injectable } from '@nestjs/common';
+import inquirer from 'inquirer';
+import { ChainConfig, TokenConfig } from '@/shared/types';
+
+@Injectable()
+export class PromptService {
+ async selectChain(chains: ChainConfig[], message: string): Promise {
+ const { chain } = await inquirer.prompt([{
+ type: 'list', name: 'chain', message,
+ choices: chains.map(c => ({ name: `${c.name} (${c.id})`, value: c })),
+ }]);
+ return chain;
+ }
+
+ async selectToken(tokens: TokenConfig[], label: string): Promise<{ address: string; decimals: number; symbol?: string }> {
+ // ... existing logic from intent-prompts.ts
+ }
+
+ async inputAmount(symbol: string): Promise<{ raw: string; parsed: bigint; decimals: number }> {
+ // ... existing logic
+ }
+
+ async inputAddress(chain: ChainConfig, label: string, defaultValue?: string): Promise {
+ // ... existing logic
+ }
+
+ async confirmPublish(): Promise {
+ const { confirmed } = await inquirer.prompt([{
+ type: 'confirm', name: 'confirmed', message: 'Publish this intent?', default: true,
+ }]);
+ return confirmed;
+ }
+
+ async confirm(message: string, defaultValue = false): Promise {
+ const { confirmed } = await inquirer.prompt([{
+ type: 'confirm', name: 'confirmed', message, default: defaultValue,
+ }]);
+ return confirmed;
+ }
+
+ async inputManualPortal(chain: ChainConfig): Promise {
+ // ... existing fallback logic
+ }
+}
+```
+
+**Step 2: Verify**
+
+```bash
+pnpm typecheck
+```
+
+**Step 3: Commit**
+
+```bash
+git add src/cli/services/prompt.service.ts
+git commit -m "feat(cli): add injectable PromptService wrapping all inquirer calls"
+```
+
+---
+
+### Task 24: Create cli/services/display.service.ts
+
+**Files:**
+- Create: `src/cli/services/display.service.ts`
+
+**Step 1:** Extract all `ora` and `cli-table3` calls from `src/utils/logger.ts` into an injectable class. Methods map 1:1 — no UX changes.
+
+```typescript
+import { Injectable } from '@nestjs/common';
+import ora, { Ora } from 'ora';
+import Table from 'cli-table3';
+import chalk from 'chalk';
+import { PublishResult } from '@/blockchain/base.publisher';
+import { ChainConfig, TokenConfig } from '@/shared/types';
+
+@Injectable()
+export class DisplayService {
+ private activeSpinner: Ora | null = null;
+
+ spinner(text: string): void {
+ this.stopSpinner();
+ this.activeSpinner = ora(text).start();
+ }
+
+ succeed(text?: string): void { this.activeSpinner?.succeed(text); this.activeSpinner = null; }
+ fail(text?: string): void { this.activeSpinner?.fail(text); this.activeSpinner = null; }
+ warn(text?: string): void { this.activeSpinner?.warn(text); this.activeSpinner = null; }
+ stopSpinner(): void { this.activeSpinner?.stop(); this.activeSpinner = null; }
+
+ log(msg: string): void { console.log(chalk.gray(msg)); }
+ success(msg: string): void { console.log(chalk.green(`✅ ${msg}`)); }
+ error(msg: string): void { console.error(chalk.red(`❌ ${msg}`)); }
+ warning(msg: string): void { console.warn(chalk.yellow(`⚠️ ${msg}`)); }
+ title(msg: string): void { console.log(chalk.bold.blue(msg)); }
+ section(msg: string): void { console.log(chalk.blue(msg)); }
+
+ displayTable(headers: string[], rows: string[][]): void {
+ const table = new Table({ head: headers.map(h => chalk.cyan(h)), style: { border: ['gray'] } });
+ rows.forEach(row => table.push(row));
+ console.log(table.toString());
+ }
+
+ displayTransactionResult(result: PublishResult): void {
+ this.displayTable(['Field', 'Value'], [
+ ['Transaction Hash', result.transactionHash ?? '-'],
+ ['Intent Hash', result.intentHash ?? '-'],
+ ['Vault Address', result.vaultAddress ?? '-'],
+ ]);
+ }
+
+ displayChains(chains: ChainConfig[]): void {
+ this.displayTable(
+ ['Name', 'ID', 'Type', 'Native Currency'],
+ chains.map(c => [c.name, c.id.toString(), c.type, c.nativeCurrency.symbol]),
+ );
+ }
+
+ displayTokens(tokens: TokenConfig[]): void {
+ this.displayTable(
+ ['Symbol', 'Name', 'Decimals', 'Available Chains'],
+ tokens.map(t => [t.symbol, t.name, t.decimals.toString(), Object.keys(t.addresses).join(', ')]),
+ );
+ }
+}
+```
+
+**Step 2: Verify**
+
+```bash
+pnpm typecheck
+```
+
+**Step 3: Commit**
+
+```bash
+git add src/cli/services/display.service.ts
+git commit -m "feat(cli): add injectable DisplayService wrapping ora + cli-table3"
+```
+
+---
+
+### Task 25: Create cli/commands/publish.command.ts
+
+**Files:**
+- Create: `src/cli/commands/publish.command.ts`
+
+**Step 1: Write the file**
+
+This is the thin orchestrator — prompts + service calls, no business logic:
+
+```typescript
+import { Command, CommandRunner, Option } from 'nestjs-commander';
+import { Injectable } from '@nestjs/common';
+import { ConfigService } from '@/config/config.service';
+import { ChainsService } from '@/blockchain/chains.service';
+import { AddressNormalizerService } from '@/blockchain/address-normalizer.service';
+import { PublisherFactory } from '@/blockchain/publisher-factory.service';
+import { QuoteService } from '@/quote/quote.service';
+import { IntentBuilder } from '@/intent/intent-builder.service';
+import { IntentStorage } from '@/intent/intent-storage.service';
+import { PromptService } from '../services/prompt.service';
+import { DisplayService } from '../services/display.service';
+import { KeyHandle } from '@/shared/security';
+
+interface PublishOptions {
+ source?: string;
+ destination?: string;
+ privateKey?: string;
+ rpc?: string;
+ recipient?: string;
+ dryRun?: boolean;
+}
+
+@Injectable()
+@Command({ name: 'publish', description: 'Publish an intent to the blockchain' })
+export class PublishCommand extends CommandRunner {
+ constructor(
+ private readonly chains: ChainsService,
+ private readonly config: ConfigService,
+ private readonly normalizer: AddressNormalizerService,
+ private readonly publisherFactory: PublisherFactory,
+ private readonly quoteService: QuoteService,
+ private readonly intentBuilder: IntentBuilder,
+ private readonly intentStorage: IntentStorage,
+ private readonly prompt: PromptService,
+ private readonly display: DisplayService,
+ ) {
+ super();
+ }
+
+ async run(_params: string[], options: PublishOptions): Promise {
+ this.display.title('🎨 Interactive Intent Publishing');
+
+ const allChains = this.chains.listChains();
+ const sourceChain = options.source
+ ? this.chains.resolveChain(options.source)
+ : await this.prompt.selectChain(allChains, 'Select source chain:');
+
+ const destChain = options.destination
+ ? this.chains.resolveChain(options.destination)
+ : await this.prompt.selectChain(allChains.filter(c => c.id !== sourceChain.id), 'Select destination chain:');
+
+ this.display.section('📏 Route Configuration (Destination Chain)');
+ const routeToken = await this.prompt.selectToken([], 'route');
+
+ this.display.section('💰 Reward Configuration (Source Chain)');
+ const rewardToken = await this.prompt.selectToken([], 'reward');
+ const { parsed: rewardAmount } = await this.prompt.inputAmount(rewardToken.symbol ?? 'tokens');
+
+ this.display.section('👤 Recipient Configuration');
+ const recipientRaw = options.recipient ?? await this.prompt.inputAddress(destChain, 'recipient');
+ const recipient = this.normalizer.normalize(recipientRaw as any, destChain.type);
+
+ const rawKey = options.privateKey ?? this.config.getEvmPrivateKey() ?? '';
+ const keyHandle = new KeyHandle(rawKey);
+
+ // Derive sender address synchronously, then keep async key handle for publisher
+ let senderAddress: string;
+ const publishKeyHandle = new KeyHandle(rawKey);
+ keyHandle.use(key => {
+ // derive wallet address for display
+ senderAddress = key; // replace with getWalletAddress(sourceChain.type, key)
+ });
+
+ // Quote or fallback
+ let encodedRoute: string;
+ let sourcePortal = sourceChain.portalAddress!;
+ let proverAddress = sourceChain.proverAddress!;
+
+ try {
+ this.display.spinner('Getting quote...');
+ const quote = await this.quoteService.getQuote({
+ source: sourceChain.id,
+ destination: destChain.id,
+ amount: rewardAmount,
+ funder: senderAddress!,
+ recipient: recipientRaw,
+ routeToken: routeToken.address,
+ rewardToken: rewardToken.address,
+ });
+ this.display.succeed('Quote received');
+ encodedRoute = quote.encodedRoute;
+ sourcePortal = this.normalizer.normalize(quote.sourcePortal as any, sourceChain.type);
+ proverAddress = this.normalizer.normalize(quote.prover as any, sourceChain.type);
+ } catch {
+ this.display.warn('Quote service unavailable — using manual configuration');
+ const manual = await this.prompt.inputManualPortal(sourceChain);
+ encodedRoute = manual; // simplified — full manual fallback in production
+ }
+
+ const reward = this.intentBuilder.buildReward({
+ sourceChain,
+ creator: this.normalizer.normalize(senderAddress! as any, sourceChain.type),
+ prover: proverAddress,
+ rewardToken: this.normalizer.normalize(rewardToken.address as any, sourceChain.type),
+ rewardAmount,
+ });
+
+ // Display summary + confirm
+ const confirmed = await this.prompt.confirmPublish();
+ if (!confirmed) throw new Error('Publication cancelled by user');
+
+ if (options.dryRun) {
+ this.display.warning('Dry run — not publishing');
+ return;
+ }
+
+ this.display.spinner('Publishing intent to blockchain...');
+ const publisher = this.publisherFactory.create(sourceChain);
+ const result = await publisher.publish(
+ sourceChain.id, destChain.id, reward, encodedRoute, publishKeyHandle, sourcePortal,
+ );
+
+ if (!result.success) {
+ this.display.fail('Publishing failed');
+ throw new Error(result.error);
+ }
+
+ await this.intentStorage.save({ destination: destChain.id, sourceChainId: sourceChain.id, route: {} as any, reward }, result);
+ this.display.succeed('Intent published!');
+ this.display.displayTransactionResult(result);
+ }
+
+ @Option({ flags: '-s, --source ', description: 'Source chain name or ID' })
+ parseSource(val: string) { return val; }
+
+ @Option({ flags: '-d, --destination ', description: 'Destination chain name or ID' })
+ parseDestination(val: string) { return val; }
+
+ @Option({ flags: '-k, --private-key ', description: 'Private key override' })
+ parsePrivateKey(val: string) { return val; }
+
+ @Option({ flags: '-r, --rpc ', description: 'RPC URL override' })
+ parseRpc(val: string) { return val; }
+
+ @Option({ flags: '--recipient ', description: 'Recipient address on destination chain' })
+ parseRecipient(val: string) { return val; }
+
+ @Option({ flags: '--dry-run', description: 'Validate without broadcasting' })
+ parseDryRun() { return true; }
+}
+```
+
+**Step 2: Verify**
+
+```bash
+pnpm typecheck
+```
+
+**Step 3: Commit**
+
+```bash
+git add src/cli/commands/publish.command.ts
+git commit -m "feat(cli): add PublishCommand as nestjs-commander injectable"
+```
+
+---
+
+### Task 26: Create remaining commands
+
+**Files:**
+- Create: `src/cli/commands/status.command.ts`
+- Create: `src/cli/commands/config.command.ts`
+- Create: `src/cli/commands/chains.command.ts`
+- Create: `src/cli/commands/tokens.command.ts`
+
+**Step 1: Create `src/cli/commands/chains.command.ts`**
+
+```typescript
+import { Command, CommandRunner } from 'nestjs-commander';
+import { Injectable } from '@nestjs/common';
+import { ChainsService } from '@/blockchain/chains.service';
+import { DisplayService } from '../services/display.service';
+
+@Injectable()
+@Command({ name: 'chains', description: 'List supported chains' })
+export class ChainsCommand extends CommandRunner {
+ constructor(
+ private readonly chains: ChainsService,
+ private readonly display: DisplayService,
+ ) { super(); }
+
+ async run(): Promise {
+ this.display.displayChains(this.chains.listChains());
+ }
+}
+```
+
+**Step 2: Create `src/cli/commands/tokens.command.ts`**
+
+```typescript
+import { Command, CommandRunner } from 'nestjs-commander';
+import { Injectable } from '@nestjs/common';
+import { DisplayService } from '../services/display.service';
+import { TOKENS } from '@/config/tokens.config';
+
+@Injectable()
+@Command({ name: 'tokens', description: 'List configured tokens' })
+export class TokensCommand extends CommandRunner {
+ constructor(private readonly display: DisplayService) { super(); }
+
+ async run(): Promise {
+ this.display.displayTokens(Object.values(TOKENS));
+ }
+}
+```
+
+**Step 3: Create `src/cli/commands/status.command.ts`**
+
+Migrate `src/commands/status.ts` logic. Inject `StatusService` and `DisplayService`. Replace Commander option declarations with `@Option()` decorators.
+
+**Step 4: Create `src/cli/commands/config.command.ts`**
+
+Migrate `src/commands/config.ts` logic. Inject `ConfigService` and `PromptService`.
+
+**Step 5: Verify**
+
+```bash
+pnpm typecheck
+```
+
+**Step 6: Commit**
+
+```bash
+git add src/cli/commands/
+git commit -m "feat(cli): add chains, tokens, status, config commands as nestjs-commander injectables"
+```
+
+---
+
+### Task 27: Create cli.module.ts
+
+**Files:**
+- Create: `src/cli/cli.module.ts`
+
+**Step 1: Write the file**
+
+```typescript
+import { Module } from '@nestjs/common';
+import { PromptService } from './services/prompt.service';
+import { DisplayService } from './services/display.service';
+import { PublishCommand } from './commands/publish.command';
+import { StatusCommand } from './commands/status.command';
+import { ConfigCommand } from './commands/config.command';
+import { ChainsCommand } from './commands/chains.command';
+import { TokensCommand } from './commands/tokens.command';
+
+@Module({
+ providers: [
+ PromptService,
+ DisplayService,
+ PublishCommand,
+ StatusCommand,
+ ConfigCommand,
+ ChainsCommand,
+ TokensCommand,
+ ],
+})
+export class CliModule {}
+```
+
+**Step 2: Verify**
+
+```bash
+pnpm typecheck
+```
+
+**Step 3: Commit**
+
+```bash
+git add src/cli/cli.module.ts
+git commit -m "feat(cli): assemble CliModule (leaf module)"
+```
+
+---
+
+## Phase 8: App Bootstrap
+
+### Task 28: Create app.module.ts and main.ts
+
+**Files:**
+- Create: `src/app.module.ts`
+- Create: `src/main.ts`
+
+**Step 1: Create `src/app.module.ts`**
+
+```typescript
+import { Module } from '@nestjs/common';
+import { ConfigModule } from './config/config.module';
+import { BlockchainModule } from './blockchain/blockchain.module';
+import { IntentModule } from './intent/intent.module';
+import { QuoteModule } from './quote/quote.module';
+import { StatusModule } from './status/status.module';
+import { CliModule } from './cli/cli.module';
+
+@Module({
+ imports: [
+ ConfigModule,
+ BlockchainModule,
+ IntentModule,
+ QuoteModule,
+ StatusModule,
+ CliModule,
+ ],
+})
+export class AppModule {}
+```
+
+**Step 2: Create `src/main.ts`**
+
+```typescript
+import 'reflect-metadata';
+import { NestFactory } from '@nestjs/core';
+import { CommandFactory } from 'nestjs-commander';
+import { AppModule } from './app.module';
+
+async function bootstrap() {
+ const majorVersion = parseInt(process.version.slice(1).split('.')[0], 10);
+ if (majorVersion < 18) {
+ console.error(`Node.js >= 18 required. Current: ${process.version}`);
+ process.exit(1);
+ }
+
+ await CommandFactory.run(AppModule, {
+ logger: false,
+ errorHandler: (err) => {
+ console.error(err.message);
+ if (process.env['DEBUG']) console.error(err.stack);
+ process.exit(1);
+ },
+ });
+}
+
+bootstrap();
+```
+
+**Step 3: Update `package.json` scripts**
+
+```json
+"dev": "tsx -r tsconfig-paths/register src/main.ts",
+"dev:testnet": "NODE_CHAINS_ENV=development tsx -r tsconfig-paths/register src/main.ts",
+"start": "node -r tsconfig-paths/register dist/main.js"
+```
+
+**Step 4: Verify full build**
+
+```bash
+pnpm build
+```
+Expected: Clean compile, `dist/` populated.
+
+**Step 5: Smoke test**
+
+```bash
+pnpm dev chains
+```
+Expected: Table of supported chains printed.
+
+```bash
+pnpm dev tokens
+```
+Expected: Table of tokens printed.
+
+**Step 6: Commit**
+
+```bash
+git add src/app.module.ts src/main.ts package.json
+git commit -m "feat: bootstrap NestJS application with CommandFactory"
+```
+
+---
+
+### Task 29: Remove old source files
+
+Once the new structure is verified working, remove the old files to avoid confusion.
+
+**Files to delete:**
+- `src/index.ts` (replaced by `src/main.ts`)
+- `src/core/` (replaced by `src/shared/` + moved to `src/blockchain/`)
+- `src/commands/` (replaced by `src/cli/commands/`)
+- `src/builders/` (replaced by `src/intent/intent-builder.service.ts`)
+- `src/utils/logger.ts` (replaced by `src/cli/services/display.service.ts`)
+- `src/commons/` if fully migrated
+
+**Step 1: Delete old directories**
+
+```bash
+rm -rf src/index.ts src/core/ src/commands/ src/builders/ src/utils/logger.ts
+```
+
+**Step 2: Verify clean build**
+
+```bash
+pnpm build
+```
+Expected: No errors.
+
+**Step 3: Full smoke test**
+
+```bash
+pnpm dev chains
+pnpm dev tokens
+pnpm dev publish --dry-run
+```
+
+**Step 4: Commit**
+
+```bash
+git add -A
+git commit -m "refactor: remove old src/core, src/commands, src/builders, src/index.ts"
+```
+
+---
+
+### Task 30: Update tsconfig.json
+
+**Files:**
+- Modify: `tsconfig.json`
+
+**Step 1:** Update `exclude` to remove `src/scripts` if moved, add `src/shared` to include:
+
+```json
+{
+ "compilerOptions": {
+ "paths": {
+ "@/*": ["src/*"]
+ }
+ },
+ "exclude": ["node_modules", "dist", "tests"]
+}
+```
+
+**Step 2: Final full verification**
+
+```bash
+pnpm build && pnpm dev chains && pnpm dev tokens
+```
+
+**Step 3: Commit**
+
+```bash
+git add tsconfig.json
+git commit -m "chore: update tsconfig paths for new directory structure"
+```
+
+---
+
+## Summary
+
+| Phase | Tasks | Key Deliverables |
+|-------|-------|-----------------|
+| 1: Foundation | 1–4 | NestJS deps, `shared/` types + security + errors |
+| 2: ConfigModule | 5–7 | Typed config with Zod, configurable deadline + dAppID |
+| 3: BlockchainModule | 8–18 | ChainRegistry with `onModuleInit`, all chain logic co-located, `useAsync`, per-call TronWeb, `RpcService` |
+| 4: QuoteModule | 19 | Injectable `QuoteService` |
+| 5: IntentModule | 20–21 | Pure `IntentBuilder`, `IntentStorage` persistence |
+| 6: StatusModule | 22 | Multi-chain `StatusService` |
+| 7: CliModule | 23–27 | `PromptService`, `DisplayService`, all commands |
+| 8: Bootstrap | 28–30 | `AppModule`, `main.ts`, cleanup |
+
+**Issues resolved:** All 12 from `ARCHITECTURE.md`
diff --git a/eslint.config.js b/eslint.config.js
index 39345d4..0a1f5c4 100644
--- a/eslint.config.js
+++ b/eslint.config.js
@@ -21,7 +21,7 @@ module.exports = defineConfig([
sourceType: 'module',
parserOptions: {
- project: 'tsconfig.json',
+ projectService: true,
tsconfigRootDir: __dirname,
},
@@ -40,9 +40,19 @@ module.exports = defineConfig([
rules: {
'@typescript-eslint/interface-name-prefix': 'off',
- '@typescript-eslint/explicit-function-return-type': 'off',
+ '@typescript-eslint/explicit-function-return-type': [
+ 'error',
+ {
+ allowExpressions: true,
+ allowTypedFunctionExpressions: true,
+ },
+ ],
'@typescript-eslint/explicit-module-boundary-types': 'off',
- '@typescript-eslint/no-explicit-any': 'off',
+ '@typescript-eslint/no-explicit-any': 'error',
+ '@typescript-eslint/no-floating-promises': 'error',
+ '@typescript-eslint/require-await': 'error',
+ 'no-console': ['error', { allow: ['warn', 'error'] }],
+ '@typescript-eslint/no-unsafe-assignment': 'warn',
'@typescript-eslint/no-unused-vars': [
'error',
@@ -66,7 +76,13 @@ module.exports = defineConfig([
'@typescript-eslint/member-ordering': 'off',
},
},
- globalIgnores(['**/.eslintrc.js']),
+ globalIgnores(['**/.eslintrc.js', 'src/scripts/**']),
+ {
+ files: ['src/cli/**/*.ts'],
+ rules: {
+ 'no-console': 'off',
+ },
+ },
{
files: ['**/*.ts', '**/*.tsx'],
diff --git a/jest.config.js b/jest.config.js
deleted file mode 100644
index 4591fc1..0000000
--- a/jest.config.js
+++ /dev/null
@@ -1,17 +0,0 @@
-module.exports = {
- preset: 'ts-jest',
- testEnvironment: 'node',
- testMatch: ['**/?(*.)+(spec|test).ts'],
- collectCoverageFrom: [
- 'src/**/*.ts',
- '!src/**/*.d.ts',
- '!src/index.ts',
- '!src/scripts/**/*',
- ],
- transform: {
- '^.+\\.ts$': ['ts-jest', { tsconfig: 'tests/tsconfig.json' }]
- },
- moduleNameMapper: {
- '^@/(.*)$': '/src/$1'
- }
-};
\ No newline at end of file
diff --git a/jest.config.ts b/jest.config.ts
new file mode 100644
index 0000000..15ae839
--- /dev/null
+++ b/jest.config.ts
@@ -0,0 +1,30 @@
+import type { Config } from 'jest';
+
+const config: Config = {
+ preset: 'ts-jest',
+ testEnvironment: 'node',
+ roots: [''],
+ testMatch: ['**/tests/**/*.test.ts'],
+ testPathIgnorePatterns: ['/node_modules/', '/tests/e2e/'],
+ collectCoverageFrom: [
+ 'src/**/*.ts',
+ '!src/index.ts',
+ '!src/main.ts',
+ '!src/**/*.d.ts',
+ '!src/**/*.module.ts',
+ '!src/scripts/**',
+ ],
+ coverageThreshold: {
+ global: { branches: 15, functions: 20, lines: 25, statements: 25 },
+ },
+ moduleNameMapper: {
+ '^@/(.*)$': '/src/$1',
+ '^ora$': '/tests/__mocks__/ora.ts',
+ },
+ setupFilesAfterEnv: ['/tests/setup/register-chain-handlers.ts'],
+ transform: {
+ '^.+\\.ts$': ['ts-jest', { tsconfig: 'tests/tsconfig.json' }],
+ },
+};
+
+export default config;
diff --git a/jest.e2e.config.ts b/jest.e2e.config.ts
new file mode 100644
index 0000000..087fd17
--- /dev/null
+++ b/jest.e2e.config.ts
@@ -0,0 +1,19 @@
+import type { Config } from 'jest';
+
+const config: Config = {
+ preset: 'ts-jest',
+ testEnvironment: 'node',
+ testMatch: ['**/tests/e2e/**/*.e2e.test.ts'],
+ globalSetup: './tests/e2e/setup/global-setup.ts',
+ globalTeardown: './tests/e2e/setup/global-teardown.ts',
+ testTimeout: 120_000, // 2 minutes — fork startup + transaction confirmation
+ maxWorkers: 1, // E2E tests must be serial (shared Anvil state)
+ moduleNameMapper: {
+ '^@/(.*)$': '/src/$1',
+ },
+ transform: {
+ '^.+\\.ts$': ['ts-jest', { tsconfig: 'tests/tsconfig.json' }],
+ },
+};
+
+export default config;
diff --git a/package.json b/package.json
index a3335eb..30ead52 100644
--- a/package.json
+++ b/package.json
@@ -1,26 +1,36 @@
{
"name": "eco-routes-cli",
- "version": "1.0.0",
+ "version": "1.0.1",
"description": "CLI tool for publishing intents to EVM, TVM, and SVM chains",
- "main": "dist/index.js",
+ "main": "bundle/index.js",
"bin": {
- "eco-routes-cli": "./dist/index.js"
+ "eco-routes-cli": "./bundle/index.js"
},
+ "files": [
+ "bundle",
+ "README.md",
+ "LICENSE"
+ ],
"scripts": {
- "build": "tsc",
- "dev": "tsx -r tsconfig-paths/register src/index.ts",
- "dev:testnet": "NODE_CHAINS_ENV=development tsx -r tsconfig-paths/register src/index.ts",
- "start": "node -r tsconfig-paths/register dist/index.js",
- "clean": "rm -rf dist",
+ "build": "tsc --noEmit && ncc build src/main.ts -o bundle --no-cache",
+ "dev": "ts-node --transpile-only -r tsconfig-paths/register src/main.ts",
+ "dev:testnet": "NODE_CHAINS_ENV=development ts-node --transpile-only -r tsconfig-paths/register src/main.ts",
+ "start": "node bundle/index.js",
+ "clean": "rm -rf dist bundle",
"test": "jest",
+ "test:unit": "jest --testPathPattern='tests/(core|config|blockchain)'",
+ "test:integration": "jest --testPathPattern='tests/integration'",
"test:watch": "jest --watch",
"test:coverage": "jest --coverage",
+ "test:e2e": "jest --config jest.e2e.config.ts",
+ "test:e2e:ci": "jest --config jest.e2e.config.ts --forceExit",
"lint": "eslint src tests --ext .ts",
"lint:fix": "eslint src tests --ext .ts --fix",
"format": "prettier --write src tests",
"format:check": "prettier --check src tests",
"typecheck": "tsc --noEmit",
- "prepare": "husky"
+ "prepare": "husky",
+ "docs": "typedoc"
},
"keywords": [
"blockchain",
@@ -33,37 +43,46 @@
"author": "Eco Protocol",
"license": "MIT",
"dependencies": {
- "@coral-xyz/anchor": "^0.31.1",
+ "@coral-xyz/anchor": "^0.32.1",
+ "@nestjs/common": "^11.1.14",
+ "@nestjs/config": "^4.0.3",
+ "@nestjs/core": "^11.1.14",
"@solana/spl-token": "^0.4.14",
"@solana/web3.js": "^1.91.8",
"chalk": "^4.1.2",
"cli-table3": "^0.6.5",
- "commander": "^12.1.0",
"dotenv": "^16.4.5",
"inquirer": "^9.3.7",
+ "nest-commander": "^3.20.1",
"ora": "^8.2.0",
- "tronweb": "^6.0.0",
- "viem": "~2.40.1"
+ "reflect-metadata": "^0.2.2",
+ "tronweb": "^6.2.0",
+ "viem": "^2.40.1",
+ "zod": "^4.3.6"
},
"devDependencies": {
+ "@changesets/cli": "^2.29.8",
"@types/bn.js": "^5.2.0",
"@types/inquirer": "^9.0.7",
"@types/jest": "^30.0.0",
"@types/node": "^20.16.16",
- "@typescript-eslint/eslint-plugin": "^8.43.0",
- "@typescript-eslint/parser": "^8.43.0",
- "eslint": "^9.35.0",
+ "@typescript-eslint/eslint-plugin": "^8.56.0",
+ "@typescript-eslint/parser": "^8.56.0",
+ "@vercel/ncc": "^0.38.4",
+ "eslint": "^10.0.0",
"eslint-config-prettier": "^9.1.0",
"eslint-plugin-prettier": "^5.1.3",
"eslint-plugin-simple-import-sort": "^12.1.1",
+ "globals": "^17.3.0",
"husky": "^9.1.7",
- "jest": "^30.1.3",
+ "jest": "^30.2.0",
"lint-staged": "^16.1.6",
"prettier": "^3.6.2",
- "ts-jest": "^29.4.1",
+ "ts-jest": "^29.4.6",
"ts-node": "^10.9.2",
"tsconfig-paths": "^4.2.0",
"tsx": "^4.20.5",
+ "typedoc": "^0.28.17",
"typescript": "^5.7.2"
},
"lint-staged": {
@@ -71,5 +90,26 @@
"eslint --fix",
"prettier --write"
]
+ },
+ "engines": {
+ "node": ">=18.0.0",
+ "pnpm": ">=8.0.0"
+ },
+ "pnpm": {
+ "overrides": {
+ "axios": ">=1.13.5",
+ "minimatch": ">=10.2.1",
+ "test-exclude": "8.0.0",
+ "bn.js": "5.2.3"
+ },
+ "auditConfig": {
+ "ignoreCves": [
+ "GHSA-3gc7-fjrx-p6mg",
+ "CVE-2025-3194",
+ "GHSA-2g4f-4pwh-qvx6",
+ "CVE-2025-69873",
+ "GHSA-378v-28hj-76wf"
+ ]
+ }
}
}
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 6765cac..6f6b2d4 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -4,13 +4,28 @@ settings:
autoInstallPeers: true
excludeLinksFromLockfile: false
+overrides:
+ axios: '>=1.13.5'
+ minimatch: '>=10.2.1'
+ test-exclude: 8.0.0
+ bn.js: 5.2.3
+
importers:
.:
dependencies:
'@coral-xyz/anchor':
- specifier: ^0.31.1
- version: 0.31.1(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)
+ specifier: ^0.32.1
+ version: 0.32.1(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)
+ '@nestjs/common':
+ specifier: ^11.1.14
+ version: 11.1.14(reflect-metadata@0.2.2)(rxjs@7.8.2)
+ '@nestjs/config':
+ specifier: ^4.0.3
+ version: 4.0.3(@nestjs/common@11.1.14(reflect-metadata@0.2.2)(rxjs@7.8.2))(rxjs@7.8.2)
+ '@nestjs/core':
+ specifier: ^11.1.14
+ version: 11.1.14(@nestjs/common@11.1.14(reflect-metadata@0.2.2)(rxjs@7.8.2))(reflect-metadata@0.2.2)(rxjs@7.8.2)
'@solana/spl-token':
specifier: ^0.4.14
version: 0.4.14(@solana/web3.js@1.98.4(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10))(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(utf-8-validate@5.0.10)
@@ -23,25 +38,34 @@ importers:
cli-table3:
specifier: ^0.6.5
version: 0.6.5
- commander:
- specifier: ^12.1.0
- version: 12.1.0
dotenv:
specifier: ^16.4.5
version: 16.6.1
inquirer:
specifier: ^9.3.7
version: 9.3.7
+ nest-commander:
+ specifier: ^3.20.1
+ version: 3.20.1(@nestjs/common@11.1.14(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.14(@nestjs/common@11.1.14(reflect-metadata@0.2.2)(rxjs@7.8.2))(reflect-metadata@0.2.2)(rxjs@7.8.2))(@types/inquirer@9.0.9)(@types/node@20.19.11)(typescript@5.9.2)
ora:
specifier: ^8.2.0
version: 8.2.0
+ reflect-metadata:
+ specifier: ^0.2.2
+ version: 0.2.2
tronweb:
- specifier: ^6.0.0
- version: 6.0.4(bufferutil@4.0.9)(utf-8-validate@5.0.10)
+ specifier: ^6.2.0
+ version: 6.2.0(bufferutil@4.0.9)(utf-8-validate@5.0.10)
viem:
- specifier: ~2.40.1
- version: 2.40.4(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)
+ specifier: ^2.40.1
+ version: 2.40.4(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@4.3.6)
+ zod:
+ specifier: ^4.3.6
+ version: 4.3.6
devDependencies:
+ '@changesets/cli':
+ specifier: ^2.29.8
+ version: 2.29.8(@types/node@20.19.11)
'@types/bn.js':
specifier: ^5.2.0
version: 5.2.0
@@ -55,29 +79,35 @@ importers:
specifier: ^20.16.16
version: 20.19.11
'@typescript-eslint/eslint-plugin':
- specifier: ^8.43.0
- version: 8.43.0(@typescript-eslint/parser@8.43.0(eslint@9.35.0)(typescript@5.9.2))(eslint@9.35.0)(typescript@5.9.2)
+ specifier: ^8.56.0
+ version: 8.56.0(@typescript-eslint/parser@8.56.0(eslint@10.0.0)(typescript@5.9.2))(eslint@10.0.0)(typescript@5.9.2)
'@typescript-eslint/parser':
- specifier: ^8.43.0
- version: 8.43.0(eslint@9.35.0)(typescript@5.9.2)
+ specifier: ^8.56.0
+ version: 8.56.0(eslint@10.0.0)(typescript@5.9.2)
+ '@vercel/ncc':
+ specifier: ^0.38.4
+ version: 0.38.4
eslint:
- specifier: ^9.35.0
- version: 9.35.0
+ specifier: ^10.0.0
+ version: 10.0.0
eslint-config-prettier:
specifier: ^9.1.0
- version: 9.1.2(eslint@9.35.0)
+ version: 9.1.2(eslint@10.0.0)
eslint-plugin-prettier:
specifier: ^5.1.3
- version: 5.5.4(eslint-config-prettier@9.1.2(eslint@9.35.0))(eslint@9.35.0)(prettier@3.6.2)
+ version: 5.5.4(eslint-config-prettier@9.1.2(eslint@10.0.0))(eslint@10.0.0)(prettier@3.6.2)
eslint-plugin-simple-import-sort:
specifier: ^12.1.1
- version: 12.1.1(eslint@9.35.0)
+ version: 12.1.1(eslint@10.0.0)
+ globals:
+ specifier: ^17.3.0
+ version: 17.3.0
husky:
specifier: ^9.1.7
version: 9.1.7
jest:
- specifier: ^30.1.3
- version: 30.1.3(@types/node@20.19.11)(ts-node@10.9.2(@types/node@20.19.11)(typescript@5.9.2))
+ specifier: ^30.2.0
+ version: 30.2.0(@types/node@20.19.11)(ts-node@10.9.2(@types/node@20.19.11)(typescript@5.9.2))
lint-staged:
specifier: ^16.1.6
version: 16.1.6
@@ -85,8 +115,8 @@ importers:
specifier: ^3.6.2
version: 3.6.2
ts-jest:
- specifier: ^29.4.1
- version: 29.4.1(@babel/core@7.28.4)(@jest/transform@30.1.2)(@jest/types@30.0.5)(babel-jest@30.1.2(@babel/core@7.28.4))(jest-util@30.0.5)(jest@30.1.3(@types/node@20.19.11)(ts-node@10.9.2(@types/node@20.19.11)(typescript@5.9.2)))(typescript@5.9.2)
+ specifier: ^29.4.6
+ version: 29.4.6(@babel/core@7.29.0)(@jest/transform@30.2.0)(@jest/types@30.2.0)(babel-jest@30.2.0(@babel/core@7.29.0))(jest-util@30.2.0)(jest@30.2.0(@types/node@20.19.11)(ts-node@10.9.2(@types/node@20.19.11)(typescript@5.9.2)))(typescript@5.9.2)
ts-node:
specifier: ^10.9.2
version: 10.9.2(@types/node@20.19.11)(typescript@5.9.2)
@@ -96,6 +126,9 @@ importers:
tsx:
specifier: ^4.20.5
version: 4.20.5
+ typedoc:
+ specifier: ^0.28.17
+ version: 0.28.17(typescript@5.9.2)
typescript:
specifier: ^5.7.2
version: 5.9.2
@@ -105,65 +138,65 @@ packages:
'@adraffy/ens-normalize@1.10.1':
resolution: {integrity: sha512-96Z2IP3mYmF1Xg2cDm8f1gWGf/HUVedQ3FMifV4kG/PQ4yEP51xDtRAEfhVNt5f/uzpNkZHwWQuUcu6D6K+Ekw==}
- '@adraffy/ens-normalize@1.11.0':
- resolution: {integrity: sha512-/3DDPKHqqIqxUULp8yP4zODUY1i+2xvVWsv8A79xGWdCAG+8sb0hRh0Rk2QyOJUnnbyPUAZYcpBuRe3nS2OIUg==}
+ '@adraffy/ens-normalize@1.11.1':
+ resolution: {integrity: sha512-nhCBV3quEgesuf7c7KYfperqSS14T8bYuvJ8PcLJp6znkZpFc0AuW4qBtr8eKVyPPe/8RSr7sglCWPU5eaxwKQ==}
- '@babel/code-frame@7.27.1':
- resolution: {integrity: sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==}
+ '@babel/code-frame@7.29.0':
+ resolution: {integrity: sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==}
engines: {node: '>=6.9.0'}
- '@babel/compat-data@7.28.4':
- resolution: {integrity: sha512-YsmSKC29MJwf0gF8Rjjrg5LQCmyh+j/nD8/eP7f+BeoQTKYqs9RoWbjGOdy0+1Ekr68RJZMUOPVQaQisnIo4Rw==}
+ '@babel/compat-data@7.29.0':
+ resolution: {integrity: sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg==}
engines: {node: '>=6.9.0'}
- '@babel/core@7.28.4':
- resolution: {integrity: sha512-2BCOP7TN8M+gVDj7/ht3hsaO/B/n5oDbiAyyvnRlNOs+u1o+JWNYTQrmpuNp1/Wq2gcFrI01JAW+paEKDMx/CA==}
+ '@babel/core@7.29.0':
+ resolution: {integrity: sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==}
engines: {node: '>=6.9.0'}
- '@babel/generator@7.28.3':
- resolution: {integrity: sha512-3lSpxGgvnmZznmBkCRnVREPUFJv2wrv9iAoFDvADJc0ypmdOxdUtcLeBgBJ6zE0PMeTKnxeQzyk0xTBq4Ep7zw==}
+ '@babel/generator@7.29.1':
+ resolution: {integrity: sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==}
engines: {node: '>=6.9.0'}
- '@babel/helper-compilation-targets@7.27.2':
- resolution: {integrity: sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==}
+ '@babel/helper-compilation-targets@7.28.6':
+ resolution: {integrity: sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==}
engines: {node: '>=6.9.0'}
'@babel/helper-globals@7.28.0':
resolution: {integrity: sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==}
engines: {node: '>=6.9.0'}
- '@babel/helper-module-imports@7.27.1':
- resolution: {integrity: sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==}
+ '@babel/helper-module-imports@7.28.6':
+ resolution: {integrity: sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==}
engines: {node: '>=6.9.0'}
- '@babel/helper-module-transforms@7.28.3':
- resolution: {integrity: sha512-gytXUbs8k2sXS9PnQptz5o0QnpLL51SwASIORY6XaBKF88nsOT0Zw9szLqlSGQDP/4TljBAD5y98p2U1fqkdsw==}
+ '@babel/helper-module-transforms@7.28.6':
+ resolution: {integrity: sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==}
engines: {node: '>=6.9.0'}
peerDependencies:
'@babel/core': ^7.0.0
- '@babel/helper-plugin-utils@7.27.1':
- resolution: {integrity: sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw==}
+ '@babel/helper-plugin-utils@7.28.6':
+ resolution: {integrity: sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==}
engines: {node: '>=6.9.0'}
'@babel/helper-string-parser@7.27.1':
resolution: {integrity: sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==}
engines: {node: '>=6.9.0'}
- '@babel/helper-validator-identifier@7.27.1':
- resolution: {integrity: sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==}
+ '@babel/helper-validator-identifier@7.28.5':
+ resolution: {integrity: sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==}
engines: {node: '>=6.9.0'}
'@babel/helper-validator-option@7.27.1':
resolution: {integrity: sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==}
engines: {node: '>=6.9.0'}
- '@babel/helpers@7.28.4':
- resolution: {integrity: sha512-HFN59MmQXGHVyYadKLVumYsA9dBFun/ldYxipEjzA4196jpLZd8UjEEBLkbEkvfYreDqJhZxYAWFPtrfhNpj4w==}
+ '@babel/helpers@7.28.6':
+ resolution: {integrity: sha512-xOBvwq86HHdB7WUDTfKfT/Vuxh7gElQ+Sfti2Cy6yIWNW05P8iUslOVcZ4/sKbE+/jQaukQAdz/gf3724kYdqw==}
engines: {node: '>=6.9.0'}
- '@babel/parser@7.28.4':
- resolution: {integrity: sha512-yZbBqeM6TkpP9du/I2pUZnJsRMGGvOuIrhjzC1AwHwW+6he4mni6Bp/m8ijn0iOuZuPI2BfkCoSRunpyjnrQKg==}
+ '@babel/parser@7.29.0':
+ resolution: {integrity: sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww==}
engines: {node: '>=6.0.0'}
hasBin: true
@@ -188,8 +221,8 @@ packages:
peerDependencies:
'@babel/core': ^7.0.0-0
- '@babel/plugin-syntax-import-attributes@7.27.1':
- resolution: {integrity: sha512-oFT0FrKHgF53f4vOsZGi2Hh3I35PfSmVs4IBFLFj4dnafP+hIWDLg3VyKmUHfLoLHlyxY4C7DGtmHuJgn+IGww==}
+ '@babel/plugin-syntax-import-attributes@7.28.6':
+ resolution: {integrity: sha512-jiLC0ma9XkQT3TKJ9uYvlakm66Pamywo+qwL+oL8HJOvc6TWdZXVfhqJr8CCzbSGUAbDOzlGHJC1U+vRfLQDvw==}
engines: {node: '>=6.9.0'}
peerDependencies:
'@babel/core': ^7.0.0-0
@@ -204,8 +237,8 @@ packages:
peerDependencies:
'@babel/core': ^7.0.0-0
- '@babel/plugin-syntax-jsx@7.27.1':
- resolution: {integrity: sha512-y8YTNIeKoyhGd9O0Jiyzyyqk8gdjnumGTQPsz0xOZOQ2RmkVJeZ1vmmfIvFEKqucBG6axJGBZDE/7iI5suUI/w==}
+ '@babel/plugin-syntax-jsx@7.28.6':
+ resolution: {integrity: sha512-wgEmr06G6sIpqr8YDwA2dSRTE3bJ+V0IfpzfSY3Lfgd7YWOaAdlykvJi13ZKBt8cZHfgH1IXN+CL656W3uUa4w==}
engines: {node: '>=6.9.0'}
peerDependencies:
'@babel/core': ^7.0.0-0
@@ -252,8 +285,8 @@ packages:
peerDependencies:
'@babel/core': ^7.0.0-0
- '@babel/plugin-syntax-typescript@7.27.1':
- resolution: {integrity: sha512-xfYCBMxveHrRMnAWl1ZlPXOZjzkN82THFvLhQhFXFt81Z5HnN+EtUkZhv/zcKpmT3fzmWZB0ywiBrbC3vogbwQ==}
+ '@babel/plugin-syntax-typescript@7.28.6':
+ resolution: {integrity: sha512-+nDNmQye7nlnuuHDboPbGm00Vqg3oO8niRRL27/4LYHUsHYh0zJ1xWOz0uRwNFmM1Avzk8wZbc6rdiYhomzv/A==}
engines: {node: '>=6.9.0'}
peerDependencies:
'@babel/core': ^7.0.0-0
@@ -266,21 +299,79 @@ packages:
resolution: {integrity: sha512-9uIQ10o0WGdpP6GDhXcdOJPJuDgFtIDtN/9+ArJQ2NAfAmiuhTQdzkaTGR33v43GYS2UrSA0eX2pPPHoFVvpxA==}
engines: {node: '>=6.9.0'}
- '@babel/template@7.27.2':
- resolution: {integrity: sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==}
+ '@babel/template@7.28.6':
+ resolution: {integrity: sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==}
engines: {node: '>=6.9.0'}
- '@babel/traverse@7.28.4':
- resolution: {integrity: sha512-YEzuboP2qvQavAcjgQNVgsvHIDv6ZpwXvcvjmyySP2DIMuByS/6ioU5G9pYrWHM6T2YDfc7xga9iNzYOs12CFQ==}
+ '@babel/traverse@7.29.0':
+ resolution: {integrity: sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==}
engines: {node: '>=6.9.0'}
- '@babel/types@7.28.4':
- resolution: {integrity: sha512-bkFqkLhh3pMBUQQkpVgWDWq/lqzc2678eUyDlTBhRqhCHFguYYGM0Efga7tYk4TogG/3x0EEl66/OQ+WGbWB/Q==}
+ '@babel/types@7.29.0':
+ resolution: {integrity: sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==}
engines: {node: '>=6.9.0'}
'@bcoe/v8-coverage@0.2.3':
resolution: {integrity: sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==}
+ '@borewit/text-codec@0.2.1':
+ resolution: {integrity: sha512-k7vvKPbf7J2fZ5klGRD9AeKfUvojuZIQ3BT5u7Jfv+puwXkUBUT5PVyMDfJZpy30CBDXGMgw7fguK/lpOMBvgw==}
+
+ '@changesets/apply-release-plan@7.0.14':
+ resolution: {integrity: sha512-ddBvf9PHdy2YY0OUiEl3TV78mH9sckndJR14QAt87KLEbIov81XO0q0QAmvooBxXlqRRP8I9B7XOzZwQG7JkWA==}
+
+ '@changesets/assemble-release-plan@6.0.9':
+ resolution: {integrity: sha512-tPgeeqCHIwNo8sypKlS3gOPmsS3wP0zHt67JDuL20P4QcXiw/O4Hl7oXiuLnP9yg+rXLQ2sScdV1Kkzde61iSQ==}
+
+ '@changesets/changelog-git@0.2.1':
+ resolution: {integrity: sha512-x/xEleCFLH28c3bQeQIyeZf8lFXyDFVn1SgcBiR2Tw/r4IAWlk1fzxCEZ6NxQAjF2Nwtczoen3OA2qR+UawQ8Q==}
+
+ '@changesets/cli@2.29.8':
+ resolution: {integrity: sha512-1weuGZpP63YWUYjay/E84qqwcnt5yJMM0tep10Up7Q5cS/DGe2IZ0Uj3HNMxGhCINZuR7aO9WBMdKnPit5ZDPA==}
+ hasBin: true
+
+ '@changesets/config@3.1.2':
+ resolution: {integrity: sha512-CYiRhA4bWKemdYi/uwImjPxqWNpqGPNbEBdX1BdONALFIDK7MCUj6FPkzD+z9gJcvDFUQJn9aDVf4UG7OT6Kog==}
+
+ '@changesets/errors@0.2.0':
+ resolution: {integrity: sha512-6BLOQUscTpZeGljvyQXlWOItQyU71kCdGz7Pi8H8zdw6BI0g3m43iL4xKUVPWtG+qrrL9DTjpdn8eYuCQSRpow==}
+
+ '@changesets/get-dependents-graph@2.1.3':
+ resolution: {integrity: sha512-gphr+v0mv2I3Oxt19VdWRRUxq3sseyUpX9DaHpTUmLj92Y10AGy+XOtV+kbM6L/fDcpx7/ISDFK6T8A/P3lOdQ==}
+
+ '@changesets/get-release-plan@4.0.14':
+ resolution: {integrity: sha512-yjZMHpUHgl4Xl5gRlolVuxDkm4HgSJqT93Ri1Uz8kGrQb+5iJ8dkXJ20M2j/Y4iV5QzS2c5SeTxVSKX+2eMI0g==}
+
+ '@changesets/get-version-range-type@0.4.0':
+ resolution: {integrity: sha512-hwawtob9DryoGTpixy1D3ZXbGgJu1Rhr+ySH2PvTLHvkZuQ7sRT4oQwMh0hbqZH1weAooedEjRsbrWcGLCeyVQ==}
+
+ '@changesets/git@3.0.4':
+ resolution: {integrity: sha512-BXANzRFkX+XcC1q/d27NKvlJ1yf7PSAgi8JG6dt8EfbHFHi4neau7mufcSca5zRhwOL8j9s6EqsxmT+s+/E6Sw==}
+
+ '@changesets/logger@0.1.1':
+ resolution: {integrity: sha512-OQtR36ZlnuTxKqoW4Sv6x5YIhOmClRd5pWsjZsddYxpWs517R0HkyiefQPIytCVh4ZcC5x9XaG8KTdd5iRQUfg==}
+
+ '@changesets/parse@0.4.2':
+ resolution: {integrity: sha512-Uo5MC5mfg4OM0jU3up66fmSn6/NE9INK+8/Vn/7sMVcdWg46zfbvvUSjD9EMonVqPi9fbrJH9SXHn48Tr1f2yA==}
+
+ '@changesets/pre@2.0.2':
+ resolution: {integrity: sha512-HaL/gEyFVvkf9KFg6484wR9s0qjAXlZ8qWPDkTyKF6+zqjBe/I2mygg3MbpZ++hdi0ToqNUF8cjj7fBy0dg8Ug==}
+
+ '@changesets/read@0.6.6':
+ resolution: {integrity: sha512-P5QaN9hJSQQKJShzzpBT13FzOSPyHbqdoIBUd2DJdgvnECCyO6LmAOWSV+O8se2TaZJVwSXjL+v9yhb+a9JeJg==}
+
+ '@changesets/should-skip-package@0.1.2':
+ resolution: {integrity: sha512-qAK/WrqWLNCP22UDdBTMPH5f41elVDlsNyat180A33dWxuUDyNpg6fPi/FyTZwRriVjg0L8gnjJn2F9XAoF0qw==}
+
+ '@changesets/types@4.1.0':
+ resolution: {integrity: sha512-LDQvVDv5Kb50ny2s25Fhm3d9QSZimsoUGBsUioj6MC3qbMUCuC8GPIvk/M6IvXx3lYhAs0lwWUQLb+VIEUCECw==}
+
+ '@changesets/types@6.1.0':
+ resolution: {integrity: sha512-rKQcJ+o1nKNgeoYRHKOS07tAMNd3YSN0uHaJOZYjBAgxfV7TUE7JE+z4BzZdQwb5hKaYbayKN5KrYV7ODb2rAA==}
+
+ '@changesets/write@0.4.0':
+ resolution: {integrity: sha512-CdTLvIOPiCNuH71pyDu3rA+Q0n65cmAbXnwWH84rKGiFumFzkmHNT8KHTMEchcxN+Kl8I54xGUhJ7l3E7X396Q==}
+
'@colors/colors@1.5.0':
resolution: {integrity: sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==}
engines: {node: '>=0.1.90'}
@@ -289,8 +380,8 @@ packages:
resolution: {integrity: sha512-NhNEku4F3zzUSBtrYz84FzYWm48+9OvmT1Hhnwr6GnPQry2dsEqH/ti/7ASjjpoFTWRnPXrjAIT1qM6Isop+LQ==}
engines: {node: '>=10'}
- '@coral-xyz/anchor@0.31.1':
- resolution: {integrity: sha512-QUqpoEK+gi2S6nlYc2atgT2r41TT3caWr/cPUEL8n8Md9437trZ68STknq897b82p5mW0XrTBNOzRbmIRJtfsA==}
+ '@coral-xyz/anchor@0.32.1':
+ resolution: {integrity: sha512-zAyxFtfeje2FbMA1wzgcdVs7Hng/MijPKpRijoySPCicnvcTQs/+dnPZ/cR+LcXM9v9UYSyW81uRNYZtN5G4yg==}
engines: {node: '>=17'}
'@coral-xyz/borsh@0.31.1':
@@ -303,11 +394,11 @@ packages:
resolution: {integrity: sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==}
engines: {node: '>=12'}
- '@emnapi/core@1.5.0':
- resolution: {integrity: sha512-sbP8GzB1WDzacS8fgNPpHlp6C9VZe+SJP3F90W9rLemaQj2PzIuTEl1qDOYQf58YIpyjViI24y9aPWCjEzY2cg==}
+ '@emnapi/core@1.8.1':
+ resolution: {integrity: sha512-AvT9QFpxK0Zd8J0jopedNm+w/2fIzvtPKPjqyw9jwvBaReTTqPBk9Hixaz7KbjimP+QNz605/XnjFcDAL2pqBg==}
- '@emnapi/runtime@1.5.0':
- resolution: {integrity: sha512-97/BJ3iXHww3djw6hYIfErCZFee7qCtrneuLa20UXFCOTCfBM2cvQHjWJ2EG0s0MtdNwInarqCTz35i4wWXHsQ==}
+ '@emnapi/runtime@1.8.1':
+ resolution: {integrity: sha512-mehfKSMWjjNol8659Z8KxEMrdSJDDot5SXMq00dM8BN4o+CLNXQ0xH2V7EchNHV4RmbZLmmPdEaXZc5H2FXmDg==}
'@emnapi/wasi-threads@1.1.0':
resolution: {integrity: sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ==}
@@ -468,43 +559,49 @@ packages:
cpu: [x64]
os: [win32]
- '@eslint-community/eslint-utils@4.8.0':
- resolution: {integrity: sha512-MJQFqrZgcW0UNYLGOuQpey/oTN59vyWwplvCGZztn1cKz9agZPPYpJB7h2OMmuu7VLqkvEjN8feFZJmxNF9D+Q==}
+ '@eslint-community/eslint-utils@4.9.1':
+ resolution: {integrity: sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==}
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
peerDependencies:
eslint: ^6.0.0 || ^7.0.0 || >=8.0.0
- '@eslint-community/regexpp@4.12.1':
- resolution: {integrity: sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==}
+ '@eslint-community/regexpp@4.12.2':
+ resolution: {integrity: sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==}
engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0}
- '@eslint/config-array@0.21.0':
- resolution: {integrity: sha512-ENIdc4iLu0d93HeYirvKmrzshzofPw6VkZRKQGe9Nv46ZnWUzcF1xV01dcvEg/1wXUR61OmmlSfyeyO7EvjLxQ==}
- engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+ '@eslint/config-array@0.23.1':
+ resolution: {integrity: sha512-uVSdg/V4dfQmTjJzR0szNczjOH/J+FyUMMjYtr07xFRXR7EDf9i1qdxrD0VusZH9knj1/ecxzCQQxyic5NzAiA==}
+ engines: {node: ^20.19.0 || ^22.13.0 || >=24}
- '@eslint/config-helpers@0.3.1':
- resolution: {integrity: sha512-xR93k9WhrDYpXHORXpxVL5oHj3Era7wo6k/Wd8/IsQNnZUTzkGS29lyn3nAT05v6ltUuTFVCCYDEGfy2Or/sPA==}
- engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+ '@eslint/config-helpers@0.5.2':
+ resolution: {integrity: sha512-a5MxrdDXEvqnIq+LisyCX6tQMPF/dSJpCfBgBauY+pNZ28yCtSsTvyTYrMhaI+LK26bVyCJfJkT0u8KIj2i1dQ==}
+ engines: {node: ^20.19.0 || ^22.13.0 || >=24}
- '@eslint/core@0.15.2':
- resolution: {integrity: sha512-78Md3/Rrxh83gCxoUc0EiciuOHsIITzLy53m3d9UyiW8y9Dj2D29FeETqyKA+BRK76tnTp6RXWb3pCay8Oyomg==}
- engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+ '@eslint/core@1.1.0':
+ resolution: {integrity: sha512-/nr9K9wkr3P1EzFTdFdMoLuo1PmIxjmwvPozwoSodjNBdefGujXQUF93u1DDZpEaTuDvMsIQddsd35BwtrW9Xw==}
+ engines: {node: ^20.19.0 || ^22.13.0 || >=24}
- '@eslint/eslintrc@3.3.1':
- resolution: {integrity: sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ==}
- engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+ '@eslint/object-schema@3.0.1':
+ resolution: {integrity: sha512-P9cq2dpr+LU8j3qbLygLcSZrl2/ds/pUpfnHNNuk5HW7mnngHs+6WSq5C9mO3rqRX8A1poxqLTC9cu0KOyJlBg==}
+ engines: {node: ^20.19.0 || ^22.13.0 || >=24}
- '@eslint/js@9.35.0':
- resolution: {integrity: sha512-30iXE9whjlILfWobBkNerJo+TXYsgVM5ERQwMcMKCHckHflCmf7wXDAHlARoWnh0s1U72WqlbeyE7iAcCzuCPw==}
- engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+ '@eslint/plugin-kit@0.6.0':
+ resolution: {integrity: sha512-bIZEUzOI1jkhviX2cp5vNyXQc6olzb2ohewQubuYlMXZ2Q/XjBO0x0XhGPvc9fjSIiUN0vw+0hq53BJ4eQSJKQ==}
+ engines: {node: ^20.19.0 || ^22.13.0 || >=24}
- '@eslint/object-schema@2.1.6':
- resolution: {integrity: sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA==}
- engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+ '@fig/complete-commander@3.2.0':
+ resolution: {integrity: sha512-1Holl3XtRiANVKURZwgpjCnPuV4RsHp+XC0MhgvyAX/avQwj7F2HUItYOvGi/bXjJCkEzgBZmVfCr0HBA+q+Bw==}
+ peerDependencies:
+ commander: ^11.1.0
- '@eslint/plugin-kit@0.3.5':
- resolution: {integrity: sha512-Z5kJ+wU3oA7MMIqVR9tyZRtjYPr4OC004Q4Rw7pgOKUOKkJfZ3O24nz3WYfGRpMDNmcOi3TwQOmgm7B7Tpii0w==}
- engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+ '@gerrit0/mini-shiki@3.22.0':
+ resolution: {integrity: sha512-jMpciqEVUBKE1QwU64S4saNMzpsSza6diNCk4MWAeCxO2+LFi2FIFmL2S0VDLzEJCxuvCbU783xi8Hp/gkM5CQ==}
+
+ '@golevelup/nestjs-discovery@5.0.0':
+ resolution: {integrity: sha512-NaIWLCLI+XvneUK05LH2idHLmLNITYT88YnpOuUQmllKtiJNIS3woSt7QXrMZ5k3qUWuZpehEVz1JtlX4I1KyA==}
+ peerDependencies:
+ '@nestjs/common': ^11.0.20
+ '@nestjs/core': ^11.0.20
'@humanfs/core@0.19.1':
resolution: {integrity: sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==}
@@ -522,6 +619,15 @@ packages:
resolution: {integrity: sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==}
engines: {node: '>=18.18'}
+ '@inquirer/external-editor@1.0.3':
+ resolution: {integrity: sha512-RWbSrDiYmO4LbejWY7ttpxczuwQyZLBUyygsA9Nsv95hpzUWwnNTVQmAq3xuh7vNwCp07UTmE5i11XAEExx4RA==}
+ engines: {node: '>=18'}
+ peerDependencies:
+ '@types/node': '>=18'
+ peerDependenciesMeta:
+ '@types/node':
+ optional: true
+
'@inquirer/figures@1.0.13':
resolution: {integrity: sha512-lGPVU3yO9ZNqA7vTYz26jny41lE7yoQansmqdMLBEfqaGsmdg7V3W9mK9Pvb5IL4EVZ9GnSDGMO/cJXud5dMaw==}
engines: {node: '>=18'}
@@ -538,12 +644,12 @@ packages:
resolution: {integrity: sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==}
engines: {node: '>=8'}
- '@jest/console@30.1.2':
- resolution: {integrity: sha512-BGMAxj8VRmoD0MoA/jo9alMXSRoqW8KPeqOfEo1ncxnRLatTBCpRoOwlwlEMdudp68Q6WSGwYrrLtTGOh8fLzw==}
+ '@jest/console@30.2.0':
+ resolution: {integrity: sha512-+O1ifRjkvYIkBqASKWgLxrpEhQAAE7hY77ALLUufSk5717KfOShg6IbqLmdsLMPdUiFvA2kTs0R7YZy+l0IzZQ==}
engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0}
- '@jest/core@30.1.3':
- resolution: {integrity: sha512-LIQz7NEDDO1+eyOA2ZmkiAyYvZuo6s1UxD/e2IHldR6D7UYogVq3arTmli07MkENLq6/3JEQjp0mA8rrHHJ8KQ==}
+ '@jest/core@30.2.0':
+ resolution: {integrity: sha512-03W6IhuhjqTlpzh/ojut/pDB2LPRygyWX8ExpgHtQA8H/3K7+1vKmcINx5UzeOX1se6YEsBsOHQ1CRzf3fOwTQ==}
engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0}
peerDependencies:
node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0
@@ -555,36 +661,36 @@ packages:
resolution: {integrity: sha512-n5H8QLDJ47QqbCNn5SuFjCRDrOLEZ0h8vAHCK5RL9Ls7Xa8AQLa/YxAc9UjFqoEDM48muwtBGjtMY5cr0PLDCw==}
engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0}
- '@jest/environment@30.1.2':
- resolution: {integrity: sha512-N8t1Ytw4/mr9uN28OnVf0SYE2dGhaIxOVYcwsf9IInBKjvofAjbFRvedvBBlyTYk2knbJTiEjEJ2PyyDIBnd9w==}
+ '@jest/environment@30.2.0':
+ resolution: {integrity: sha512-/QPTL7OBJQ5ac09UDRa3EQes4gt1FTEG/8jZ/4v5IVzx+Cv7dLxlVIvfvSVRiiX2drWyXeBjkMSR8hvOWSog5g==}
engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0}
- '@jest/expect-utils@30.1.2':
- resolution: {integrity: sha512-HXy1qT/bfdjCv7iC336ExbqqYtZvljrV8odNdso7dWK9bSeHtLlvwWWC3YSybSPL03Gg5rug6WLCZAZFH72m0A==}
+ '@jest/expect-utils@30.2.0':
+ resolution: {integrity: sha512-1JnRfhqpD8HGpOmQp180Fo9Zt69zNtC+9lR+kT7NVL05tNXIi+QC8Csz7lfidMoVLPD3FnOtcmp0CEFnxExGEA==}
engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0}
- '@jest/expect@30.1.2':
- resolution: {integrity: sha512-tyaIExOwQRCxPCGNC05lIjWJztDwk2gPDNSDGg1zitXJJ8dC3++G/CRjE5mb2wQsf89+lsgAgqxxNpDLiCViTA==}
+ '@jest/expect@30.2.0':
+ resolution: {integrity: sha512-V9yxQK5erfzx99Sf+7LbhBwNWEZ9eZay8qQ9+JSC0TrMR1pMDHLMY+BnVPacWU6Jamrh252/IKo4F1Xn/zfiqA==}
engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0}
- '@jest/fake-timers@30.1.2':
- resolution: {integrity: sha512-Beljfv9AYkr9K+ETX9tvV61rJTY706BhBUtiaepQHeEGfe0DbpvUA5Z3fomwc5Xkhns6NWrcFDZn+72fLieUnA==}
+ '@jest/fake-timers@30.2.0':
+ resolution: {integrity: sha512-HI3tRLjRxAbBy0VO8dqqm7Hb2mIa8d5bg/NJkyQcOk7V118ObQML8RC5luTF/Zsg4474a+gDvhce7eTnP4GhYw==}
engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0}
'@jest/get-type@30.1.0':
resolution: {integrity: sha512-eMbZE2hUnx1WV0pmURZY9XoXPkUYjpc55mb0CrhtdWLtzMQPFvu/rZkTLZFTsdaVQa+Tr4eWAteqcUzoawq/uA==}
engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0}
- '@jest/globals@30.1.2':
- resolution: {integrity: sha512-teNTPZ8yZe3ahbYnvnVRDeOjr+3pu2uiAtNtrEsiMjVPPj+cXd5E/fr8BL7v/T7F31vYdEHrI5cC/2OoO/vM9A==}
+ '@jest/globals@30.2.0':
+ resolution: {integrity: sha512-b63wmnKPaK+6ZZfpYhz9K61oybvbI1aMcIs80++JI1O1rR1vaxHUCNqo3ITu6NU0d4V34yZFoHMn/uoKr/Rwfw==}
engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0}
'@jest/pattern@30.0.1':
resolution: {integrity: sha512-gWp7NfQW27LaBQz3TITS8L7ZCQ0TLvtmI//4OwlQRx4rnWxcPNIYjxZpDcN4+UlGxgm3jS5QPz8IPTCkb59wZA==}
engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0}
- '@jest/reporters@30.1.3':
- resolution: {integrity: sha512-VWEQmJWfXMOrzdFEOyGjUEOuVXllgZsoPtEHZzfdNz18RmzJ5nlR6kp8hDdY8dDS1yGOXAY7DHT+AOHIPSBV0w==}
+ '@jest/reporters@30.2.0':
+ resolution: {integrity: sha512-DRyW6baWPqKMa9CzeiBjHwjd8XeAyco2Vt8XbcLFjiwCOEKOvy82GJ8QQnJE9ofsxCMPjH4MfH8fCWIHHDKpAQ==}
engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0}
peerDependencies:
node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0
@@ -596,28 +702,28 @@ packages:
resolution: {integrity: sha512-DmdYgtezMkh3cpU8/1uyXakv3tJRcmcXxBOcO0tbaozPwpmh4YMsnWrQm9ZmZMfa5ocbxzbFk6O4bDPEc/iAnA==}
engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0}
- '@jest/snapshot-utils@30.1.2':
- resolution: {integrity: sha512-vHoMTpimcPSR7OxS2S0V1Cpg8eKDRxucHjoWl5u4RQcnxqQrV3avETiFpl8etn4dqxEGarBeHbIBety/f8mLXw==}
+ '@jest/snapshot-utils@30.2.0':
+ resolution: {integrity: sha512-0aVxM3RH6DaiLcjj/b0KrIBZhSX1373Xci4l3cW5xiUWPctZ59zQ7jj4rqcJQ/Z8JuN/4wX3FpJSa3RssVvCug==}
engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0}
'@jest/source-map@30.0.1':
resolution: {integrity: sha512-MIRWMUUR3sdbP36oyNyhbThLHyJ2eEDClPCiHVbrYAe5g3CHRArIVpBw7cdSB5fr+ofSfIb2Tnsw8iEHL0PYQg==}
engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0}
- '@jest/test-result@30.1.3':
- resolution: {integrity: sha512-P9IV8T24D43cNRANPPokn7tZh0FAFnYS2HIfi5vK18CjRkTDR9Y3e1BoEcAJnl4ghZZF4Ecda4M/k41QkvurEQ==}
+ '@jest/test-result@30.2.0':
+ resolution: {integrity: sha512-RF+Z+0CCHkARz5HT9mcQCBulb1wgCP3FBvl9VFokMX27acKphwyQsNuWH3c+ojd1LeWBLoTYoxF0zm6S/66mjg==}
engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0}
- '@jest/test-sequencer@30.1.3':
- resolution: {integrity: sha512-82J+hzC0qeQIiiZDThh+YUadvshdBswi5nuyXlEmXzrhw5ZQSRHeQ5LpVMD/xc8B3wPePvs6VMzHnntxL+4E3w==}
+ '@jest/test-sequencer@30.2.0':
+ resolution: {integrity: sha512-wXKgU/lk8fKXMu/l5Hog1R61bL4q5GCdT6OJvdAFz1P+QrpoFuLU68eoKuVc4RbrTtNnTL5FByhWdLgOPSph+Q==}
engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0}
- '@jest/transform@30.1.2':
- resolution: {integrity: sha512-UYYFGifSgfjujf1Cbd3iU/IQoSd6uwsj8XHj5DSDf5ERDcWMdJOPTkHWXj4U+Z/uMagyOQZ6Vne8C4nRIrCxqA==}
+ '@jest/transform@30.2.0':
+ resolution: {integrity: sha512-XsauDV82o5qXbhalKxD7p4TZYYdwcaEXC77PPD2HixEFF+6YGppjrAAQurTl2ECWcEomHBMMNS9AH3kcCFx8jA==}
engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0}
- '@jest/types@30.0.5':
- resolution: {integrity: sha512-aREYa3aku9SSnea4aX6bhKn4bgv3AXkgijoQgbYV3yvbiGt6z+MQ85+6mIhx9DsKW2BuB/cLR/A+tcMThx+KLQ==}
+ '@jest/types@30.2.0':
+ resolution: {integrity: sha512-H9xg1/sfVvyfU7o3zMfBEjQ1gcsdeTMgqHoYdN79tuLqfTtuu7WckRA1R5whDwOzxaZAeMKTYWqP+WCAi0CHsg==}
engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0}
'@jridgewell/gen-mapping@0.3.13':
@@ -633,15 +739,62 @@ packages:
'@jridgewell/sourcemap-codec@1.5.5':
resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==}
- '@jridgewell/trace-mapping@0.3.30':
- resolution: {integrity: sha512-GQ7Nw5G2lTu/BtHTKfXhKHok2WGetd4XYcVKGx00SjAk8GMwgJM3zr6zORiPGuOE+/vkc90KtTosSSvaCjKb2Q==}
+ '@jridgewell/trace-mapping@0.3.31':
+ resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==}
'@jridgewell/trace-mapping@0.3.9':
resolution: {integrity: sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==}
+ '@lukeed/csprng@1.1.0':
+ resolution: {integrity: sha512-Z7C/xXCiGWsg0KuKsHTKJxbWhpI3Vs5GwLfOean7MGyVFGqdRgBbAjOCh6u4bbjPc/8MJ2pZmK/0DLdCbivLDA==}
+ engines: {node: '>=8'}
+
+ '@manypkg/find-root@1.1.0':
+ resolution: {integrity: sha512-mki5uBvhHzO8kYYix/WRy2WX8S3B5wdVSc9D6KcU5lQNglP2yt58/VfLuAK49glRXChosY8ap2oJ1qgma3GUVA==}
+
+ '@manypkg/get-packages@1.1.3':
+ resolution: {integrity: sha512-fo+QhuU3qE/2TQMQmbVMqaQ6EWbMhi4ABWP+O4AM1NqPBuy0OrApV5LO6BrrgnhtAHS2NH6RrVk9OL181tTi8A==}
+
'@napi-rs/wasm-runtime@0.2.12':
resolution: {integrity: sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ==}
+ '@nestjs/common@11.1.14':
+ resolution: {integrity: sha512-IN/tlqd7Nl9gl6f0jsWEuOrQDaCI9vHzxv0fisHysfBQzfQIkqlv5A7w4Qge02BUQyczXT9HHPgHtWHCxhjRng==}
+ peerDependencies:
+ class-transformer: '>=0.4.1'
+ class-validator: '>=0.13.2'
+ reflect-metadata: ^0.1.12 || ^0.2.0
+ rxjs: ^7.1.0
+ peerDependenciesMeta:
+ class-transformer:
+ optional: true
+ class-validator:
+ optional: true
+
+ '@nestjs/config@4.0.3':
+ resolution: {integrity: sha512-FQ3M3Ohqfl+nHAn5tp7++wUQw0f2nAk+SFKe8EpNRnIifPqvfJP6JQxPKtFLMOHbyer4X646prFG4zSRYEssQQ==}
+ peerDependencies:
+ '@nestjs/common': ^10.0.0 || ^11.0.0
+ rxjs: ^7.1.0
+
+ '@nestjs/core@11.1.14':
+ resolution: {integrity: sha512-7OXPPMoDr6z+5NkoQKu4hOhfjz/YYqM3bNilPqv1WVFWrzSmuNXxvhbX69YMmNmRYascPXiwESqf5jJdjKXEww==}
+ engines: {node: '>= 20'}
+ peerDependencies:
+ '@nestjs/common': ^11.0.0
+ '@nestjs/microservices': ^11.0.0
+ '@nestjs/platform-express': ^11.0.0
+ '@nestjs/websockets': ^11.0.0
+ reflect-metadata: ^0.1.12 || ^0.2.0
+ rxjs: ^7.1.0
+ peerDependenciesMeta:
+ '@nestjs/microservices':
+ optional: true
+ '@nestjs/platform-express':
+ optional: true
+ '@nestjs/websockets':
+ optional: true
+
'@noble/ciphers@1.3.0':
resolution: {integrity: sha512-2I0gnIVPtfnMw9ee9h1dJG7tp81+8Ob3OJb3Mv37rx5L40/b0i7djjCVvGOVqc9AEIQyvyu1i6ypKdFw8R8gQw==}
engines: {node: ^14.21.3 || >=16}
@@ -684,6 +837,11 @@ packages:
resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==}
engines: {node: '>= 8'}
+ '@nuxt/opencollective@0.4.1':
+ resolution: {integrity: sha512-GXD3wy50qYbxCJ652bDrDzgMr3NFEkIS374+IgFQKkCvk9yiYcLvX2XDYr7UyQxf4wK0e+yqDYRubZ0DtOxnmQ==}
+ engines: {node: ^14.18.0 || >=16.10.0, npm: '>=5.10.0'}
+ hasBin: true
+
'@pkgjs/parseargs@0.11.0':
resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==}
engines: {node: '>=14'}
@@ -710,8 +868,23 @@ packages:
'@scure/bip39@1.6.0':
resolution: {integrity: sha512-+lF0BbLiJNwVlev4eKelw1WWLaiKXw7sSl8T6FvBlWkdX+94aGJ4o8XjUdlyhTCjd8c+B3KT3JfS8P0bLRNU6A==}
- '@sinclair/typebox@0.34.41':
- resolution: {integrity: sha512-6gS8pZzSXdyRHTIqoqSVknxolr1kzfy4/CeDnrzsVz8TTIWUbOBr6gnzOmTYJ3eXQNh4IYHIGi5aIL7sOZ2G/g==}
+ '@shikijs/engine-oniguruma@3.22.0':
+ resolution: {integrity: sha512-DyXsOG0vGtNtl7ygvabHd7Mt5EY8gCNqR9Y7Lpbbd/PbJvgWrqaKzH1JW6H6qFkuUa8aCxoiYVv8/YfFljiQxA==}
+
+ '@shikijs/langs@3.22.0':
+ resolution: {integrity: sha512-x/42TfhWmp6H00T6uwVrdTJGKgNdFbrEdhaDwSR5fd5zhQ1Q46bHq9EO61SCEWJR0HY7z2HNDMaBZp8JRmKiIA==}
+
+ '@shikijs/themes@3.22.0':
+ resolution: {integrity: sha512-o+tlOKqsr6FE4+mYJG08tfCFDS+3CG20HbldXeVoyP+cYSUxDhrFf3GPjE60U55iOkkjbpY2uC3It/eeja35/g==}
+
+ '@shikijs/types@3.22.0':
+ resolution: {integrity: sha512-491iAekgKDBFE67z70Ok5a8KBMsQ2IJwOWw3us/7ffQkIBCyOQfm/aNwVMBUriP02QshIfgHCBSIYAl3u2eWjg==}
+
+ '@shikijs/vscode-textmate@10.0.2':
+ resolution: {integrity: sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg==}
+
+ '@sinclair/typebox@0.34.48':
+ resolution: {integrity: sha512-kKJTNuK3AQOrgjjotVxMrCn1sUJwM76wMszfq1kdU4uYVJjvEWuFQ6HgvLt4Xz3fSmZlTOxJ/Ie13KnIcWQXFA==}
'@sinonjs/commons@3.0.1':
resolution: {integrity: sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==}
@@ -807,6 +980,13 @@ packages:
'@swc/helpers@0.5.17':
resolution: {integrity: sha512-5IKx/Y13RsYd+sauPb2x+U/xZikHjolzfuDgTAl/Tdf3Q8rslRvC19NKDLgAJQ6wsqADk10ntlv08nPFw/gO/A==}
+ '@tokenizer/inflate@0.4.1':
+ resolution: {integrity: sha512-2mAv+8pkG6GIZiF1kNg1jAjh27IDxEPKwdGul3snfztFerfPGI1LjDezZp3i7BElXompqEtPmoPx6c2wgtWsOA==}
+ engines: {node: '>=18'}
+
+ '@tokenizer/token@0.3.0':
+ resolution: {integrity: sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A==}
+
'@tsconfig/node10@1.0.11':
resolution: {integrity: sha512-DcRjDCujK/kCk/cUe8Xz8ZSpm8mS3mNNpta+jGCA6USEDfktlNvm1+IuZ9eTcDbNk41BHwpHHeW+N1lKCz4zOw==}
@@ -819,8 +999,8 @@ packages:
'@tsconfig/node16@1.0.4':
resolution: {integrity: sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==}
- '@tybys/wasm-util@0.10.0':
- resolution: {integrity: sha512-VyyPYFlOMNylG45GoAe0xDoLwWuowvf92F9kySqzYh8vmYm7D2u4iUJKa1tOUpS70Ku13ASrOkS4ScXFsTaCNQ==}
+ '@tybys/wasm-util@0.10.1':
+ resolution: {integrity: sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==}
'@types/babel__core@7.20.5':
resolution: {integrity: sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==}
@@ -840,9 +1020,15 @@ packages:
'@types/connect@3.4.38':
resolution: {integrity: sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==}
+ '@types/esrecurse@4.3.1':
+ resolution: {integrity: sha512-xJBAbDifo5hpffDBuHl0Y8ywswbiAp/Wi7Y/GtAgSlZyIABppyurxVueOPE8LUQOxdlgi6Zqce7uoEpqNTeiUw==}
+
'@types/estree@1.0.8':
resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==}
+ '@types/hast@3.0.4':
+ resolution: {integrity: sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==}
+
'@types/inquirer@9.0.9':
resolution: {integrity: sha512-/mWx5136gts2Z2e5izdoRCo46lPp5TMs9R15GTSsgg/XnZyxDWVqoVU3R9lWnccKpqwsJLvRoxbCjoJtZB7DSw==}
@@ -876,6 +1062,9 @@ packages:
'@types/through@0.0.33':
resolution: {integrity: sha512-HsJ+z3QuETzP3cswwtzt2vEIiHBk/dCcHGhbmG5X3ecnwFD/lPrMpliGXxSCg03L9AhrdwA4Oz/qfspkDW+xGQ==}
+ '@types/unist@3.0.3':
+ resolution: {integrity: sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==}
+
'@types/uuid@8.3.4':
resolution: {integrity: sha512-c/I8ZRb51j+pYGAu5CrFMRxqZ2ke4y2grEBO5AUjgSkSk+qT2Ea+OdWElz/OiMf5MNpn2b17kuVBwZLQJXzihw==}
@@ -888,66 +1077,66 @@ packages:
'@types/yargs-parser@21.0.3':
resolution: {integrity: sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==}
- '@types/yargs@17.0.33':
- resolution: {integrity: sha512-WpxBCKWPLr4xSsHgz511rFJAM+wS28w2zEO1QDNY5zM/S8ok70NNfztH0xwhqKyaK0OHCbN98LDAZuy1ctxDkA==}
+ '@types/yargs@17.0.35':
+ resolution: {integrity: sha512-qUHkeCyQFxMXg79wQfTtfndEC+N9ZZg76HJftDJp+qH2tV7Gj4OJi7l+PiWwJ+pWtW8GwSmqsDj/oymhrTWXjg==}
- '@typescript-eslint/eslint-plugin@8.43.0':
- resolution: {integrity: sha512-8tg+gt7ENL7KewsKMKDHXR1vm8tt9eMxjJBYINf6swonlWgkYn5NwyIgXpbbDxTNU5DgpDFfj95prcTq2clIQQ==}
+ '@typescript-eslint/eslint-plugin@8.56.0':
+ resolution: {integrity: sha512-lRyPDLzNCuae71A3t9NEINBiTn7swyOhvUj3MyUOxb8x6g6vPEFoOU+ZRmGMusNC3X3YMhqMIX7i8ShqhT74Pw==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
peerDependencies:
- '@typescript-eslint/parser': ^8.43.0
- eslint: ^8.57.0 || ^9.0.0
+ '@typescript-eslint/parser': ^8.56.0
+ eslint: ^8.57.0 || ^9.0.0 || ^10.0.0
typescript: '>=4.8.4 <6.0.0'
- '@typescript-eslint/parser@8.43.0':
- resolution: {integrity: sha512-B7RIQiTsCBBmY+yW4+ILd6mF5h1FUwJsVvpqkrgpszYifetQ2Ke+Z4u6aZh0CblkUGIdR59iYVyXqqZGkZ3aBw==}
+ '@typescript-eslint/parser@8.56.0':
+ resolution: {integrity: sha512-IgSWvLobTDOjnaxAfDTIHaECbkNlAlKv2j5SjpB2v7QHKv1FIfjwMy8FsDbVfDX/KjmCmYICcw7uGaXLhtsLNg==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
peerDependencies:
- eslint: ^8.57.0 || ^9.0.0
+ eslint: ^8.57.0 || ^9.0.0 || ^10.0.0
typescript: '>=4.8.4 <6.0.0'
- '@typescript-eslint/project-service@8.43.0':
- resolution: {integrity: sha512-htB/+D/BIGoNTQYffZw4uM4NzzuolCoaA/BusuSIcC8YjmBYQioew5VUZAYdAETPjeed0hqCaW7EHg+Robq8uw==}
+ '@typescript-eslint/project-service@8.56.0':
+ resolution: {integrity: sha512-M3rnyL1vIQOMeWxTWIW096/TtVP+8W3p/XnaFflhmcFp+U4zlxUxWj4XwNs6HbDeTtN4yun0GNTTDBw/SvufKg==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
peerDependencies:
typescript: '>=4.8.4 <6.0.0'
- '@typescript-eslint/scope-manager@8.43.0':
- resolution: {integrity: sha512-daSWlQ87ZhsjrbMLvpuuMAt3y4ba57AuvadcR7f3nl8eS3BjRc8L9VLxFLk92RL5xdXOg6IQ+qKjjqNEimGuAg==}
+ '@typescript-eslint/scope-manager@8.56.0':
+ resolution: {integrity: sha512-7UiO/XwMHquH+ZzfVCfUNkIXlp/yQjjnlYUyYz7pfvlK3/EyyN6BK+emDmGNyQLBtLGaYrTAI6KOw8tFucWL2w==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
- '@typescript-eslint/tsconfig-utils@8.43.0':
- resolution: {integrity: sha512-ALC2prjZcj2YqqL5X/bwWQmHA2em6/94GcbB/KKu5SX3EBDOsqztmmX1kMkvAJHzxk7TazKzJfFiEIagNV3qEA==}
+ '@typescript-eslint/tsconfig-utils@8.56.0':
+ resolution: {integrity: sha512-bSJoIIt4o3lKXD3xmDh9chZcjCz5Lk8xS7Rxn+6l5/pKrDpkCwtQNQQwZ2qRPk7TkUYhrq3WPIHXOXlbXP0itg==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
peerDependencies:
typescript: '>=4.8.4 <6.0.0'
- '@typescript-eslint/type-utils@8.43.0':
- resolution: {integrity: sha512-qaH1uLBpBuBBuRf8c1mLJ6swOfzCXryhKND04Igr4pckzSEW9JX5Aw9AgW00kwfjWJF0kk0ps9ExKTfvXfw4Qg==}
+ '@typescript-eslint/type-utils@8.56.0':
+ resolution: {integrity: sha512-qX2L3HWOU2nuDs6GzglBeuFXviDODreS58tLY/BALPC7iu3Fa+J7EOTwnX9PdNBxUI7Uh0ntP0YWGnxCkXzmfA==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
peerDependencies:
- eslint: ^8.57.0 || ^9.0.0
+ eslint: ^8.57.0 || ^9.0.0 || ^10.0.0
typescript: '>=4.8.4 <6.0.0'
- '@typescript-eslint/types@8.43.0':
- resolution: {integrity: sha512-vQ2FZaxJpydjSZJKiSW/LJsabFFvV7KgLC5DiLhkBcykhQj8iK9BOaDmQt74nnKdLvceM5xmhaTF+pLekrxEkw==}
+ '@typescript-eslint/types@8.56.0':
+ resolution: {integrity: sha512-DBsLPs3GsWhX5HylbP9HNG15U0bnwut55Lx12bHB9MpXxQ+R5GC8MwQe+N1UFXxAeQDvEsEDY6ZYwX03K7Z6HQ==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
- '@typescript-eslint/typescript-estree@8.43.0':
- resolution: {integrity: sha512-7Vv6zlAhPb+cvEpP06WXXy/ZByph9iL6BQRBDj4kmBsW98AqEeQHlj/13X+sZOrKSo9/rNKH4Ul4f6EICREFdw==}
+ '@typescript-eslint/typescript-estree@8.56.0':
+ resolution: {integrity: sha512-ex1nTUMWrseMltXUHmR2GAQ4d+WjkZCT4f+4bVsps8QEdh0vlBsaCokKTPlnqBFqqGaxilDNJG7b8dolW2m43Q==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
peerDependencies:
typescript: '>=4.8.4 <6.0.0'
- '@typescript-eslint/utils@8.43.0':
- resolution: {integrity: sha512-S1/tEmkUeeswxd0GGcnwuVQPFWo8NzZTOMxCvw8BX7OMxnNae+i8Tm7REQen/SwUIPoPqfKn7EaZ+YLpiB3k9g==}
+ '@typescript-eslint/utils@8.56.0':
+ resolution: {integrity: sha512-RZ3Qsmi2nFGsS+n+kjLAYDPVlrzf7UhTffrDIKr+h2yzAlYP/y5ZulU0yeDEPItos2Ph46JAL5P/On3pe7kDIQ==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
peerDependencies:
- eslint: ^8.57.0 || ^9.0.0
+ eslint: ^8.57.0 || ^9.0.0 || ^10.0.0
typescript: '>=4.8.4 <6.0.0'
- '@typescript-eslint/visitor-keys@8.43.0':
- resolution: {integrity: sha512-T+S1KqRD4sg/bHfLwrpF/K3gQLBM1n7Rp7OjjikjTEssI2YJzQpi5WXoynOaQ93ERIuq3O8RBTOUYDKszUCEHw==}
+ '@typescript-eslint/visitor-keys@8.56.0':
+ resolution: {integrity: sha512-q+SL+b+05Ud6LbEE35qe4A99P+htKTKVbyiNEe45eCbJFyh/HVK9QXwlrbz+Q4L8SOW4roxSVwXYj4DMBT7Ieg==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
'@ungap/structured-clone@1.3.0':
@@ -1048,6 +1237,10 @@ packages:
cpu: [x64]
os: [win32]
+ '@vercel/ncc@0.38.4':
+ resolution: {integrity: sha512-8LwjnlP39s08C08J5NstzriPvW1SP8Zfpp1BvC2sI35kPeZnHfxVkCwu4/+Wodgnd60UtT1n8K8zw+Mp7J9JmQ==}
+ hasBin: true
+
abitype@1.1.0:
resolution: {integrity: sha512-6Vh4HcRxNMLA0puzPjM5GBgT4aAcFGKZzSgAXvuZ27shJP6NEpielTuqbBmZILR5/xd0PizkBGy5hReKz9jl5A==}
peerDependencies:
@@ -1073,6 +1266,11 @@ packages:
engines: {node: '>=0.4.0'}
hasBin: true
+ acorn@8.16.0:
+ resolution: {integrity: sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==}
+ engines: {node: '>=0.4.0'}
+ hasBin: true
+
aes-js@4.0.0-beta.5:
resolution: {integrity: sha512-G965FqalsNyrPqgEGON7nIx1e/OVENSgiEIzyC63haUMuvNnwIgIjMs52hlTCKhkBny7A2ORNlfY9Zu+jmGk1Q==}
@@ -1083,6 +1281,10 @@ packages:
ajv@6.12.6:
resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==}
+ ansi-colors@4.1.3:
+ resolution: {integrity: sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==}
+ engines: {node: '>=6'}
+
ansi-escapes@4.3.2:
resolution: {integrity: sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==}
engines: {node: '>=8'}
@@ -1099,6 +1301,10 @@ packages:
resolution: {integrity: sha512-TKY5pyBkHyADOPYlRT9Lx6F544mPl0vS5Ew7BJ45hA08Q+t3GjbueLliBWN3sMICk6+y7HdyxSzC4bWS8baBdg==}
engines: {node: '>=12'}
+ ansi-regex@6.2.2:
+ resolution: {integrity: sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==}
+ engines: {node: '>=12'}
+
ansi-styles@4.3.0:
resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==}
engines: {node: '>=8'}
@@ -1111,6 +1317,10 @@ packages:
resolution: {integrity: sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==}
engines: {node: '>=12'}
+ ansi-styles@6.2.3:
+ resolution: {integrity: sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==}
+ engines: {node: '>=12'}
+
anymatch@3.1.3:
resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==}
engines: {node: '>= 8'}
@@ -1124,24 +1334,28 @@ packages:
argparse@2.0.1:
resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==}
+ array-union@2.1.0:
+ resolution: {integrity: sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==}
+ engines: {node: '>=8'}
+
asynckit@0.4.0:
resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==}
- axios@1.11.0:
- resolution: {integrity: sha512-1Lx3WLFQWm3ooKDYZD1eXmoGO9fxYQjrycfHFC8P0sCfQVXyROp0p9PFWBehewBOdCwHc+f/b8I0fMto5eSfwA==}
+ axios@1.13.5:
+ resolution: {integrity: sha512-cz4ur7Vb0xS4/KUN0tPWe44eqxrIu31me+fbang3ijiNscE129POzipJJA6zniq2C/Z6sJCjMimjS8Lc/GAs8Q==}
- babel-jest@30.1.2:
- resolution: {integrity: sha512-IQCus1rt9kaSh7PQxLYRY5NmkNrNlU2TpabzwV7T2jljnpdHOcmnYYv8QmE04Li4S3a2Lj8/yXyET5pBarPr6g==}
+ babel-jest@30.2.0:
+ resolution: {integrity: sha512-0YiBEOxWqKkSQWL9nNGGEgndoeL0ZpWrbLMNL5u/Kaxrli3Eaxlt3ZtIDktEvXt4L/R9r3ODr2zKwGM/2BjxVw==}
engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0}
peerDependencies:
- '@babel/core': ^7.11.0
+ '@babel/core': ^7.11.0 || ^8.0.0-0
babel-plugin-istanbul@7.0.1:
resolution: {integrity: sha512-D8Z6Qm8jCvVXtIRkBnqNHX0zJ37rQcFJ9u8WOS6tkYOsRdHBzypCstaxWiu5ZIlqQtviRYbgnRLSoCEvjqcqbA==}
engines: {node: '>=12'}
- babel-plugin-jest-hoist@30.0.1:
- resolution: {integrity: sha512-zTPME3pI50NsFW8ZBaVIOeAxzEY7XHlmWeXXu9srI+9kNfzCUTy8MFan46xOGZY8NZThMqq+e3qZUKsvXbasnQ==}
+ babel-plugin-jest-hoist@30.2.0:
+ resolution: {integrity: sha512-ftzhzSGMUnOzcCXd6WHdBGMyuwy15Wnn0iyyWGKgBDLxf9/s5ABuraCSpBX2uG0jUg4rqJnxsLc5+oYBqoxVaA==}
engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0}
babel-preset-current-node-syntax@1.2.0:
@@ -1149,14 +1363,15 @@ packages:
peerDependencies:
'@babel/core': ^7.0.0 || ^8.0.0-0
- babel-preset-jest@30.0.1:
- resolution: {integrity: sha512-+YHejD5iTWI46cZmcc/YtX4gaKBtdqCHCVfuVinizVpbmyjO3zYmeuyFdfA8duRqQZfgCAMlsfmkVbJ+e2MAJw==}
+ babel-preset-jest@30.2.0:
+ resolution: {integrity: sha512-US4Z3NOieAQumwFnYdUWKvUKh8+YSnS/gB3t6YBiz0bskpu7Pine8pPCheNxlPEW4wnUkma2a94YuW2q3guvCQ==}
engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0}
peerDependencies:
- '@babel/core': ^7.11.0
+ '@babel/core': ^7.11.0 || ^8.0.0-beta.1
- balanced-match@1.0.2:
- resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==}
+ balanced-match@4.0.3:
+ resolution: {integrity: sha512-1pHv8LX9CpKut1Zp4EXey7Z8OfH11ONNH6Dhi2WDUt31VVZFXZzKwXcysBgqSumFCmR+0dqjMK5v5JiFHzi0+g==}
+ engines: {node: 20 || >=22}
base-x@3.0.11:
resolution: {integrity: sha512-xz7wQ8xDhdyP7tQxwdteLYeFfS68tSMNCZ/Y37WJ4bhGfKPpqEIlmIyueQHqOyoPhE6xNUqjzRr8ra0eF9VRvA==}
@@ -1164,6 +1379,15 @@ packages:
base64-js@1.5.1:
resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==}
+ baseline-browser-mapping@2.10.0:
+ resolution: {integrity: sha512-lIyg0szRfYbiy67j9KN8IyeD7q7hcmqnJ1ddWmNt19ItGpNN64mnllmxUNFIOdOm6by97jlL6wfpTTJrmnjWAA==}
+ engines: {node: '>=6.0.0'}
+ hasBin: true
+
+ better-path-resolve@1.0.0:
+ resolution: {integrity: sha512-pbnl5XzGBdrFU/wT4jqmJVPn2B6UHPBOhzMQkY/SPUPB6QtUXtmBHBIwCbXJol93mOpGMnQyP/+BB19q04xj7g==}
+ engines: {node: '>=4'}
+
bigint-buffer@1.1.5:
resolution: {integrity: sha512-trfYco6AoZ+rKhKnxA0hgX0HAbVP/s808/EuDSe2JDzUnCp/xAsli35Orvk67UrTEcwuxZqYZDmfA2RXJgxVvA==}
engines: {node: '>= 10.0.0'}
@@ -1171,30 +1395,31 @@ packages:
bignumber.js@9.1.2:
resolution: {integrity: sha512-2/mKyZH9K85bzOEfhXDBFZTGd1CTs+5IHpeFQo9luiBG7hghdC851Pj2WAhb6E3R6b9tZj/XKhbg4fum+Kepug==}
+ bignumber.js@9.3.1:
+ resolution: {integrity: sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ==}
+
bindings@1.5.0:
resolution: {integrity: sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==}
bl@4.1.0:
resolution: {integrity: sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==}
- bn.js@5.2.2:
- resolution: {integrity: sha512-v2YAxEmKaBLahNwE1mjp4WON6huMNeuDvagFZW+ASCuA/ku0bXR9hSMw0XpiqMoA3+rmnyck/tPRSFQkoC9Cuw==}
+ bn.js@5.2.3:
+ resolution: {integrity: sha512-EAcmnPkxpntVL+DS7bO1zhcZNvCkxqtkd0ZY53h06GNQ3DEkkGZ/gKgmDv6DdZQGj9BgfSPKtJJ7Dp1GPP8f7w==}
borsh@0.7.0:
resolution: {integrity: sha512-CLCsZGIBCFnPtkNnieW/a8wmreDmfUtjU2m9yHrzPXIlNbqVs0AQrSatSG6vdNYUqdc83tkQi2eHfF98ubzQLA==}
- brace-expansion@1.1.12:
- resolution: {integrity: sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==}
-
- brace-expansion@2.0.2:
- resolution: {integrity: sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==}
+ brace-expansion@5.0.2:
+ resolution: {integrity: sha512-Pdk8c9poy+YhOgVWw1JNN22/HcivgKWwpxKq04M/jTmHyCZn12WPJebZxdjSa5TmBqISrUSgNYU3eRORljfCCw==}
+ engines: {node: 20 || >=22}
braces@3.0.3:
resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==}
engines: {node: '>=8'}
- browserslist@4.25.4:
- resolution: {integrity: sha512-4jYpcjabC606xJ3kw2QwGEZKX0Aw7sgQdZCvIK9dhVSPh76BKo+C+btT1RRofH7B+8iNpEbgGNVWiLki5q93yg==}
+ browserslist@4.28.1:
+ resolution: {integrity: sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==}
engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7}
hasBin: true
@@ -1241,8 +1466,8 @@ packages:
resolution: {integrity: sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==}
engines: {node: '>=10'}
- caniuse-lite@1.0.30001741:
- resolution: {integrity: sha512-QGUGitqsc8ARjLdgAfxETDhRbJ0REsP6O3I96TAth/mVjh2cYzN2u+3AzPP3aVSm2FehEItaJw1xd+IGBXWeSw==}
+ caniuse-lite@1.0.30001770:
+ resolution: {integrity: sha512-x/2CLQ1jHENRbHg5PSId2sXq1CIO1CISvwWAj027ltMVG2UNgW+w9oH2+HzgEIRFembL8bUlXtfbBHR1fCg2xw==}
chalk@4.1.2:
resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==}
@@ -1252,6 +1477,10 @@ packages:
resolution: {integrity: sha512-46QrSQFyVSEyYAgQ22hQ+zDa60YHA4fBstHmtSApj1Y5vKtG27fWowW03jCk5KcbXEWPZUIR894aARCA/G1kfQ==}
engines: {node: ^12.17.0 || ^14.13 || >=16.0.0}
+ chalk@5.6.2:
+ resolution: {integrity: sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==}
+ engines: {node: ^12.17.0 || ^14.13 || >=16.0.0}
+
char-regex@1.0.2:
resolution: {integrity: sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==}
engines: {node: '>=10'}
@@ -1259,12 +1488,19 @@ packages:
chardet@0.7.0:
resolution: {integrity: sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==}
- ci-info@4.3.0:
- resolution: {integrity: sha512-l+2bNRMiQgcfILUi33labAZYIWlH1kWDp+ecNo5iisRKrbm0xcRyCww71/YU0Fkw0mAFpz9bJayXPjey6vkmaQ==}
+ chardet@2.1.1:
+ resolution: {integrity: sha512-PsezH1rqdV9VvyNhxxOW32/d75r01NY7TQCmOqomRo15ZSOKbpTFVsfjghxo6JloQUCGnH4k1LGu0R4yCLlWQQ==}
+
+ ci-info@3.9.0:
+ resolution: {integrity: sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==}
engines: {node: '>=8'}
- cjs-module-lexer@2.1.0:
- resolution: {integrity: sha512-UX0OwmYRYQQetfrLEZeewIFFI+wSTofC+pMBLNuH3RUuu/xzG1oz84UCEDOSoQlN3fZ4+AzmV50ZYvGqkMh9yA==}
+ ci-info@4.4.0:
+ resolution: {integrity: sha512-77PSwercCZU2Fc4sX94eF8k8Pxte6JAwL4/ICZLFjJLqegs7kCuAsqqj/70NQF6TvDpgFjkubQB2FW2ZZddvQg==}
+ engines: {node: '>=8'}
+
+ cjs-module-lexer@2.2.0:
+ resolution: {integrity: sha512-4bHTS2YuzUvtoLjdy+98ykbNB5jS0+07EvFNXerqZQJ89F7DI6ET7OQo/HJuW6K0aVsKA9hj9/RVb2kQVOrPDQ==}
cli-cursor@3.1.0:
resolution: {integrity: sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==}
@@ -1286,6 +1522,10 @@ packages:
resolution: {integrity: sha512-nPdaFdQ0h/GEigbPClz11D0v/ZJEwxmeVZGeMo3Z5StPtUTkA9o1lD6QwoirYiSDzbcwn2XcjwmCp68W1IS4TA==}
engines: {node: '>=18'}
+ cli-width@3.0.0:
+ resolution: {integrity: sha512-FxqpkPPwu1HjuN93Omfm4h8uIanXofW0RxVEW3k5RKx+mJJYSthzNhp32Kzxxy3YAEZ/Dc/EWN1vZRY0+kOhbw==}
+ engines: {node: '>= 10'}
+
cli-width@4.1.0:
resolution: {integrity: sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==}
engines: {node: '>= 12'}
@@ -1302,8 +1542,8 @@ packages:
resolution: {integrity: sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==}
engines: {iojs: '>= 1.0.0', node: '>= 0.12.0'}
- collect-v8-coverage@1.0.2:
- resolution: {integrity: sha512-lHl4d5/ONEbLlJvaJNtsF/Lz+WvB07u2ycqTYbdrq7UypDXailES4valYb2eWiJFxZlVmpGekfqoxQhzyFdT4Q==}
+ collect-v8-coverage@1.0.3:
+ resolution: {integrity: sha512-1L5aqIkwPfiodaMgQunkF1zRhNqifHBmtbbbxcr6yVxxBnliw4TDOW6NxpO8DJLgJ16OT+Y4ztZqP6p/FtXnAw==}
color-convert@2.0.1:
resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==}
@@ -1319,6 +1559,10 @@ packages:
resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==}
engines: {node: '>= 0.8'}
+ commander@11.1.0:
+ resolution: {integrity: sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ==}
+ engines: {node: '>=16'}
+
commander@12.1.0:
resolution: {integrity: sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==}
engines: {node: '>=18'}
@@ -1330,12 +1574,22 @@ packages:
commander@2.20.3:
resolution: {integrity: sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==}
- concat-map@0.0.1:
- resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==}
+ consola@3.4.2:
+ resolution: {integrity: sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA==}
+ engines: {node: ^14.18.0 || >=16.10.0}
convert-source-map@2.0.0:
resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==}
+ cosmiconfig@8.3.6:
+ resolution: {integrity: sha512-kcZ6+W5QzcJ3P1Mt+83OUv/oHFqZHIx8DuxG6eZ5RGMERoLqp4BuGjhHLYGK+Kf5XVkQvqBSmAy/nGWN3qDgEA==}
+ engines: {node: '>=14'}
+ peerDependencies:
+ typescript: '>=4.9.5'
+ peerDependenciesMeta:
+ typescript:
+ optional: true
+
create-require@1.1.1:
resolution: {integrity: sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==}
@@ -1355,8 +1609,17 @@ packages:
supports-color:
optional: true
- dedent@1.7.0:
- resolution: {integrity: sha512-HGFtf8yhuhGhqO07SV79tRp+br4MnbdjeVxotpn1QBl30pcLLCQjX5b2295ll0fv8RKDKsmWYrl05usHM9CewQ==}
+ debug@4.4.3:
+ resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==}
+ engines: {node: '>=6.0'}
+ peerDependencies:
+ supports-color: '*'
+ peerDependenciesMeta:
+ supports-color:
+ optional: true
+
+ dedent@1.7.1:
+ resolution: {integrity: sha512-9JmrhGZpOlEgOLdQgSm0zxFaYoQon408V1v49aqTWuXENVlnCuY9JBZcXZiCsZQWDjTm5Qf/nIvAy77mXDAjEg==}
peerDependencies:
babel-plugin-macros: ^3.1.0
peerDependenciesMeta:
@@ -1381,6 +1644,10 @@ packages:
resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==}
engines: {node: '>=0.4.0'}
+ detect-indent@6.1.0:
+ resolution: {integrity: sha512-reYkTUJAZb9gUuZ2RvVCNhVHdg62RHnJ7WJl8ftMi4diZ6NWlciOzQN88pUhSELEwflJht4oQDv0F0BMlwaYtA==}
+ engines: {node: '>=8'}
+
detect-newline@3.1.0:
resolution: {integrity: sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==}
engines: {node: '>=8'}
@@ -1389,10 +1656,22 @@ packages:
resolution: {integrity: sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==}
engines: {node: '>=0.3.1'}
+ dir-glob@3.0.1:
+ resolution: {integrity: sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==}
+ engines: {node: '>=8'}
+
+ dotenv-expand@12.0.3:
+ resolution: {integrity: sha512-uc47g4b+4k/M/SeaW1y4OApx+mtLWl92l5LMPP0GNXctZqELk+YGgOPIIC5elYmUH4OuoK3JLhuRUYegeySiFA==}
+ engines: {node: '>=12'}
+
dotenv@16.6.1:
resolution: {integrity: sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==}
engines: {node: '>=12'}
+ dotenv@17.2.3:
+ resolution: {integrity: sha512-JVUnt+DUIzu87TABbhPmNfVdBDt18BLOWjMUFJMSi/Qqg7NTYtabbvSNJGOJ7afbRuv9D/lngizHtP7QyLQ+9w==}
+ engines: {node: '>=12'}
+
dunder-proto@1.0.1:
resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==}
engines: {node: '>= 0.4'}
@@ -1400,8 +1679,8 @@ packages:
eastasianwidth@0.2.0:
resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==}
- electron-to-chromium@1.5.214:
- resolution: {integrity: sha512-TpvUNdha+X3ybfU78NoQatKvQEm1oq3lf2QbnmCEdw+Bd9RuIAY+hJTvq1avzHM0f7EJfnH3vbCnbzKzisc/9Q==}
+ electron-to-chromium@1.5.286:
+ resolution: {integrity: sha512-9tfDXhJ4RKFNerfjdCcZfufu49vg620741MNs26a9+bhLThdB+plgMeou98CAaHu/WATj2iHOOHTp1hWtABj2A==}
emittery@0.13.1:
resolution: {integrity: sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ==}
@@ -1416,12 +1695,20 @@ packages:
emoji-regex@9.2.2:
resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==}
+ enquirer@2.4.1:
+ resolution: {integrity: sha512-rRqJg/6gd538VHvR3PSrdRBb/1Vy2YfzHqzvbhGIQpDRKIa4FgV/54b5Q1xYSxOOwKvjXweS26E0Q+nAMwp2pQ==}
+ engines: {node: '>=8.6'}
+
+ entities@4.5.0:
+ resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==}
+ engines: {node: '>=0.12'}
+
environment@1.1.0:
resolution: {integrity: sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q==}
engines: {node: '>=18'}
- error-ex@1.3.2:
- resolution: {integrity: sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==}
+ error-ex@1.3.4:
+ resolution: {integrity: sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==}
es-define-property@1.0.1:
resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==}
@@ -1454,6 +1741,10 @@ packages:
resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==}
engines: {node: '>=6'}
+ escape-string-regexp@1.0.5:
+ resolution: {integrity: sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==}
+ engines: {node: '>=0.8.0'}
+
escape-string-regexp@2.0.0:
resolution: {integrity: sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==}
engines: {node: '>=8'}
@@ -1487,21 +1778,21 @@ packages:
peerDependencies:
eslint: '>=5.0.0'
- eslint-scope@8.4.0:
- resolution: {integrity: sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==}
- engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+ eslint-scope@9.1.0:
+ resolution: {integrity: sha512-CkWE42hOJsNj9FJRaoMX9waUFYhqY4jmyLFdAdzZr6VaCg3ynLYx4WnOdkaIifGfH4gsUcBTn4OZbHXkpLD0FQ==}
+ engines: {node: ^20.19.0 || ^22.13.0 || >=24}
eslint-visitor-keys@3.4.3:
resolution: {integrity: sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==}
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
- eslint-visitor-keys@4.2.1:
- resolution: {integrity: sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==}
- engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+ eslint-visitor-keys@5.0.0:
+ resolution: {integrity: sha512-A0XeIi7CXU7nPlfHS9loMYEKxUaONu/hTEzHTGba9Huu94Cq1hPivf+DE5erJozZOky0LfvXAyrV/tcswpLI0Q==}
+ engines: {node: ^20.19.0 || ^22.13.0 || >=24}
- eslint@9.35.0:
- resolution: {integrity: sha512-QePbBFMJFjgmlE+cXAlbHZbHpdFVS2E/6vzCy7aKlebddvl1vadiC4JFV5u/wqTkNUwEV8WrQi257jf5f06hrg==}
- engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+ eslint@10.0.0:
+ resolution: {integrity: sha512-O0piBKY36YSJhlFSG8p9VUdPV/SxxS4FYDWVpr/9GJuMaepzwlf4J8I4ov1b+ySQfDTPhc3DtLaxcT1fN0yqCg==}
+ engines: {node: ^20.19.0 || ^22.13.0 || >=24}
hasBin: true
peerDependencies:
jiti: '*'
@@ -1509,17 +1800,17 @@ packages:
jiti:
optional: true
- espree@10.4.0:
- resolution: {integrity: sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==}
- engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+ espree@11.1.0:
+ resolution: {integrity: sha512-WFWYhO1fV4iYkqOOvq8FbqIhr2pYfoDY0kCotMkDeNtGpiGGkZ1iov2u8ydjtgM8yF8rzK7oaTbw2NAzbAbehw==}
+ engines: {node: ^20.19.0 || ^22.13.0 || >=24}
esprima@4.0.1:
resolution: {integrity: sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==}
engines: {node: '>=4'}
hasBin: true
- esquery@1.6.0:
- resolution: {integrity: sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==}
+ esquery@1.7.0:
+ resolution: {integrity: sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==}
engines: {node: '>=0.10'}
esrecurse@4.3.0:
@@ -1555,10 +1846,13 @@ packages:
resolution: {integrity: sha512-+I6B/IkJc1o/2tiURyz/ivu/O0nKNEArIUB5O7zBrlDVJr22SCLH3xTeEry428LvFhRzIA1g8izguxJ/gbNcVQ==}
engines: {node: '>= 0.8.0'}
- expect@30.1.2:
- resolution: {integrity: sha512-xvHszRavo28ejws8FpemjhwswGj4w/BetHIL8cU49u4sGyXDw2+p3YbeDbj6xzlxi6kWTjIRSTJ+9sNXPnF0Zg==}
+ expect@30.2.0:
+ resolution: {integrity: sha512-u/feCi0GPsI+988gU2FLcsHyAHTU0MX1Wg68NhAnN7z/+C5wqG+CY8J53N9ioe8RXgaoz0nBR/TYMf3AycUuPw==}
engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0}
+ extendable-error@0.1.7:
+ resolution: {integrity: sha512-UOiS2in6/Q0FK0R0q6UY9vYpQ21mr/Qn1KOnte7vsACuNJf514WvCCUHSRCPcgjPT2bAhNIJdlE6bVap1GKmeg==}
+
external-editor@3.1.0:
resolution: {integrity: sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew==}
engines: {node: '>=4'}
@@ -1583,22 +1877,42 @@ packages:
fast-levenshtein@2.0.6:
resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==}
+ fast-safe-stringify@2.1.1:
+ resolution: {integrity: sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==}
+
fast-stable-stringify@1.0.0:
resolution: {integrity: sha512-wpYMUmFu5f00Sm0cj2pfivpmawLZ0NKdviQ4w9zJeR8JVtOpOxHmLaJuj0vxvGqMJQWyP/COUkF75/57OKyRag==}
fastestsmallesttextencoderdecoder@1.0.22:
resolution: {integrity: sha512-Pb8d48e+oIuY4MaM64Cd7OW1gt4nxCHs7/ddPPZ/Ic3sg8yVGM7O9wDvZ7us6ScaUupzM+pfBolwtYhN1IxBIw==}
- fastq@1.19.1:
- resolution: {integrity: sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==}
+ fastq@1.20.1:
+ resolution: {integrity: sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==}
fb-watchman@2.0.2:
resolution: {integrity: sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA==}
+ fdir@6.5.0:
+ resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==}
+ engines: {node: '>=12.0.0'}
+ peerDependencies:
+ picomatch: ^3 || ^4
+ peerDependenciesMeta:
+ picomatch:
+ optional: true
+
+ figures@3.2.0:
+ resolution: {integrity: sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg==}
+ engines: {node: '>=8'}
+
file-entry-cache@8.0.0:
resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==}
engines: {node: '>=16.0.0'}
+ file-type@21.3.0:
+ resolution: {integrity: sha512-8kPJMIGz1Yt/aPEwOsrR97ZyZaD1Iqm8PClb1nYFclUCkBi0Ma5IsYNQzvSFS9ib51lWyIw5mIT9rWzI/xjpzA==}
+ engines: {node: '>=20'}
+
file-uri-to-path@1.0.0:
resolution: {integrity: sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==}
@@ -1634,12 +1948,17 @@ packages:
resolution: {integrity: sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==}
engines: {node: '>=14'}
- form-data@4.0.4:
- resolution: {integrity: sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==}
+ form-data@4.0.5:
+ resolution: {integrity: sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==}
engines: {node: '>= 6'}
- fs.realpath@1.0.0:
- resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==}
+ fs-extra@7.0.1:
+ resolution: {integrity: sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==}
+ engines: {node: '>=6 <7 || >=8'}
+
+ fs-extra@8.1.0:
+ resolution: {integrity: sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==}
+ engines: {node: '>=6 <7 || >=8'}
fsevents@2.3.3:
resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==}
@@ -1688,18 +2007,23 @@ packages:
resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==}
engines: {node: '>=10.13.0'}
- glob@10.4.5:
- resolution: {integrity: sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==}
+ glob@10.5.0:
+ resolution: {integrity: sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==}
+ deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me
hasBin: true
- glob@7.2.3:
- resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==}
- deprecated: Glob versions prior to v9 are no longer supported
+ glob@13.0.6:
+ resolution: {integrity: sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw==}
+ engines: {node: 18 || 20 || >=22}
- globals@14.0.0:
- resolution: {integrity: sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==}
+ globals@17.3.0:
+ resolution: {integrity: sha512-yMqGUQVVCkD4tqjOJf3TnrvaaHDMYp4VlUSObbkIiuCPe/ofdMBFIAcBbCSRFWOnos6qRiTVStDwqPLUclaxIw==}
engines: {node: '>=18'}
+ globby@11.1.0:
+ resolution: {integrity: sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==}
+ engines: {node: '>=10'}
+
google-protobuf@3.21.4:
resolution: {integrity: sha512-MnG7N936zcKTco4Jd2PX2U96Kf9PxygAPKBug+74LHzmHXmceN16MmRcdgZv+DGef/S9YvQAfRsNCn4cjf9yyQ==}
@@ -1710,9 +2034,6 @@ packages:
graceful-fs@4.2.11:
resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==}
- graphemer@1.4.0:
- resolution: {integrity: sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==}
-
handlebars@4.7.8:
resolution: {integrity: sha512-vafaFqs8MZkRrSX7sFVUdo3ap/eNiLnb4IakshzvP56X5Nr1iGKAIqdX6tMlm6HcNRIkr6AxO5jFEoJzzpT8aQ==}
engines: {node: '>=0.4.7'}
@@ -1737,6 +2058,10 @@ packages:
html-escaper@2.0.2:
resolution: {integrity: sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==}
+ human-id@4.1.3:
+ resolution: {integrity: sha512-tsYlhAYpjCKa//8rXZ9DqKEawhPoSytweBC2eNvcaDK+57RZLHGqNs3PZTQO6yekLFSuvA6AlnAfrw1uBvtb+Q==}
+ hasBin: true
+
human-signals@2.1.0:
resolution: {integrity: sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==}
engines: {node: '>=10.17.0'}
@@ -1753,6 +2078,10 @@ packages:
resolution: {integrity: sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==}
engines: {node: '>=0.10.0'}
+ iconv-lite@0.7.2:
+ resolution: {integrity: sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==}
+ engines: {node: '>=0.10.0'}
+
ieee754@1.2.1:
resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==}
@@ -1777,13 +2106,13 @@ packages:
resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==}
engines: {node: '>=0.8.19'}
- inflight@1.0.6:
- resolution: {integrity: sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==}
- deprecated: This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.
-
inherits@2.0.4:
resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==}
+ inquirer@8.2.7:
+ resolution: {integrity: sha512-UjOaSel/iddGZJ5xP/Eixh6dY1XghiBw4XK13rCCIJcJfyhhoul/7KhLLUGtebEj6GDYM6Vnx/mVsjx2L/mFIA==}
+ engines: {node: '>=12.0.0'}
+
inquirer@9.3.7:
resolution: {integrity: sha512-LJKFHCSeIRq9hanN14IlOtPSTe3lNES7TYDTE2xxdAy1LS5rYphajK1qtwvj3YmQXvvk0U2Vbmcni8P9EIQW9w==}
engines: {node: '>=18'}
@@ -1831,6 +2160,10 @@ packages:
resolution: {integrity: sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==}
engines: {node: '>=8'}
+ is-subdir@1.2.0:
+ resolution: {integrity: sha512-2AT6j+gXe/1ueqbW6fLZJiIw3F8iXGJtt0yDrZaBhAZEG1raiTxKWU+IPqMCzQAXOUCKdA4UDMgacKH25XG2Cw==}
+ engines: {node: '>=4'}
+
is-unicode-supported@0.1.0:
resolution: {integrity: sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==}
engines: {node: '>=10'}
@@ -1843,6 +2176,10 @@ packages:
resolution: {integrity: sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ==}
engines: {node: '>=18'}
+ is-windows@1.0.2:
+ resolution: {integrity: sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==}
+ engines: {node: '>=0.10.0'}
+
isexe@2.0.0:
resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==}
@@ -1876,6 +2213,10 @@ packages:
resolution: {integrity: sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==}
engines: {node: '>=8'}
+ iterare@1.2.1:
+ resolution: {integrity: sha512-RKYVTCjAnRthyJes037NX/IiqeidgN1xc3j1RjFfECFp28A1GVwK9nA+i0rJPaHqSZwygLzRnFlzUuHFoWWy+Q==}
+ engines: {node: '>=6'}
+
jackspeak@3.4.3:
resolution: {integrity: sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==}
@@ -1884,16 +2225,16 @@ packages:
engines: {node: '>=8'}
hasBin: true
- jest-changed-files@30.0.5:
- resolution: {integrity: sha512-bGl2Ntdx0eAwXuGpdLdVYVr5YQHnSZlQ0y9HVDu565lCUAe9sj6JOtBbMmBBikGIegne9piDDIOeiLVoqTkz4A==}
+ jest-changed-files@30.2.0:
+ resolution: {integrity: sha512-L8lR1ChrRnSdfeOvTrwZMlnWV8G/LLjQ0nG9MBclwWZidA2N5FviRki0Bvh20WRMOX31/JYvzdqTJrk5oBdydQ==}
engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0}
- jest-circus@30.1.3:
- resolution: {integrity: sha512-Yf3dnhRON2GJT4RYzM89t/EXIWNxKTpWTL9BfF3+geFetWP4XSvJjiU1vrWplOiUkmq8cHLiwuhz+XuUp9DscA==}
+ jest-circus@30.2.0:
+ resolution: {integrity: sha512-Fh0096NC3ZkFx05EP2OXCxJAREVxj1BcW/i6EWqqymcgYKWjyyDpral3fMxVcHXg6oZM7iULer9wGRFvfpl+Tg==}
engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0}
- jest-cli@30.1.3:
- resolution: {integrity: sha512-G8E2Ol3OKch1DEeIBl41NP7OiC6LBhfg25Btv+idcusmoUSpqUkbrneMqbW9lVpI/rCKb/uETidb7DNteheuAQ==}
+ jest-cli@30.2.0:
+ resolution: {integrity: sha512-Os9ukIvADX/A9sLt6Zse3+nmHtHaE6hqOsjQtNiugFTbKRHYIYtZXNGNK9NChseXy7djFPjndX1tL0sCTlfpAA==}
engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0}
hasBin: true
peerDependencies:
@@ -1902,8 +2243,8 @@ packages:
node-notifier:
optional: true
- jest-config@30.1.3:
- resolution: {integrity: sha512-M/f7gqdQEPgZNA181Myz+GXCe8jXcJsGjCMXUzRj22FIXsZOyHNte84e0exntOvdPaeh9tA0w+B8qlP2fAezfw==}
+ jest-config@30.2.0:
+ resolution: {integrity: sha512-g4WkyzFQVWHtu6uqGmQR4CQxz/CH3yDSlhzXMWzNjDx843gYjReZnMRanjRCq5XZFuQrGDxgUaiYWE8BRfVckA==}
engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0}
peerDependencies:
'@types/node': '*'
@@ -1917,40 +2258,40 @@ packages:
ts-node:
optional: true
- jest-diff@30.1.2:
- resolution: {integrity: sha512-4+prq+9J61mOVXCa4Qp8ZjavdxzrWQXrI80GNxP8f4tkI2syPuPrJgdRPZRrfUTRvIoUwcmNLbqEJy9W800+NQ==}
+ jest-diff@30.2.0:
+ resolution: {integrity: sha512-dQHFo3Pt4/NLlG5z4PxZ/3yZTZ1C7s9hveiOj+GCN+uT109NC2QgsoVZsVOAvbJ3RgKkvyLGXZV9+piDpWbm6A==}
engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0}
- jest-docblock@30.0.1:
- resolution: {integrity: sha512-/vF78qn3DYphAaIc3jy4gA7XSAz167n9Bm/wn/1XhTLW7tTBIzXtCJpb/vcmc73NIIeeohCbdL94JasyXUZsGA==}
+ jest-docblock@30.2.0:
+ resolution: {integrity: sha512-tR/FFgZKS1CXluOQzZvNH3+0z9jXr3ldGSD8bhyuxvlVUwbeLOGynkunvlTMxchC5urrKndYiwCFC0DLVjpOCA==}
engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0}
- jest-each@30.1.0:
- resolution: {integrity: sha512-A+9FKzxPluqogNahpCv04UJvcZ9B3HamqpDNWNKDjtxVRYB8xbZLFuCr8JAJFpNp83CA0anGQFlpQna9Me+/tQ==}
+ jest-each@30.2.0:
+ resolution: {integrity: sha512-lpWlJlM7bCUf1mfmuqTA8+j2lNURW9eNafOy99knBM01i5CQeY5UH1vZjgT9071nDJac1M4XsbyI44oNOdhlDQ==}
engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0}
- jest-environment-node@30.1.2:
- resolution: {integrity: sha512-w8qBiXtqGWJ9xpJIA98M0EIoq079GOQRQUyse5qg1plShUCQ0Ek1VTTcczqKrn3f24TFAgFtT+4q3aOXvjbsuA==}
+ jest-environment-node@30.2.0:
+ resolution: {integrity: sha512-ElU8v92QJ9UrYsKrxDIKCxu6PfNj4Hdcktcn0JX12zqNdqWHB0N+hwOnnBBXvjLd2vApZtuLUGs1QSY+MsXoNA==}
engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0}
- jest-haste-map@30.1.0:
- resolution: {integrity: sha512-JLeM84kNjpRkggcGpQLsV7B8W4LNUWz7oDNVnY1Vjj22b5/fAb3kk3htiD+4Na8bmJmjJR7rBtS2Rmq/NEcADg==}
+ jest-haste-map@30.2.0:
+ resolution: {integrity: sha512-sQA/jCb9kNt+neM0anSj6eZhLZUIhQgwDt7cPGjumgLM4rXsfb9kpnlacmvZz3Q5tb80nS+oG/if+NBKrHC+Xw==}
engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0}
- jest-leak-detector@30.1.0:
- resolution: {integrity: sha512-AoFvJzwxK+4KohH60vRuHaqXfWmeBATFZpzpmzNmYTtmRMiyGPVhkXpBqxUQunw+dQB48bDf4NpUs6ivVbRv1g==}
+ jest-leak-detector@30.2.0:
+ resolution: {integrity: sha512-M6jKAjyzjHG0SrQgwhgZGy9hFazcudwCNovY/9HPIicmNSBuockPSedAP9vlPK6ONFJ1zfyH/M2/YYJxOz5cdQ==}
engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0}
- jest-matcher-utils@30.1.2:
- resolution: {integrity: sha512-7ai16hy4rSbDjvPTuUhuV8nyPBd6EX34HkBsBcBX2lENCuAQ0qKCPb/+lt8OSWUa9WWmGYLy41PrEzkwRwoGZQ==}
+ jest-matcher-utils@30.2.0:
+ resolution: {integrity: sha512-dQ94Nq4dbzmUWkQ0ANAWS9tBRfqCrn0bV9AMYdOi/MHW726xn7eQmMeRTpX2ViC00bpNaWXq+7o4lIQ3AX13Hg==}
engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0}
- jest-message-util@30.1.0:
- resolution: {integrity: sha512-HizKDGG98cYkWmaLUHChq4iN+oCENohQLb7Z5guBPumYs+/etonmNFlg1Ps6yN9LTPyZn+M+b/9BbnHx3WTMDg==}
+ jest-message-util@30.2.0:
+ resolution: {integrity: sha512-y4DKFLZ2y6DxTWD4cDe07RglV88ZiNEdlRfGtqahfbIjfsw1nMCPx49Uev4IA/hWn3sDKyAnSPwoYSsAEdcimw==}
engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0}
- jest-mock@30.0.5:
- resolution: {integrity: sha512-Od7TyasAAQX/6S+QCbN6vZoWOMwlTtzzGuxJku1GhGanAjz9y+QsQkpScDmETvdc9aSXyJ/Op4rhpMYBWW91wQ==}
+ jest-mock@30.2.0:
+ resolution: {integrity: sha512-JNNNl2rj4b5ICpmAcq+WbLH83XswjPbjH4T7yvGzfAGCPh1rw+xVNbtk+FnRslvt9lkCcdn9i1oAoKUuFsOxRw==}
engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0}
jest-pnp-resolver@1.2.3:
@@ -1966,44 +2307,44 @@ packages:
resolution: {integrity: sha512-jHEQgBXAgc+Gh4g0p3bCevgRCVRkB4VB70zhoAE48gxeSr1hfUOsM/C2WoJgVL7Eyg//hudYENbm3Ne+/dRVVA==}
engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0}
- jest-resolve-dependencies@30.1.3:
- resolution: {integrity: sha512-DNfq3WGmuRyHRHfEet+Zm3QOmVFtIarUOQHHryKPc0YL9ROfgWZxl4+aZq/VAzok2SS3gZdniP+dO4zgo59hBg==}
+ jest-resolve-dependencies@30.2.0:
+ resolution: {integrity: sha512-xTOIGug/0RmIe3mmCqCT95yO0vj6JURrn1TKWlNbhiAefJRWINNPgwVkrVgt/YaerPzY3iItufd80v3lOrFJ2w==}
engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0}
- jest-resolve@30.1.3:
- resolution: {integrity: sha512-DI4PtTqzw9GwELFS41sdMK32Ajp3XZQ8iygeDMWkxlRhm7uUTOFSZFVZABFuxr0jvspn8MAYy54NxZCsuCTSOw==}
+ jest-resolve@30.2.0:
+ resolution: {integrity: sha512-TCrHSxPlx3tBY3hWNtRQKbtgLhsXa1WmbJEqBlTBrGafd5fiQFByy2GNCEoGR+Tns8d15GaL9cxEzKOO3GEb2A==}
engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0}
- jest-runner@30.1.3:
- resolution: {integrity: sha512-dd1ORcxQraW44Uz029TtXj85W11yvLpDuIzNOlofrC8GN+SgDlgY4BvyxJiVeuabA1t6idjNbX59jLd2oplOGQ==}
+ jest-runner@30.2.0:
+ resolution: {integrity: sha512-PqvZ2B2XEyPEbclp+gV6KO/F1FIFSbIwewRgmROCMBo/aZ6J1w8Qypoj2pEOcg3G2HzLlaP6VUtvwCI8dM3oqQ==}
engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0}
- jest-runtime@30.1.3:
- resolution: {integrity: sha512-WS8xgjuNSphdIGnleQcJ3AKE4tBKOVP+tKhCD0u+Tb2sBmsU8DxfbBpZX7//+XOz81zVs4eFpJQwBNji2Y07DA==}
+ jest-runtime@30.2.0:
+ resolution: {integrity: sha512-p1+GVX/PJqTucvsmERPMgCPvQJpFt4hFbM+VN3n8TMo47decMUcJbt+rgzwrEme0MQUA/R+1de2axftTHkKckg==}
engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0}
- jest-snapshot@30.1.2:
- resolution: {integrity: sha512-4q4+6+1c8B6Cy5pGgFvjDy/Pa6VYRiGu0yQafKkJ9u6wQx4G5PqI2QR6nxTl43yy7IWsINwz6oT4o6tD12a8Dg==}
+ jest-snapshot@30.2.0:
+ resolution: {integrity: sha512-5WEtTy2jXPFypadKNpbNkZ72puZCa6UjSr/7djeecHWOu7iYhSXSnHScT8wBz3Rn8Ena5d5RYRcsyKIeqG1IyA==}
engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0}
- jest-util@30.0.5:
- resolution: {integrity: sha512-pvyPWssDZR0FlfMxCBoc0tvM8iUEskaRFALUtGQYzVEAqisAztmy+R8LnU14KT4XA0H/a5HMVTXat1jLne010g==}
+ jest-util@30.2.0:
+ resolution: {integrity: sha512-QKNsM0o3Xe6ISQU869e+DhG+4CK/48aHYdJZGlFQVTjnbvgpcKyxpzk29fGiO7i/J8VENZ+d2iGnSsvmuHywlA==}
engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0}
- jest-validate@30.1.0:
- resolution: {integrity: sha512-7P3ZlCFW/vhfQ8pE7zW6Oi4EzvuB4sgR72Q1INfW9m0FGo0GADYlPwIkf4CyPq7wq85g+kPMtPOHNAdWHeBOaA==}
+ jest-validate@30.2.0:
+ resolution: {integrity: sha512-FBGWi7dP2hpdi8nBoWxSsLvBFewKAg0+uSQwBaof4Y4DPgBabXgpSYC5/lR7VmnIlSpASmCi/ntRWPbv7089Pw==}
engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0}
- jest-watcher@30.1.3:
- resolution: {integrity: sha512-6jQUZCP1BTL2gvG9E4YF06Ytq4yMb4If6YoQGRR6PpjtqOXSP3sKe2kqwB6SQ+H9DezOfZaSLnmka1NtGm3fCQ==}
+ jest-watcher@30.2.0:
+ resolution: {integrity: sha512-PYxa28dxJ9g777pGm/7PrbnMeA0Jr7osHP9bS7eJy9DuAjMgdGtxgf0uKMyoIsTWAkIbUW5hSDdJ3urmgXBqxg==}
engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0}
- jest-worker@30.1.0:
- resolution: {integrity: sha512-uvWcSjlwAAgIu133Tt77A05H7RIk3Ho8tZL50bQM2AkvLdluw9NG48lRCl3Dt+MOH719n/0nnb5YxUwcuJiKRA==}
+ jest-worker@30.2.0:
+ resolution: {integrity: sha512-0Q4Uk8WF7BUwqXHuAjc23vmopWJw5WH7w2tqBoUOZpOjW/ZnR44GXXd1r82RvnmI2GZge3ivrYXk/BE2+VtW2g==}
engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0}
- jest@30.1.3:
- resolution: {integrity: sha512-Ry+p2+NLk6u8Agh5yVqELfUJvRfV51hhVBRIB5yZPY7mU0DGBmOuFG5GebZbMbm86cdQNK0fhJuDX8/1YorISQ==}
+ jest@30.2.0:
+ resolution: {integrity: sha512-F26gjC0yWN8uAA5m5Ss8ZQf5nDHWGlN/xWZIh8S5SRbsEKBovwZhxGd6LJlbZYxBgCYOtreSUyb8hpXyGC5O4A==}
engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0}
hasBin: true
peerDependencies:
@@ -2015,12 +2356,12 @@ packages:
js-tokens@4.0.0:
resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==}
- js-yaml@3.14.1:
- resolution: {integrity: sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==}
+ js-yaml@3.14.2:
+ resolution: {integrity: sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==}
hasBin: true
- js-yaml@4.1.0:
- resolution: {integrity: sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==}
+ js-yaml@4.1.1:
+ resolution: {integrity: sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==}
hasBin: true
jsesc@3.1.0:
@@ -2048,6 +2389,9 @@ packages:
engines: {node: '>=6'}
hasBin: true
+ jsonfile@4.0.0:
+ resolution: {integrity: sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==}
+
keyv@4.5.4:
resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==}
@@ -2066,6 +2410,9 @@ packages:
lines-and-columns@1.2.4:
resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==}
+ linkify-it@5.0.0:
+ resolution: {integrity: sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ==}
+
lint-staged@16.1.6:
resolution: {integrity: sha512-U4kuulU3CKIytlkLlaHcGgKscNfJPNTiDF2avIUGFCv7K95/DCYQ7Ra62ydeRWmgQGg9zJYw2dzdbztwJlqrow==}
engines: {node: '>=20.17'}
@@ -2075,6 +2422,10 @@ packages:
resolution: {integrity: sha512-0aeh5HHHgmq1KRdMMDHfhMWQmIT/m7nRDTlxlFqni2Sp0had9baqsjJRvDGdlvgd6NmPE0nPloOipiQJGFtTHQ==}
engines: {node: '>=20.0.0'}
+ load-esm@1.0.3:
+ resolution: {integrity: sha512-v5xlu8eHD1+6r8EHTg6hfmO97LN8ugKtiXcy5e6oN72iD2r6u0RPfLl6fxM+7Wnh2ZRq15o0russMst44WauPA==}
+ engines: {node: '>=13.2.0'}
+
locate-path@5.0.0:
resolution: {integrity: sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==}
engines: {node: '>=8'}
@@ -2086,8 +2437,11 @@ packages:
lodash.memoize@4.1.2:
resolution: {integrity: sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==}
- lodash.merge@4.6.2:
- resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==}
+ lodash.startcase@4.4.0:
+ resolution: {integrity: sha512-+WKqsK294HMSc2jEbNgpHpd0JfIBhp7rEV4aqXWqFr6AlXov+SlcgB1Fv01y2kGe3Gc8nMW7VA0SrGuSkRfIEg==}
+
+ lodash@4.17.23:
+ resolution: {integrity: sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==}
log-symbols@4.1.0:
resolution: {integrity: sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==}
@@ -2104,9 +2458,16 @@ packages:
lru-cache@10.4.3:
resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==}
+ lru-cache@11.2.6:
+ resolution: {integrity: sha512-ESL2CrkS/2wTPfuend7Zhkzo2u0daGJ/A2VucJOgQ/C48S/zB8MMeMHSGKYpXhIjbPxfuezITkaBH1wqv00DDQ==}
+ engines: {node: 20 || >=22}
+
lru-cache@5.1.1:
resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==}
+ lunr@2.3.9:
+ resolution: {integrity: sha512-zTU3DaZaF3Rt9rhN3uBMGQD3dD2/vFQqnvZCDv4dl5iOzq2IZQqTxu90r4E5J+nP70J3ilqVCrbho2eWaeW8Ow==}
+
make-dir@4.0.0:
resolution: {integrity: sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==}
engines: {node: '>=10'}
@@ -2117,10 +2478,17 @@ packages:
makeerror@1.0.12:
resolution: {integrity: sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg==}
+ markdown-it@14.1.1:
+ resolution: {integrity: sha512-BuU2qnTti9YKgK5N+IeMubp14ZUKUUw7yeJbkjtosvHiP0AZ5c8IAgEMk79D0eC8F23r4Ac/q8cAIFdm2FtyoA==}
+ hasBin: true
+
math-intrinsics@1.1.0:
resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==}
engines: {node: '>= 0.4'}
+ mdurl@2.0.0:
+ resolution: {integrity: sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w==}
+
merge-stream@2.0.0:
resolution: {integrity: sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==}
@@ -2148,23 +2516,27 @@ packages:
resolution: {integrity: sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==}
engines: {node: '>=18'}
- minimatch@3.1.2:
- resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==}
-
- minimatch@9.0.5:
- resolution: {integrity: sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==}
- engines: {node: '>=16 || 14 >=14.17'}
+ minimatch@10.2.2:
+ resolution: {integrity: sha512-+G4CpNBxa5MprY+04MbgOw1v7So6n5JY166pFi9KfYwT78fxScCeSNQSNzp6dpPSW2rONOps6Ocam1wFhCgoVw==}
+ engines: {node: 18 || 20 || >=22}
minimist@1.2.8:
resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==}
- minipass@7.1.2:
- resolution: {integrity: sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==}
+ minipass@7.1.3:
+ resolution: {integrity: sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==}
engines: {node: '>=16 || 14 >=14.17'}
+ mri@1.2.0:
+ resolution: {integrity: sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==}
+ engines: {node: '>=4'}
+
ms@2.1.3:
resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==}
+ mute-stream@0.0.8:
+ resolution: {integrity: sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==}
+
mute-stream@1.0.0:
resolution: {integrity: sha512-avsJQhyd+680gKXyG/sQc0nXaC6rBkPOfyHYcFb9+hdkqQkR9bdnkJ0AMZhke0oesPqIO+mFFJ+IdBc7mst4IA==}
engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0}
@@ -2173,8 +2545,8 @@ packages:
resolution: {integrity: sha512-jtpsQDetTnvS2Ts1fiRdci5rx0VYws5jGyC+4IYOTnIQ/wwdf6JdomlHBwqC3bJYOvaKu0C2GSZ1A60anrYpaA==}
engines: {node: '>=20.17'}
- napi-postinstall@0.3.3:
- resolution: {integrity: sha512-uTp172LLXSxuSYHv/kou+f6KW3SMppU9ivthaVTXian9sOt3XM/zHYHpRZiLgQoxeWfYUnslNWQHF1+G71xcow==}
+ napi-postinstall@0.3.4:
+ resolution: {integrity: sha512-PHI5f1O0EP5xJ9gQmFGMS6IZcrVvTjpXjz7Na41gTE7eE2hK11lg04CECCYEEjdc17EV4DO+fkGEtt7TpTaTiQ==}
engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0}
hasBin: true
@@ -2184,6 +2556,13 @@ packages:
neo-async@2.6.2:
resolution: {integrity: sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==}
+ nest-commander@3.20.1:
+ resolution: {integrity: sha512-LRI7z6UlWy2vWyQR0PYnAXsaRyJvpfiuvOCmx2jk2kLXJH9+y/omPDl9NE3fq4WMaE0/AhviuUjA12eC/zDqXw==}
+ peerDependencies:
+ '@nestjs/common': ^8.0.0 || ^9.0.0 || ^10.0.0 || ^11.0.0
+ '@nestjs/core': ^8.0.0 || ^9.0.0 || ^10.0.0 || ^11.0.0
+ '@types/inquirer': ^8.1.3
+
node-fetch@2.7.0:
resolution: {integrity: sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==}
engines: {node: 4.x || >=6.0.0}
@@ -2200,8 +2579,8 @@ packages:
node-int64@0.4.0:
resolution: {integrity: sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==}
- node-releases@2.0.20:
- resolution: {integrity: sha512-7gK6zSXEH6neM212JgfYFXe+GmZQM+fia5SsusuBIUgnPheLFBmIPhtFoAQRj8/7wASYQnbDlHPVwY0BefoFgA==}
+ node-releases@2.0.27:
+ resolution: {integrity: sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==}
normalize-path@3.0.0:
resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==}
@@ -2211,9 +2590,6 @@ packages:
resolution: {integrity: sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==}
engines: {node: '>=8'}
- once@1.4.0:
- resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==}
-
onetime@5.1.2:
resolution: {integrity: sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==}
engines: {node: '>=6'}
@@ -2238,6 +2614,9 @@ packages:
resolution: {integrity: sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g==}
engines: {node: '>=0.10.0'}
+ outdent@0.5.0:
+ resolution: {integrity: sha512-/jHxFIzoMXdqPzTaCpFzAAWhpkSjZPF4Vsn6jAfNpmbH/ymsmd7Qc6VE9BGn0L6YMj6uwpQLxCECpus4ukKS9Q==}
+
ox@0.9.6:
resolution: {integrity: sha512-8SuCbHPvv2eZLYXrNmC0EC12rdzXQLdhnOMlHDW2wiCPLxBrOOJwX5L5E61by+UjTPOryqQiRSnjIKCI+GykKg==}
peerDependencies:
@@ -2246,6 +2625,10 @@ packages:
typescript:
optional: true
+ p-filter@2.1.0:
+ resolution: {integrity: sha512-ZBxxZ5sL2HghephhpGAQdoskxplTwr7ICaehZwLIlfL6acuVgZPm8yBNuRAFBGEqtD/hmUeq9eqLg2ys9Xr/yw==}
+ engines: {node: '>=8'}
+
p-limit@2.3.0:
resolution: {integrity: sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==}
engines: {node: '>=6'}
@@ -2262,6 +2645,10 @@ packages:
resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==}
engines: {node: '>=10'}
+ p-map@2.1.0:
+ resolution: {integrity: sha512-y3b8Kpd8OAN444hxfBbFfj1FY/RjtTd8tzYwhUqNYXx0fXx2iX4maP4Qr6qhIKbQXI02wTLAda4fYUbDagTUFw==}
+ engines: {node: '>=6'}
+
p-try@2.2.0:
resolution: {integrity: sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==}
engines: {node: '>=6'}
@@ -2269,6 +2656,9 @@ packages:
package-json-from-dist@1.0.1:
resolution: {integrity: sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==}
+ package-manager-detector@0.2.11:
+ resolution: {integrity: sha512-BEnLolu+yuz22S56CU1SUKq3XC3PkwD5wv4ikR4MfGvnRVcmzXR9DwSlW2fEamyTPyXHomBJRzgapeuBvRNzJQ==}
+
pako@2.1.0:
resolution: {integrity: sha512-w+eufiZ1WuJYgPXbV/PO3NCMEc3xqylkKHzp8bxp1uW4qaSNQUkwmLLEc3kKsfz8lpV1F8Ht3U1Cm+9Srog2ug==}
@@ -2284,10 +2674,6 @@ packages:
resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==}
engines: {node: '>=8'}
- path-is-absolute@1.0.1:
- resolution: {integrity: sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==}
- engines: {node: '>=0.10.0'}
-
path-key@3.1.1:
resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==}
engines: {node: '>=8'}
@@ -2296,6 +2682,17 @@ packages:
resolution: {integrity: sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==}
engines: {node: '>=16 || 14 >=14.18'}
+ path-scurry@2.0.2:
+ resolution: {integrity: sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==}
+ engines: {node: 18 || 20 || >=22}
+
+ path-to-regexp@8.3.0:
+ resolution: {integrity: sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA==}
+
+ path-type@4.0.0:
+ resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==}
+ engines: {node: '>=8'}
+
picocolors@1.1.1:
resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==}
@@ -2312,6 +2709,10 @@ packages:
engines: {node: '>=0.10'}
hasBin: true
+ pify@4.0.1:
+ resolution: {integrity: sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==}
+ engines: {node: '>=6'}
+
pirates@4.0.7:
resolution: {integrity: sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==}
engines: {node: '>= 6'}
@@ -2328,18 +2729,27 @@ packages:
resolution: {integrity: sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w==}
engines: {node: '>=6.0.0'}
+ prettier@2.8.8:
+ resolution: {integrity: sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==}
+ engines: {node: '>=10.13.0'}
+ hasBin: true
+
prettier@3.6.2:
resolution: {integrity: sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==}
engines: {node: '>=14'}
hasBin: true
- pretty-format@30.0.5:
- resolution: {integrity: sha512-D1tKtYvByrBkFLe2wHJl2bwMJIiT8rW+XA+TiataH79/FszLQMrpGEvzUVkzPau7OCO0Qnrhpe87PqtOAIB8Yw==}
+ pretty-format@30.2.0:
+ resolution: {integrity: sha512-9uBdv/B4EefsuAL+pWqueZyZS2Ba+LxfFeQ9DN14HU4bN8bhaxKdkpjpB6fs9+pSjIBu+FXQHImEg8j/Lw0+vA==}
engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0}
proxy-from-env@1.1.0:
resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==}
+ punycode.js@2.3.1:
+ resolution: {integrity: sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==}
+ engines: {node: '>=6'}
+
punycode@2.3.1:
resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==}
engines: {node: '>=6'}
@@ -2347,16 +2757,26 @@ packages:
pure-rand@7.0.1:
resolution: {integrity: sha512-oTUZM/NAZS8p7ANR3SHh30kXB+zK2r2BPcEn/awJIbOvq82WoMN4p62AWWp3Hhw50G0xMsw1mhIBLqHw64EcNQ==}
+ quansync@0.2.11:
+ resolution: {integrity: sha512-AifT7QEbW9Nri4tAwR5M/uzpBuqfZf+zwaEM/QkzEjj7NBuFD2rBuy0K3dE+8wltbezDV7JMA0WfnCPYRSYbXA==}
+
queue-microtask@1.2.3:
resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==}
react-is@18.3.1:
resolution: {integrity: sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==}
+ read-yaml-file@1.1.0:
+ resolution: {integrity: sha512-VIMnQi/Z4HT2Fxuwg5KrY174U1VdUIASQVWXXyqtNRtxSr9IYkn1rsI6Tb6HsrHCmB7gVpNwX6JxPTHcH6IoTA==}
+ engines: {node: '>=6'}
+
readable-stream@3.6.2:
resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==}
engines: {node: '>= 6'}
+ reflect-metadata@0.2.2:
+ resolution: {integrity: sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q==}
+
regenerator-runtime@0.14.1:
resolution: {integrity: sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==}
@@ -2397,6 +2817,10 @@ packages:
rpc-websockets@9.1.3:
resolution: {integrity: sha512-I+kNjW0udB4Fetr3vvtRuYZJS0PcSPyyvBcH5sDdoV8DFs5E4W2pTr7aiMlKfPxANTClP9RlqCPolj9dd5MsEA==}
+ run-async@2.4.1:
+ resolution: {integrity: sha512-tvVnVv01b8c1RrA6Ep7JkStj85Guv/YrMcwqYQnwjsAS2cTmmPGBBjAjpCW7RrSodNSoE2/qg9O4bceNvUuDgQ==}
+ engines: {node: '>=0.12.0'}
+
run-async@3.0.0:
resolution: {integrity: sha512-540WwVDOMxA6dN6We19EcT9sc3hkXPw5mzRNGM3FkdN/vtE9NFvj5lFAPNwUDmJjXidm3v7TC1cTE7t17Ulm1Q==}
engines: {node: '>=0.12.0'}
@@ -2422,8 +2846,8 @@ packages:
engines: {node: '>=10'}
hasBin: true
- semver@7.7.2:
- resolution: {integrity: sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==}
+ semver@7.7.4:
+ resolution: {integrity: sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==}
engines: {node: '>=10'}
hasBin: true
@@ -2461,6 +2885,9 @@ packages:
resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==}
engines: {node: '>=0.10.0'}
+ spawndamnit@3.0.1:
+ resolution: {integrity: sha512-MmnduQUuHCoFckZoWnXsTg7JaiLBJrKFj9UI2MbRPGaJeVpsLcVBu6P/IGZovziM/YBsellCmsprgNA+w0CzVg==}
+
sprintf-js@1.0.3:
resolution: {integrity: sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==}
@@ -2509,6 +2936,10 @@ packages:
resolution: {integrity: sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==}
engines: {node: '>=12'}
+ strip-ansi@7.1.2:
+ resolution: {integrity: sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==}
+ engines: {node: '>=12'}
+
strip-bom@3.0.0:
resolution: {integrity: sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==}
engines: {node: '>=4'}
@@ -2525,6 +2956,10 @@ packages:
resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==}
engines: {node: '>=8'}
+ strtok3@10.3.4:
+ resolution: {integrity: sha512-KIy5nylvC5le1OdaaoCJ07L+8iQzJHGH6pWDuzS+d07Cu7n1MZ2x26P8ZKIWfbK02+XIL8Mp4RkWeqdUCrDMfg==}
+ engines: {node: '>=18'}
+
superstruct@0.15.5:
resolution: {integrity: sha512-4AOeU+P5UuE/4nOUkmcQdW5y7i9ndt1cQd/3iUe+LTz3RxESf/W/5lg4B74HbDMMv8PHnPnGCQFH45kBcrQYoQ==}
@@ -2544,13 +2979,28 @@ packages:
resolution: {integrity: sha512-MeQTA1r0litLUf0Rp/iisCaL8761lKAZHaimlbGK4j0HysC4PLfqygQj9srcs0m2RdtDYnF8UuYyKpbjHYp7Jw==}
engines: {node: ^14.18.0 || >=16.0.0}
- test-exclude@6.0.0:
- resolution: {integrity: sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==}
+ synckit@0.11.12:
+ resolution: {integrity: sha512-Bh7QjT8/SuKUIfObSXNHNSK6WHo6J1tHCqJsuaFDP7gP0fkzSfTxI8y85JrppZ0h8l0maIgc2tfuZQ6/t3GtnQ==}
+ engines: {node: ^14.18.0 || >=16.0.0}
+
+ term-size@2.2.1:
+ resolution: {integrity: sha512-wK0Ri4fOGjv/XPy8SBHZChl8CM7uMc5VML7SqiQ0zG7+J5Vr+RMQDoHa2CNT6KHUnTGIXH34UDMkPzAUyapBZg==}
engines: {node: '>=8'}
+ test-exclude@8.0.0:
+ resolution: {integrity: sha512-ZOffsNrXYggvU1mDGHk54I96r26P8SyMjO5slMKSc7+IWmtB/MQKnEC2fP51imB3/pT6YK5cT5E8f+Dd9KdyOQ==}
+ engines: {node: 20 || >=22}
+
text-encoding-utf-8@1.0.2:
resolution: {integrity: sha512-8bw4MY9WjdsD2aMtO0OzOCY3pXGYNx2d2FfHRVUKkiCPDWjKuOlhLVASS+pD7VkLTVjW268LYJHwsnPFlBpbAg==}
+ through@2.3.8:
+ resolution: {integrity: sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==}
+
+ tinyglobby@0.2.15:
+ resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==}
+ engines: {node: '>=12.0.0'}
+
tmp@0.0.33:
resolution: {integrity: sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==}
engines: {node: '>=0.6.0'}
@@ -2562,23 +3012,27 @@ packages:
resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==}
engines: {node: '>=8.0'}
+ token-types@6.1.2:
+ resolution: {integrity: sha512-dRXchy+C0IgK8WPC6xvCHFRIWYUbqqdEIKPaKo/AcTUNzwLTK6AH7RjdLWsEZcAN/TBdtfUw3PYEgPr5VPr6ww==}
+ engines: {node: '>=14.16'}
+
toml@3.0.0:
resolution: {integrity: sha512-y/mWCZinnvxjTKYhJ+pYxwD0mRLVvOtdS2Awbgxln6iEnt4rk0yBxeSBHkGJcPucRiG0e55mwWp+g/05rsrd6w==}
tr46@0.0.3:
resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==}
- tronweb@6.0.4:
- resolution: {integrity: sha512-+9Nc7H4FYVh2DcOnQG93WLm3UdlHSf9W+GXkfrXI77oLjTB1cptROJDKRSSxQBiOAyjjAJOOTuYDzlAkaLT85w==}
+ tronweb@6.2.0:
+ resolution: {integrity: sha512-09kyW6mqiFuSYXkR35ndxCeNF5rW1O18hKAClCLtVHP2xBFPYSGx3lDYC2hRKcuLiq6iLPxOVCrhzoKNGlFuQQ==}
- ts-api-utils@2.1.0:
- resolution: {integrity: sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ==}
+ ts-api-utils@2.4.0:
+ resolution: {integrity: sha512-3TaVTaAv2gTiMB35i3FiGJaRfwb3Pyn/j3m/bfAvGe8FB7CF6u+LMYqYlDh7reQf7UNvoTvdfAqHGmPGOSsPmA==}
engines: {node: '>=18.12'}
peerDependencies:
typescript: '>=4.8.4'
- ts-jest@29.4.1:
- resolution: {integrity: sha512-SaeUtjfpg9Uqu8IbeDKtdaS0g8lS6FT6OzM3ezrDfErPJPHNDo/Ey+VFGP1bQIDfagYDLyRpd7O15XpG1Es2Uw==}
+ ts-jest@29.4.6:
+ resolution: {integrity: sha512-fSpWtOO/1AjSNQguk43hb/JCo16oJDnMJf3CdEGNkqsEX3t0KX96xvyX1D7PfLCpVoKu4MfVrqUkFyblYoY4lA==}
engines: {node: ^14.15.0 || ^16.10.0 || ^18.0.0 || >=20.0.0}
hasBin: true
peerDependencies:
@@ -2649,27 +3103,49 @@ packages:
resolution: {integrity: sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==}
engines: {node: '>=16'}
+ typedoc@0.28.17:
+ resolution: {integrity: sha512-ZkJ2G7mZrbxrKxinTQMjFqsCoYY6a5Luwv2GKbTnBCEgV2ihYm5CflA9JnJAwH0pZWavqfYxmDkFHPt4yx2oDQ==}
+ engines: {node: '>= 18', pnpm: '>= 10'}
+ hasBin: true
+ peerDependencies:
+ typescript: 5.0.x || 5.1.x || 5.2.x || 5.3.x || 5.4.x || 5.5.x || 5.6.x || 5.7.x || 5.8.x || 5.9.x
+
typescript@5.9.2:
resolution: {integrity: sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A==}
engines: {node: '>=14.17'}
hasBin: true
+ uc.micro@2.1.0:
+ resolution: {integrity: sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==}
+
uglify-js@3.19.3:
resolution: {integrity: sha512-v3Xu+yuwBXisp6QYTcH4UbH+xYJXqnq2m/LtQVWKWzYc1iehYnLixoQDN9FH6/j9/oybfd6W9Ghwkl8+UMKTKQ==}
engines: {node: '>=0.8.0'}
hasBin: true
+ uid@2.0.2:
+ resolution: {integrity: sha512-u3xV3X7uzvi5b1MncmZo3i2Aw222Zk1keqLA1YkHldREkAhAqi65wuPfe7lHx8H/Wzy+8CE7S7uS3jekIM5s8g==}
+ engines: {node: '>=8'}
+
+ uint8array-extras@1.5.0:
+ resolution: {integrity: sha512-rvKSBiC5zqCCiDZ9kAOszZcDvdAHwwIKJG33Ykj43OKcWsnmcBRL09YTU4nOeHZ8Y2a7l1MgTd08SBe9A8Qj6A==}
+ engines: {node: '>=18'}
+
undici-types@6.19.8:
resolution: {integrity: sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==}
undici-types@6.21.0:
resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==}
+ universalify@0.1.2:
+ resolution: {integrity: sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==}
+ engines: {node: '>= 4.0.0'}
+
unrs-resolver@1.11.1:
resolution: {integrity: sha512-bSjt9pjaEBnNiGgc9rUiHGKv5l4/TGzDmYw3RhnkJGtLhbnnA/5qJj7x3dNDCRx/PJxu774LlH8lCOlB4hEfKg==}
- update-browserslist-db@1.1.3:
- resolution: {integrity: sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==}
+ update-browserslist-db@1.2.3:
+ resolution: {integrity: sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==}
hasBin: true
peerDependencies:
browserslist: '>= 4.21.0'
@@ -2695,8 +3171,8 @@ packages:
resolution: {integrity: sha512-kiGUalWN+rgBJ/1OHZsBtU4rXZOfj/7rKQxULKlIzwzQSvMJUUNgPwJEEh7gU6xEVxC0ahoOBvN2YI8GH6FNgA==}
engines: {node: '>=10.12.0'}
- validator@13.12.0:
- resolution: {integrity: sha512-c1Q0mCiPlgdTVVVIJIrBuxNicYE+t/7oKeI9MWLj3fh/uq2Pxh/3eeWbVZ4OcGW1TUf53At0njHw5SMdA3tmMg==}
+ validator@13.15.23:
+ resolution: {integrity: sha512-4yoz1kEWqUjzi5zsPbAS/903QXSYp0UOtHsPpp7p9rHAw/W+dkInskAE386Fat3oKRROwO98d9ZB0G4cObgUyw==}
engines: {node: '>= 0.10'}
viem@2.40.4:
@@ -2747,9 +3223,6 @@ packages:
resolution: {integrity: sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==}
engines: {node: '>=18'}
- wrappy@1.0.2:
- resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==}
-
write-file-atomic@5.0.1:
resolution: {integrity: sha512-+QU2zd6OTD8XWIJCbffaiQeH9U73qIqafo1x6V1snCWYGJf6cVE0cDR4D8xRzcEnfI21IFrUPzPGtcPf8AC+Rw==}
engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0}
@@ -2822,175 +3295,178 @@ packages:
resolution: {integrity: sha512-U/PBtDf35ff0D8X8D0jfdzHYEPFxAI7jJlxZXwCSez5M3190m+QobIfh+sWDWSHMCWWJN2AWamkegn6vr6YBTw==}
engines: {node: '>=18'}
+ zod@4.3.6:
+ resolution: {integrity: sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==}
+
snapshots:
'@adraffy/ens-normalize@1.10.1': {}
- '@adraffy/ens-normalize@1.11.0': {}
+ '@adraffy/ens-normalize@1.11.1': {}
- '@babel/code-frame@7.27.1':
+ '@babel/code-frame@7.29.0':
dependencies:
- '@babel/helper-validator-identifier': 7.27.1
+ '@babel/helper-validator-identifier': 7.28.5
js-tokens: 4.0.0
picocolors: 1.1.1
- '@babel/compat-data@7.28.4': {}
+ '@babel/compat-data@7.29.0': {}
- '@babel/core@7.28.4':
+ '@babel/core@7.29.0':
dependencies:
- '@babel/code-frame': 7.27.1
- '@babel/generator': 7.28.3
- '@babel/helper-compilation-targets': 7.27.2
- '@babel/helper-module-transforms': 7.28.3(@babel/core@7.28.4)
- '@babel/helpers': 7.28.4
- '@babel/parser': 7.28.4
- '@babel/template': 7.27.2
- '@babel/traverse': 7.28.4
- '@babel/types': 7.28.4
+ '@babel/code-frame': 7.29.0
+ '@babel/generator': 7.29.1
+ '@babel/helper-compilation-targets': 7.28.6
+ '@babel/helper-module-transforms': 7.28.6(@babel/core@7.29.0)
+ '@babel/helpers': 7.28.6
+ '@babel/parser': 7.29.0
+ '@babel/template': 7.28.6
+ '@babel/traverse': 7.29.0
+ '@babel/types': 7.29.0
'@jridgewell/remapping': 2.3.5
convert-source-map: 2.0.0
- debug: 4.4.1
+ debug: 4.4.3
gensync: 1.0.0-beta.2
json5: 2.2.3
semver: 6.3.1
transitivePeerDependencies:
- supports-color
- '@babel/generator@7.28.3':
+ '@babel/generator@7.29.1':
dependencies:
- '@babel/parser': 7.28.4
- '@babel/types': 7.28.4
+ '@babel/parser': 7.29.0
+ '@babel/types': 7.29.0
'@jridgewell/gen-mapping': 0.3.13
- '@jridgewell/trace-mapping': 0.3.30
+ '@jridgewell/trace-mapping': 0.3.31
jsesc: 3.1.0
- '@babel/helper-compilation-targets@7.27.2':
+ '@babel/helper-compilation-targets@7.28.6':
dependencies:
- '@babel/compat-data': 7.28.4
+ '@babel/compat-data': 7.29.0
'@babel/helper-validator-option': 7.27.1
- browserslist: 4.25.4
+ browserslist: 4.28.1
lru-cache: 5.1.1
semver: 6.3.1
'@babel/helper-globals@7.28.0': {}
- '@babel/helper-module-imports@7.27.1':
+ '@babel/helper-module-imports@7.28.6':
dependencies:
- '@babel/traverse': 7.28.4
- '@babel/types': 7.28.4
+ '@babel/traverse': 7.29.0
+ '@babel/types': 7.29.0
transitivePeerDependencies:
- supports-color
- '@babel/helper-module-transforms@7.28.3(@babel/core@7.28.4)':
+ '@babel/helper-module-transforms@7.28.6(@babel/core@7.29.0)':
dependencies:
- '@babel/core': 7.28.4
- '@babel/helper-module-imports': 7.27.1
- '@babel/helper-validator-identifier': 7.27.1
- '@babel/traverse': 7.28.4
+ '@babel/core': 7.29.0
+ '@babel/helper-module-imports': 7.28.6
+ '@babel/helper-validator-identifier': 7.28.5
+ '@babel/traverse': 7.29.0
transitivePeerDependencies:
- supports-color
- '@babel/helper-plugin-utils@7.27.1': {}
+ '@babel/helper-plugin-utils@7.28.6': {}
'@babel/helper-string-parser@7.27.1': {}
- '@babel/helper-validator-identifier@7.27.1': {}
+ '@babel/helper-validator-identifier@7.28.5': {}
'@babel/helper-validator-option@7.27.1': {}
- '@babel/helpers@7.28.4':
+ '@babel/helpers@7.28.6':
dependencies:
- '@babel/template': 7.27.2
- '@babel/types': 7.28.4
+ '@babel/template': 7.28.6
+ '@babel/types': 7.29.0
- '@babel/parser@7.28.4':
+ '@babel/parser@7.29.0':
dependencies:
- '@babel/types': 7.28.4
+ '@babel/types': 7.29.0
- '@babel/plugin-syntax-async-generators@7.8.4(@babel/core@7.28.4)':
+ '@babel/plugin-syntax-async-generators@7.8.4(@babel/core@7.29.0)':
dependencies:
- '@babel/core': 7.28.4
- '@babel/helper-plugin-utils': 7.27.1
+ '@babel/core': 7.29.0
+ '@babel/helper-plugin-utils': 7.28.6
- '@babel/plugin-syntax-bigint@7.8.3(@babel/core@7.28.4)':
+ '@babel/plugin-syntax-bigint@7.8.3(@babel/core@7.29.0)':
dependencies:
- '@babel/core': 7.28.4
- '@babel/helper-plugin-utils': 7.27.1
+ '@babel/core': 7.29.0
+ '@babel/helper-plugin-utils': 7.28.6
- '@babel/plugin-syntax-class-properties@7.12.13(@babel/core@7.28.4)':
+ '@babel/plugin-syntax-class-properties@7.12.13(@babel/core@7.29.0)':
dependencies:
- '@babel/core': 7.28.4
- '@babel/helper-plugin-utils': 7.27.1
+ '@babel/core': 7.29.0
+ '@babel/helper-plugin-utils': 7.28.6
- '@babel/plugin-syntax-class-static-block@7.14.5(@babel/core@7.28.4)':
+ '@babel/plugin-syntax-class-static-block@7.14.5(@babel/core@7.29.0)':
dependencies:
- '@babel/core': 7.28.4
- '@babel/helper-plugin-utils': 7.27.1
+ '@babel/core': 7.29.0
+ '@babel/helper-plugin-utils': 7.28.6
- '@babel/plugin-syntax-import-attributes@7.27.1(@babel/core@7.28.4)':
+ '@babel/plugin-syntax-import-attributes@7.28.6(@babel/core@7.29.0)':
dependencies:
- '@babel/core': 7.28.4
- '@babel/helper-plugin-utils': 7.27.1
+ '@babel/core': 7.29.0
+ '@babel/helper-plugin-utils': 7.28.6
- '@babel/plugin-syntax-import-meta@7.10.4(@babel/core@7.28.4)':
+ '@babel/plugin-syntax-import-meta@7.10.4(@babel/core@7.29.0)':
dependencies:
- '@babel/core': 7.28.4
- '@babel/helper-plugin-utils': 7.27.1
+ '@babel/core': 7.29.0
+ '@babel/helper-plugin-utils': 7.28.6
- '@babel/plugin-syntax-json-strings@7.8.3(@babel/core@7.28.4)':
+ '@babel/plugin-syntax-json-strings@7.8.3(@babel/core@7.29.0)':
dependencies:
- '@babel/core': 7.28.4
- '@babel/helper-plugin-utils': 7.27.1
+ '@babel/core': 7.29.0
+ '@babel/helper-plugin-utils': 7.28.6
- '@babel/plugin-syntax-jsx@7.27.1(@babel/core@7.28.4)':
+ '@babel/plugin-syntax-jsx@7.28.6(@babel/core@7.29.0)':
dependencies:
- '@babel/core': 7.28.4
- '@babel/helper-plugin-utils': 7.27.1
+ '@babel/core': 7.29.0
+ '@babel/helper-plugin-utils': 7.28.6
- '@babel/plugin-syntax-logical-assignment-operators@7.10.4(@babel/core@7.28.4)':
+ '@babel/plugin-syntax-logical-assignment-operators@7.10.4(@babel/core@7.29.0)':
dependencies:
- '@babel/core': 7.28.4
- '@babel/helper-plugin-utils': 7.27.1
+ '@babel/core': 7.29.0
+ '@babel/helper-plugin-utils': 7.28.6
- '@babel/plugin-syntax-nullish-coalescing-operator@7.8.3(@babel/core@7.28.4)':
+ '@babel/plugin-syntax-nullish-coalescing-operator@7.8.3(@babel/core@7.29.0)':
dependencies:
- '@babel/core': 7.28.4
- '@babel/helper-plugin-utils': 7.27.1
+ '@babel/core': 7.29.0
+ '@babel/helper-plugin-utils': 7.28.6
- '@babel/plugin-syntax-numeric-separator@7.10.4(@babel/core@7.28.4)':
+ '@babel/plugin-syntax-numeric-separator@7.10.4(@babel/core@7.29.0)':
dependencies:
- '@babel/core': 7.28.4
- '@babel/helper-plugin-utils': 7.27.1
+ '@babel/core': 7.29.0
+ '@babel/helper-plugin-utils': 7.28.6
- '@babel/plugin-syntax-object-rest-spread@7.8.3(@babel/core@7.28.4)':
+ '@babel/plugin-syntax-object-rest-spread@7.8.3(@babel/core@7.29.0)':
dependencies:
- '@babel/core': 7.28.4
- '@babel/helper-plugin-utils': 7.27.1
+ '@babel/core': 7.29.0
+ '@babel/helper-plugin-utils': 7.28.6
- '@babel/plugin-syntax-optional-catch-binding@7.8.3(@babel/core@7.28.4)':
+ '@babel/plugin-syntax-optional-catch-binding@7.8.3(@babel/core@7.29.0)':
dependencies:
- '@babel/core': 7.28.4
- '@babel/helper-plugin-utils': 7.27.1
+ '@babel/core': 7.29.0
+ '@babel/helper-plugin-utils': 7.28.6
- '@babel/plugin-syntax-optional-chaining@7.8.3(@babel/core@7.28.4)':
+ '@babel/plugin-syntax-optional-chaining@7.8.3(@babel/core@7.29.0)':
dependencies:
- '@babel/core': 7.28.4
- '@babel/helper-plugin-utils': 7.27.1
+ '@babel/core': 7.29.0
+ '@babel/helper-plugin-utils': 7.28.6
- '@babel/plugin-syntax-private-property-in-object@7.14.5(@babel/core@7.28.4)':
+ '@babel/plugin-syntax-private-property-in-object@7.14.5(@babel/core@7.29.0)':
dependencies:
- '@babel/core': 7.28.4
- '@babel/helper-plugin-utils': 7.27.1
+ '@babel/core': 7.29.0
+ '@babel/helper-plugin-utils': 7.28.6
- '@babel/plugin-syntax-top-level-await@7.14.5(@babel/core@7.28.4)':
+ '@babel/plugin-syntax-top-level-await@7.14.5(@babel/core@7.29.0)':
dependencies:
- '@babel/core': 7.28.4
- '@babel/helper-plugin-utils': 7.27.1
+ '@babel/core': 7.29.0
+ '@babel/helper-plugin-utils': 7.28.6
- '@babel/plugin-syntax-typescript@7.27.1(@babel/core@7.28.4)':
+ '@babel/plugin-syntax-typescript@7.28.6(@babel/core@7.29.0)':
dependencies:
- '@babel/core': 7.28.4
- '@babel/helper-plugin-utils': 7.27.1
+ '@babel/core': 7.29.0
+ '@babel/helper-plugin-utils': 7.28.6
'@babel/runtime@7.26.10':
dependencies:
@@ -2998,43 +3474,189 @@ snapshots:
'@babel/runtime@7.28.3': {}
- '@babel/template@7.27.2':
+ '@babel/template@7.28.6':
dependencies:
- '@babel/code-frame': 7.27.1
- '@babel/parser': 7.28.4
- '@babel/types': 7.28.4
+ '@babel/code-frame': 7.29.0
+ '@babel/parser': 7.29.0
+ '@babel/types': 7.29.0
- '@babel/traverse@7.28.4':
+ '@babel/traverse@7.29.0':
dependencies:
- '@babel/code-frame': 7.27.1
- '@babel/generator': 7.28.3
+ '@babel/code-frame': 7.29.0
+ '@babel/generator': 7.29.1
'@babel/helper-globals': 7.28.0
- '@babel/parser': 7.28.4
- '@babel/template': 7.27.2
- '@babel/types': 7.28.4
- debug: 4.4.1
+ '@babel/parser': 7.29.0
+ '@babel/template': 7.28.6
+ '@babel/types': 7.29.0
+ debug: 4.4.3
transitivePeerDependencies:
- supports-color
- '@babel/types@7.28.4':
+ '@babel/types@7.29.0':
dependencies:
'@babel/helper-string-parser': 7.27.1
- '@babel/helper-validator-identifier': 7.27.1
+ '@babel/helper-validator-identifier': 7.28.5
'@bcoe/v8-coverage@0.2.3': {}
+ '@borewit/text-codec@0.2.1': {}
+
+ '@changesets/apply-release-plan@7.0.14':
+ dependencies:
+ '@changesets/config': 3.1.2
+ '@changesets/get-version-range-type': 0.4.0
+ '@changesets/git': 3.0.4
+ '@changesets/should-skip-package': 0.1.2
+ '@changesets/types': 6.1.0
+ '@manypkg/get-packages': 1.1.3
+ detect-indent: 6.1.0
+ fs-extra: 7.0.1
+ lodash.startcase: 4.4.0
+ outdent: 0.5.0
+ prettier: 2.8.8
+ resolve-from: 5.0.0
+ semver: 7.7.4
+
+ '@changesets/assemble-release-plan@6.0.9':
+ dependencies:
+ '@changesets/errors': 0.2.0
+ '@changesets/get-dependents-graph': 2.1.3
+ '@changesets/should-skip-package': 0.1.2
+ '@changesets/types': 6.1.0
+ '@manypkg/get-packages': 1.1.3
+ semver: 7.7.4
+
+ '@changesets/changelog-git@0.2.1':
+ dependencies:
+ '@changesets/types': 6.1.0
+
+ '@changesets/cli@2.29.8(@types/node@20.19.11)':
+ dependencies:
+ '@changesets/apply-release-plan': 7.0.14
+ '@changesets/assemble-release-plan': 6.0.9
+ '@changesets/changelog-git': 0.2.1
+ '@changesets/config': 3.1.2
+ '@changesets/errors': 0.2.0
+ '@changesets/get-dependents-graph': 2.1.3
+ '@changesets/get-release-plan': 4.0.14
+ '@changesets/git': 3.0.4
+ '@changesets/logger': 0.1.1
+ '@changesets/pre': 2.0.2
+ '@changesets/read': 0.6.6
+ '@changesets/should-skip-package': 0.1.2
+ '@changesets/types': 6.1.0
+ '@changesets/write': 0.4.0
+ '@inquirer/external-editor': 1.0.3(@types/node@20.19.11)
+ '@manypkg/get-packages': 1.1.3
+ ansi-colors: 4.1.3
+ ci-info: 3.9.0
+ enquirer: 2.4.1
+ fs-extra: 7.0.1
+ mri: 1.2.0
+ p-limit: 2.3.0
+ package-manager-detector: 0.2.11
+ picocolors: 1.1.1
+ resolve-from: 5.0.0
+ semver: 7.7.4
+ spawndamnit: 3.0.1
+ term-size: 2.2.1
+ transitivePeerDependencies:
+ - '@types/node'
+
+ '@changesets/config@3.1.2':
+ dependencies:
+ '@changesets/errors': 0.2.0
+ '@changesets/get-dependents-graph': 2.1.3
+ '@changesets/logger': 0.1.1
+ '@changesets/types': 6.1.0
+ '@manypkg/get-packages': 1.1.3
+ fs-extra: 7.0.1
+ micromatch: 4.0.8
+
+ '@changesets/errors@0.2.0':
+ dependencies:
+ extendable-error: 0.1.7
+
+ '@changesets/get-dependents-graph@2.1.3':
+ dependencies:
+ '@changesets/types': 6.1.0
+ '@manypkg/get-packages': 1.1.3
+ picocolors: 1.1.1
+ semver: 7.7.4
+
+ '@changesets/get-release-plan@4.0.14':
+ dependencies:
+ '@changesets/assemble-release-plan': 6.0.9
+ '@changesets/config': 3.1.2
+ '@changesets/pre': 2.0.2
+ '@changesets/read': 0.6.6
+ '@changesets/types': 6.1.0
+ '@manypkg/get-packages': 1.1.3
+
+ '@changesets/get-version-range-type@0.4.0': {}
+
+ '@changesets/git@3.0.4':
+ dependencies:
+ '@changesets/errors': 0.2.0
+ '@manypkg/get-packages': 1.1.3
+ is-subdir: 1.2.0
+ micromatch: 4.0.8
+ spawndamnit: 3.0.1
+
+ '@changesets/logger@0.1.1':
+ dependencies:
+ picocolors: 1.1.1
+
+ '@changesets/parse@0.4.2':
+ dependencies:
+ '@changesets/types': 6.1.0
+ js-yaml: 4.1.1
+
+ '@changesets/pre@2.0.2':
+ dependencies:
+ '@changesets/errors': 0.2.0
+ '@changesets/types': 6.1.0
+ '@manypkg/get-packages': 1.1.3
+ fs-extra: 7.0.1
+
+ '@changesets/read@0.6.6':
+ dependencies:
+ '@changesets/git': 3.0.4
+ '@changesets/logger': 0.1.1
+ '@changesets/parse': 0.4.2
+ '@changesets/types': 6.1.0
+ fs-extra: 7.0.1
+ p-filter: 2.1.0
+ picocolors: 1.1.1
+
+ '@changesets/should-skip-package@0.1.2':
+ dependencies:
+ '@changesets/types': 6.1.0
+ '@manypkg/get-packages': 1.1.3
+
+ '@changesets/types@4.1.0': {}
+
+ '@changesets/types@6.1.0': {}
+
+ '@changesets/write@0.4.0':
+ dependencies:
+ '@changesets/types': 6.1.0
+ fs-extra: 7.0.1
+ human-id: 4.1.3
+ prettier: 2.8.8
+
'@colors/colors@1.5.0':
optional: true
'@coral-xyz/anchor-errors@0.31.1': {}
- '@coral-xyz/anchor@0.31.1(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)':
+ '@coral-xyz/anchor@0.32.1(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)':
dependencies:
'@coral-xyz/anchor-errors': 0.31.1
'@coral-xyz/borsh': 0.31.1(@solana/web3.js@1.98.4(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10))
'@noble/hashes': 1.8.0
'@solana/web3.js': 1.98.4(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)
- bn.js: 5.2.2
+ bn.js: 5.2.3
bs58: 4.0.1
buffer-layout: 1.2.2
camelcase: 6.3.0
@@ -3052,20 +3674,20 @@ snapshots:
'@coral-xyz/borsh@0.31.1(@solana/web3.js@1.98.4(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10))':
dependencies:
'@solana/web3.js': 1.98.4(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)
- bn.js: 5.2.2
+ bn.js: 5.2.3
buffer-layout: 1.2.2
'@cspotcode/source-map-support@0.8.1':
dependencies:
'@jridgewell/trace-mapping': 0.3.9
- '@emnapi/core@1.5.0':
+ '@emnapi/core@1.8.1':
dependencies:
'@emnapi/wasi-threads': 1.1.0
tslib: 2.8.1
optional: true
- '@emnapi/runtime@1.5.0':
+ '@emnapi/runtime@1.8.1':
dependencies:
tslib: 2.8.1
optional: true
@@ -3153,49 +3775,54 @@ snapshots:
'@esbuild/win32-x64@0.25.9':
optional: true
- '@eslint-community/eslint-utils@4.8.0(eslint@9.35.0)':
+ '@eslint-community/eslint-utils@4.9.1(eslint@10.0.0)':
dependencies:
- eslint: 9.35.0
+ eslint: 10.0.0
eslint-visitor-keys: 3.4.3
- '@eslint-community/regexpp@4.12.1': {}
+ '@eslint-community/regexpp@4.12.2': {}
- '@eslint/config-array@0.21.0':
+ '@eslint/config-array@0.23.1':
dependencies:
- '@eslint/object-schema': 2.1.6
- debug: 4.4.1
- minimatch: 3.1.2
+ '@eslint/object-schema': 3.0.1
+ debug: 4.4.3
+ minimatch: 10.2.2
transitivePeerDependencies:
- supports-color
- '@eslint/config-helpers@0.3.1': {}
+ '@eslint/config-helpers@0.5.2':
+ dependencies:
+ '@eslint/core': 1.1.0
- '@eslint/core@0.15.2':
+ '@eslint/core@1.1.0':
dependencies:
'@types/json-schema': 7.0.15
- '@eslint/eslintrc@3.3.1':
+ '@eslint/object-schema@3.0.1': {}
+
+ '@eslint/plugin-kit@0.6.0':
dependencies:
- ajv: 6.12.6
- debug: 4.4.1
- espree: 10.4.0
- globals: 14.0.0
- ignore: 5.3.2
- import-fresh: 3.3.1
- js-yaml: 4.1.0
- minimatch: 3.1.2
- strip-json-comments: 3.1.1
- transitivePeerDependencies:
- - supports-color
+ '@eslint/core': 1.1.0
+ levn: 0.4.1
- '@eslint/js@9.35.0': {}
+ '@fig/complete-commander@3.2.0(commander@11.1.0)':
+ dependencies:
+ commander: 11.1.0
+ prettier: 3.6.2
- '@eslint/object-schema@2.1.6': {}
+ '@gerrit0/mini-shiki@3.22.0':
+ dependencies:
+ '@shikijs/engine-oniguruma': 3.22.0
+ '@shikijs/langs': 3.22.0
+ '@shikijs/themes': 3.22.0
+ '@shikijs/types': 3.22.0
+ '@shikijs/vscode-textmate': 10.0.2
- '@eslint/plugin-kit@0.3.5':
+ '@golevelup/nestjs-discovery@5.0.0(@nestjs/common@11.1.14(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.14(@nestjs/common@11.1.14(reflect-metadata@0.2.2)(rxjs@7.8.2))(reflect-metadata@0.2.2)(rxjs@7.8.2))':
dependencies:
- '@eslint/core': 0.15.2
- levn: 0.4.1
+ '@nestjs/common': 11.1.14(reflect-metadata@0.2.2)(rxjs@7.8.2)
+ '@nestjs/core': 11.1.14(@nestjs/common@11.1.14(reflect-metadata@0.2.2)(rxjs@7.8.2))(reflect-metadata@0.2.2)(rxjs@7.8.2)
+ lodash: 4.17.23
'@humanfs/core@0.19.1': {}
@@ -3208,13 +3835,20 @@ snapshots:
'@humanwhocodes/retry@0.4.3': {}
+ '@inquirer/external-editor@1.0.3(@types/node@20.19.11)':
+ dependencies:
+ chardet: 2.1.1
+ iconv-lite: 0.7.2
+ optionalDependencies:
+ '@types/node': 20.19.11
+
'@inquirer/figures@1.0.13': {}
'@isaacs/cliui@8.0.2':
dependencies:
string-width: 5.1.2
string-width-cjs: string-width@4.2.3
- strip-ansi: 7.1.0
+ strip-ansi: 7.1.2
strip-ansi-cjs: strip-ansi@6.0.1
wrap-ansi: 8.1.0
wrap-ansi-cjs: wrap-ansi@7.0.0
@@ -3224,49 +3858,49 @@ snapshots:
camelcase: 5.3.1
find-up: 4.1.0
get-package-type: 0.1.0
- js-yaml: 3.14.1
+ js-yaml: 3.14.2
resolve-from: 5.0.0
'@istanbuljs/schema@0.1.3': {}
- '@jest/console@30.1.2':
+ '@jest/console@30.2.0':
dependencies:
- '@jest/types': 30.0.5
+ '@jest/types': 30.2.0
'@types/node': 20.19.11
chalk: 4.1.2
- jest-message-util: 30.1.0
- jest-util: 30.0.5
+ jest-message-util: 30.2.0
+ jest-util: 30.2.0
slash: 3.0.0
- '@jest/core@30.1.3(ts-node@10.9.2(@types/node@20.19.11)(typescript@5.9.2))':
+ '@jest/core@30.2.0(ts-node@10.9.2(@types/node@20.19.11)(typescript@5.9.2))':
dependencies:
- '@jest/console': 30.1.2
+ '@jest/console': 30.2.0
'@jest/pattern': 30.0.1
- '@jest/reporters': 30.1.3
- '@jest/test-result': 30.1.3
- '@jest/transform': 30.1.2
- '@jest/types': 30.0.5
+ '@jest/reporters': 30.2.0
+ '@jest/test-result': 30.2.0
+ '@jest/transform': 30.2.0
+ '@jest/types': 30.2.0
'@types/node': 20.19.11
ansi-escapes: 4.3.2
chalk: 4.1.2
- ci-info: 4.3.0
+ ci-info: 4.4.0
exit-x: 0.2.2
graceful-fs: 4.2.11
- jest-changed-files: 30.0.5
- jest-config: 30.1.3(@types/node@20.19.11)(ts-node@10.9.2(@types/node@20.19.11)(typescript@5.9.2))
- jest-haste-map: 30.1.0
- jest-message-util: 30.1.0
+ jest-changed-files: 30.2.0
+ jest-config: 30.2.0(@types/node@20.19.11)(ts-node@10.9.2(@types/node@20.19.11)(typescript@5.9.2))
+ jest-haste-map: 30.2.0
+ jest-message-util: 30.2.0
jest-regex-util: 30.0.1
- jest-resolve: 30.1.3
- jest-resolve-dependencies: 30.1.3
- jest-runner: 30.1.3
- jest-runtime: 30.1.3
- jest-snapshot: 30.1.2
- jest-util: 30.0.5
- jest-validate: 30.1.0
- jest-watcher: 30.1.3
+ jest-resolve: 30.2.0
+ jest-resolve-dependencies: 30.2.0
+ jest-runner: 30.2.0
+ jest-runtime: 30.2.0
+ jest-snapshot: 30.2.0
+ jest-util: 30.2.0
+ jest-validate: 30.2.0
+ jest-watcher: 30.2.0
micromatch: 4.0.8
- pretty-format: 30.0.5
+ pretty-format: 30.2.0
slash: 3.0.0
transitivePeerDependencies:
- babel-plugin-macros
@@ -3276,41 +3910,41 @@ snapshots:
'@jest/diff-sequences@30.0.1': {}
- '@jest/environment@30.1.2':
+ '@jest/environment@30.2.0':
dependencies:
- '@jest/fake-timers': 30.1.2
- '@jest/types': 30.0.5
+ '@jest/fake-timers': 30.2.0
+ '@jest/types': 30.2.0
'@types/node': 20.19.11
- jest-mock: 30.0.5
+ jest-mock: 30.2.0
- '@jest/expect-utils@30.1.2':
+ '@jest/expect-utils@30.2.0':
dependencies:
'@jest/get-type': 30.1.0
- '@jest/expect@30.1.2':
+ '@jest/expect@30.2.0':
dependencies:
- expect: 30.1.2
- jest-snapshot: 30.1.2
+ expect: 30.2.0
+ jest-snapshot: 30.2.0
transitivePeerDependencies:
- supports-color
- '@jest/fake-timers@30.1.2':
+ '@jest/fake-timers@30.2.0':
dependencies:
- '@jest/types': 30.0.5
+ '@jest/types': 30.2.0
'@sinonjs/fake-timers': 13.0.5
'@types/node': 20.19.11
- jest-message-util: 30.1.0
- jest-mock: 30.0.5
- jest-util: 30.0.5
+ jest-message-util: 30.2.0
+ jest-mock: 30.2.0
+ jest-util: 30.2.0
'@jest/get-type@30.1.0': {}
- '@jest/globals@30.1.2':
+ '@jest/globals@30.2.0':
dependencies:
- '@jest/environment': 30.1.2
- '@jest/expect': 30.1.2
- '@jest/types': 30.0.5
- jest-mock: 30.0.5
+ '@jest/environment': 30.2.0
+ '@jest/expect': 30.2.0
+ '@jest/types': 30.2.0
+ jest-mock: 30.2.0
transitivePeerDependencies:
- supports-color
@@ -3319,28 +3953,28 @@ snapshots:
'@types/node': 20.19.11
jest-regex-util: 30.0.1
- '@jest/reporters@30.1.3':
+ '@jest/reporters@30.2.0':
dependencies:
'@bcoe/v8-coverage': 0.2.3
- '@jest/console': 30.1.2
- '@jest/test-result': 30.1.3
- '@jest/transform': 30.1.2
- '@jest/types': 30.0.5
- '@jridgewell/trace-mapping': 0.3.30
+ '@jest/console': 30.2.0
+ '@jest/test-result': 30.2.0
+ '@jest/transform': 30.2.0
+ '@jest/types': 30.2.0
+ '@jridgewell/trace-mapping': 0.3.31
'@types/node': 20.19.11
chalk: 4.1.2
- collect-v8-coverage: 1.0.2
+ collect-v8-coverage: 1.0.3
exit-x: 0.2.2
- glob: 10.4.5
+ glob: 10.5.0
graceful-fs: 4.2.11
istanbul-lib-coverage: 3.2.2
istanbul-lib-instrument: 6.0.3
istanbul-lib-report: 3.0.1
istanbul-lib-source-maps: 5.0.6
istanbul-reports: 3.2.0
- jest-message-util: 30.1.0
- jest-util: 30.0.5
- jest-worker: 30.1.0
+ jest-message-util: 30.2.0
+ jest-util: 30.2.0
+ jest-worker: 30.2.0
slash: 3.0.0
string-length: 4.0.2
v8-to-istanbul: 9.3.0
@@ -3349,48 +3983,48 @@ snapshots:
'@jest/schemas@30.0.5':
dependencies:
- '@sinclair/typebox': 0.34.41
+ '@sinclair/typebox': 0.34.48
- '@jest/snapshot-utils@30.1.2':
+ '@jest/snapshot-utils@30.2.0':
dependencies:
- '@jest/types': 30.0.5
+ '@jest/types': 30.2.0
chalk: 4.1.2
graceful-fs: 4.2.11
natural-compare: 1.4.0
'@jest/source-map@30.0.1':
dependencies:
- '@jridgewell/trace-mapping': 0.3.30
+ '@jridgewell/trace-mapping': 0.3.31
callsites: 3.1.0
graceful-fs: 4.2.11
- '@jest/test-result@30.1.3':
+ '@jest/test-result@30.2.0':
dependencies:
- '@jest/console': 30.1.2
- '@jest/types': 30.0.5
+ '@jest/console': 30.2.0
+ '@jest/types': 30.2.0
'@types/istanbul-lib-coverage': 2.0.6
- collect-v8-coverage: 1.0.2
+ collect-v8-coverage: 1.0.3
- '@jest/test-sequencer@30.1.3':
+ '@jest/test-sequencer@30.2.0':
dependencies:
- '@jest/test-result': 30.1.3
+ '@jest/test-result': 30.2.0
graceful-fs: 4.2.11
- jest-haste-map: 30.1.0
+ jest-haste-map: 30.2.0
slash: 3.0.0
- '@jest/transform@30.1.2':
+ '@jest/transform@30.2.0':
dependencies:
- '@babel/core': 7.28.4
- '@jest/types': 30.0.5
- '@jridgewell/trace-mapping': 0.3.30
+ '@babel/core': 7.29.0
+ '@jest/types': 30.2.0
+ '@jridgewell/trace-mapping': 0.3.31
babel-plugin-istanbul: 7.0.1
chalk: 4.1.2
convert-source-map: 2.0.0
fast-json-stable-stringify: 2.1.0
graceful-fs: 4.2.11
- jest-haste-map: 30.1.0
+ jest-haste-map: 30.2.0
jest-regex-util: 30.0.1
- jest-util: 30.0.5
+ jest-util: 30.2.0
micromatch: 4.0.8
pirates: 4.0.7
slash: 3.0.0
@@ -3398,31 +4032,31 @@ snapshots:
transitivePeerDependencies:
- supports-color
- '@jest/types@30.0.5':
+ '@jest/types@30.2.0':
dependencies:
'@jest/pattern': 30.0.1
'@jest/schemas': 30.0.5
'@types/istanbul-lib-coverage': 2.0.6
'@types/istanbul-reports': 3.0.4
'@types/node': 20.19.11
- '@types/yargs': 17.0.33
+ '@types/yargs': 17.0.35
chalk: 4.1.2
'@jridgewell/gen-mapping@0.3.13':
dependencies:
'@jridgewell/sourcemap-codec': 1.5.5
- '@jridgewell/trace-mapping': 0.3.30
+ '@jridgewell/trace-mapping': 0.3.31
'@jridgewell/remapping@2.3.5':
dependencies:
'@jridgewell/gen-mapping': 0.3.13
- '@jridgewell/trace-mapping': 0.3.30
+ '@jridgewell/trace-mapping': 0.3.31
'@jridgewell/resolve-uri@3.1.2': {}
'@jridgewell/sourcemap-codec@1.5.5': {}
- '@jridgewell/trace-mapping@0.3.30':
+ '@jridgewell/trace-mapping@0.3.31':
dependencies:
'@jridgewell/resolve-uri': 3.1.2
'@jridgewell/sourcemap-codec': 1.5.5
@@ -3432,13 +4066,63 @@ snapshots:
'@jridgewell/resolve-uri': 3.1.2
'@jridgewell/sourcemap-codec': 1.5.5
+ '@lukeed/csprng@1.1.0': {}
+
+ '@manypkg/find-root@1.1.0':
+ dependencies:
+ '@babel/runtime': 7.28.3
+ '@types/node': 12.20.55
+ find-up: 4.1.0
+ fs-extra: 8.1.0
+
+ '@manypkg/get-packages@1.1.3':
+ dependencies:
+ '@babel/runtime': 7.28.3
+ '@changesets/types': 4.1.0
+ '@manypkg/find-root': 1.1.0
+ fs-extra: 8.1.0
+ globby: 11.1.0
+ read-yaml-file: 1.1.0
+
'@napi-rs/wasm-runtime@0.2.12':
dependencies:
- '@emnapi/core': 1.5.0
- '@emnapi/runtime': 1.5.0
- '@tybys/wasm-util': 0.10.0
+ '@emnapi/core': 1.8.1
+ '@emnapi/runtime': 1.8.1
+ '@tybys/wasm-util': 0.10.1
optional: true
+ '@nestjs/common@11.1.14(reflect-metadata@0.2.2)(rxjs@7.8.2)':
+ dependencies:
+ file-type: 21.3.0
+ iterare: 1.2.1
+ load-esm: 1.0.3
+ reflect-metadata: 0.2.2
+ rxjs: 7.8.2
+ tslib: 2.8.1
+ uid: 2.0.2
+ transitivePeerDependencies:
+ - supports-color
+
+ '@nestjs/config@4.0.3(@nestjs/common@11.1.14(reflect-metadata@0.2.2)(rxjs@7.8.2))(rxjs@7.8.2)':
+ dependencies:
+ '@nestjs/common': 11.1.14(reflect-metadata@0.2.2)(rxjs@7.8.2)
+ dotenv: 17.2.3
+ dotenv-expand: 12.0.3
+ lodash: 4.17.23
+ rxjs: 7.8.2
+
+ '@nestjs/core@11.1.14(@nestjs/common@11.1.14(reflect-metadata@0.2.2)(rxjs@7.8.2))(reflect-metadata@0.2.2)(rxjs@7.8.2)':
+ dependencies:
+ '@nestjs/common': 11.1.14(reflect-metadata@0.2.2)(rxjs@7.8.2)
+ '@nuxt/opencollective': 0.4.1
+ fast-safe-stringify: 2.1.1
+ iterare: 1.2.1
+ path-to-regexp: 8.3.0
+ reflect-metadata: 0.2.2
+ rxjs: 7.8.2
+ tslib: 2.8.1
+ uid: 2.0.2
+
'@noble/ciphers@1.3.0': {}
'@noble/curves@1.2.0':
@@ -3473,7 +4157,11 @@ snapshots:
'@nodelib/fs.walk@1.2.8':
dependencies:
'@nodelib/fs.scandir': 2.1.5
- fastq: 1.19.1
+ fastq: 1.20.1
+
+ '@nuxt/opencollective@0.4.1':
+ dependencies:
+ consola: 3.4.2
'@pkgjs/parseargs@0.11.0':
optional: true
@@ -3492,7 +4180,7 @@ snapshots:
'@scure/bip32@1.7.0':
dependencies:
- '@noble/curves': 1.9.7
+ '@noble/curves': 1.9.1
'@noble/hashes': 1.8.0
'@scure/base': 1.2.6
@@ -3506,7 +4194,27 @@ snapshots:
'@noble/hashes': 1.8.0
'@scure/base': 1.2.6
- '@sinclair/typebox@0.34.41': {}
+ '@shikijs/engine-oniguruma@3.22.0':
+ dependencies:
+ '@shikijs/types': 3.22.0
+ '@shikijs/vscode-textmate': 10.0.2
+
+ '@shikijs/langs@3.22.0':
+ dependencies:
+ '@shikijs/types': 3.22.0
+
+ '@shikijs/themes@3.22.0':
+ dependencies:
+ '@shikijs/types': 3.22.0
+
+ '@shikijs/types@3.22.0':
+ dependencies:
+ '@shikijs/vscode-textmate': 10.0.2
+ '@types/hast': 3.0.4
+
+ '@shikijs/vscode-textmate@10.0.2': {}
+
+ '@sinclair/typebox@0.34.48': {}
'@sinonjs/commons@3.0.1':
dependencies:
@@ -3521,7 +4229,7 @@ snapshots:
'@solana/buffer-layout': 4.0.1
'@solana/web3.js': 1.98.4(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)
bigint-buffer: 1.1.5
- bignumber.js: 9.1.2
+ bignumber.js: 9.3.1
transitivePeerDependencies:
- bufferutil
- encoding
@@ -3582,7 +4290,7 @@ snapshots:
'@solana/errors@2.0.0-rc.1(typescript@5.9.2)':
dependencies:
- chalk: 5.6.0
+ chalk: 5.6.2
commander: 12.1.0
typescript: 5.9.2
@@ -3642,7 +4350,7 @@ snapshots:
'@solana/buffer-layout': 4.0.1
'@solana/codecs-numbers': 2.3.0(typescript@5.9.2)
agentkeepalive: 4.6.0
- bn.js: 5.2.2
+ bn.js: 5.2.3
borsh: 0.7.0
bs58: 4.0.1
buffer: 6.0.3
@@ -3661,6 +4369,15 @@ snapshots:
dependencies:
tslib: 2.8.1
+ '@tokenizer/inflate@0.4.1':
+ dependencies:
+ debug: 4.4.3
+ token-types: 6.1.2
+ transitivePeerDependencies:
+ - supports-color
+
+ '@tokenizer/token@0.3.0': {}
+
'@tsconfig/node10@1.0.11': {}
'@tsconfig/node12@1.0.11': {}
@@ -3669,31 +4386,31 @@ snapshots:
'@tsconfig/node16@1.0.4': {}
- '@tybys/wasm-util@0.10.0':
+ '@tybys/wasm-util@0.10.1':
dependencies:
tslib: 2.8.1
optional: true
'@types/babel__core@7.20.5':
dependencies:
- '@babel/parser': 7.28.4
- '@babel/types': 7.28.4
+ '@babel/parser': 7.29.0
+ '@babel/types': 7.29.0
'@types/babel__generator': 7.27.0
'@types/babel__template': 7.4.4
'@types/babel__traverse': 7.28.0
'@types/babel__generator@7.27.0':
dependencies:
- '@babel/types': 7.28.4
+ '@babel/types': 7.29.0
'@types/babel__template@7.4.4':
dependencies:
- '@babel/parser': 7.28.4
- '@babel/types': 7.28.4
+ '@babel/parser': 7.29.0
+ '@babel/types': 7.29.0
'@types/babel__traverse@7.28.0':
dependencies:
- '@babel/types': 7.28.4
+ '@babel/types': 7.29.0
'@types/bn.js@5.2.0':
dependencies:
@@ -3703,8 +4420,14 @@ snapshots:
dependencies:
'@types/node': 20.19.11
+ '@types/esrecurse@4.3.1': {}
+
'@types/estree@1.0.8': {}
+ '@types/hast@3.0.4':
+ dependencies:
+ '@types/unist': 3.0.3
+
'@types/inquirer@9.0.9':
dependencies:
'@types/through': 0.0.33
@@ -3722,8 +4445,8 @@ snapshots:
'@types/jest@30.0.0':
dependencies:
- expect: 30.1.2
- pretty-format: 30.0.5
+ expect: 30.2.0
+ pretty-format: 30.2.0
'@types/json-schema@7.0.15': {}
@@ -3743,6 +4466,8 @@ snapshots:
dependencies:
'@types/node': 20.19.11
+ '@types/unist@3.0.3': {}
+
'@types/uuid@8.3.4': {}
'@types/ws@7.4.7':
@@ -3755,102 +4480,100 @@ snapshots:
'@types/yargs-parser@21.0.3': {}
- '@types/yargs@17.0.33':
+ '@types/yargs@17.0.35':
dependencies:
'@types/yargs-parser': 21.0.3
- '@typescript-eslint/eslint-plugin@8.43.0(@typescript-eslint/parser@8.43.0(eslint@9.35.0)(typescript@5.9.2))(eslint@9.35.0)(typescript@5.9.2)':
+ '@typescript-eslint/eslint-plugin@8.56.0(@typescript-eslint/parser@8.56.0(eslint@10.0.0)(typescript@5.9.2))(eslint@10.0.0)(typescript@5.9.2)':
dependencies:
- '@eslint-community/regexpp': 4.12.1
- '@typescript-eslint/parser': 8.43.0(eslint@9.35.0)(typescript@5.9.2)
- '@typescript-eslint/scope-manager': 8.43.0
- '@typescript-eslint/type-utils': 8.43.0(eslint@9.35.0)(typescript@5.9.2)
- '@typescript-eslint/utils': 8.43.0(eslint@9.35.0)(typescript@5.9.2)
- '@typescript-eslint/visitor-keys': 8.43.0
- eslint: 9.35.0
- graphemer: 1.4.0
+ '@eslint-community/regexpp': 4.12.2
+ '@typescript-eslint/parser': 8.56.0(eslint@10.0.0)(typescript@5.9.2)
+ '@typescript-eslint/scope-manager': 8.56.0
+ '@typescript-eslint/type-utils': 8.56.0(eslint@10.0.0)(typescript@5.9.2)
+ '@typescript-eslint/utils': 8.56.0(eslint@10.0.0)(typescript@5.9.2)
+ '@typescript-eslint/visitor-keys': 8.56.0
+ eslint: 10.0.0
ignore: 7.0.5
natural-compare: 1.4.0
- ts-api-utils: 2.1.0(typescript@5.9.2)
+ ts-api-utils: 2.4.0(typescript@5.9.2)
typescript: 5.9.2
transitivePeerDependencies:
- supports-color
- '@typescript-eslint/parser@8.43.0(eslint@9.35.0)(typescript@5.9.2)':
+ '@typescript-eslint/parser@8.56.0(eslint@10.0.0)(typescript@5.9.2)':
dependencies:
- '@typescript-eslint/scope-manager': 8.43.0
- '@typescript-eslint/types': 8.43.0
- '@typescript-eslint/typescript-estree': 8.43.0(typescript@5.9.2)
- '@typescript-eslint/visitor-keys': 8.43.0
- debug: 4.4.1
- eslint: 9.35.0
+ '@typescript-eslint/scope-manager': 8.56.0
+ '@typescript-eslint/types': 8.56.0
+ '@typescript-eslint/typescript-estree': 8.56.0(typescript@5.9.2)
+ '@typescript-eslint/visitor-keys': 8.56.0
+ debug: 4.4.3
+ eslint: 10.0.0
typescript: 5.9.2
transitivePeerDependencies:
- supports-color
- '@typescript-eslint/project-service@8.43.0(typescript@5.9.2)':
+ '@typescript-eslint/project-service@8.56.0(typescript@5.9.2)':
dependencies:
- '@typescript-eslint/tsconfig-utils': 8.43.0(typescript@5.9.2)
- '@typescript-eslint/types': 8.43.0
- debug: 4.4.1
+ '@typescript-eslint/tsconfig-utils': 8.56.0(typescript@5.9.2)
+ '@typescript-eslint/types': 8.56.0
+ debug: 4.4.3
typescript: 5.9.2
transitivePeerDependencies:
- supports-color
- '@typescript-eslint/scope-manager@8.43.0':
+ '@typescript-eslint/scope-manager@8.56.0':
dependencies:
- '@typescript-eslint/types': 8.43.0
- '@typescript-eslint/visitor-keys': 8.43.0
+ '@typescript-eslint/types': 8.56.0
+ '@typescript-eslint/visitor-keys': 8.56.0
- '@typescript-eslint/tsconfig-utils@8.43.0(typescript@5.9.2)':
+ '@typescript-eslint/tsconfig-utils@8.56.0(typescript@5.9.2)':
dependencies:
typescript: 5.9.2
- '@typescript-eslint/type-utils@8.43.0(eslint@9.35.0)(typescript@5.9.2)':
+ '@typescript-eslint/type-utils@8.56.0(eslint@10.0.0)(typescript@5.9.2)':
dependencies:
- '@typescript-eslint/types': 8.43.0
- '@typescript-eslint/typescript-estree': 8.43.0(typescript@5.9.2)
- '@typescript-eslint/utils': 8.43.0(eslint@9.35.0)(typescript@5.9.2)
- debug: 4.4.1
- eslint: 9.35.0
- ts-api-utils: 2.1.0(typescript@5.9.2)
+ '@typescript-eslint/types': 8.56.0
+ '@typescript-eslint/typescript-estree': 8.56.0(typescript@5.9.2)
+ '@typescript-eslint/utils': 8.56.0(eslint@10.0.0)(typescript@5.9.2)
+ debug: 4.4.3
+ eslint: 10.0.0
+ ts-api-utils: 2.4.0(typescript@5.9.2)
typescript: 5.9.2
transitivePeerDependencies:
- supports-color
- '@typescript-eslint/types@8.43.0': {}
+ '@typescript-eslint/types@8.56.0': {}
- '@typescript-eslint/typescript-estree@8.43.0(typescript@5.9.2)':
+ '@typescript-eslint/typescript-estree@8.56.0(typescript@5.9.2)':
dependencies:
- '@typescript-eslint/project-service': 8.43.0(typescript@5.9.2)
- '@typescript-eslint/tsconfig-utils': 8.43.0(typescript@5.9.2)
- '@typescript-eslint/types': 8.43.0
- '@typescript-eslint/visitor-keys': 8.43.0
- debug: 4.4.1
- fast-glob: 3.3.3
- is-glob: 4.0.3
- minimatch: 9.0.5
- semver: 7.7.2
- ts-api-utils: 2.1.0(typescript@5.9.2)
+ '@typescript-eslint/project-service': 8.56.0(typescript@5.9.2)
+ '@typescript-eslint/tsconfig-utils': 8.56.0(typescript@5.9.2)
+ '@typescript-eslint/types': 8.56.0
+ '@typescript-eslint/visitor-keys': 8.56.0
+ debug: 4.4.3
+ minimatch: 10.2.2
+ semver: 7.7.4
+ tinyglobby: 0.2.15
+ ts-api-utils: 2.4.0(typescript@5.9.2)
typescript: 5.9.2
transitivePeerDependencies:
- supports-color
- '@typescript-eslint/utils@8.43.0(eslint@9.35.0)(typescript@5.9.2)':
+ '@typescript-eslint/utils@8.56.0(eslint@10.0.0)(typescript@5.9.2)':
dependencies:
- '@eslint-community/eslint-utils': 4.8.0(eslint@9.35.0)
- '@typescript-eslint/scope-manager': 8.43.0
- '@typescript-eslint/types': 8.43.0
- '@typescript-eslint/typescript-estree': 8.43.0(typescript@5.9.2)
- eslint: 9.35.0
+ '@eslint-community/eslint-utils': 4.9.1(eslint@10.0.0)
+ '@typescript-eslint/scope-manager': 8.56.0
+ '@typescript-eslint/types': 8.56.0
+ '@typescript-eslint/typescript-estree': 8.56.0(typescript@5.9.2)
+ eslint: 10.0.0
typescript: 5.9.2
transitivePeerDependencies:
- supports-color
- '@typescript-eslint/visitor-keys@8.43.0':
+ '@typescript-eslint/visitor-keys@8.56.0':
dependencies:
- '@typescript-eslint/types': 8.43.0
- eslint-visitor-keys: 4.2.1
+ '@typescript-eslint/types': 8.56.0
+ eslint-visitor-keys: 5.0.0
'@ungap/structured-clone@1.3.0': {}
@@ -3913,13 +4636,16 @@ snapshots:
'@unrs/resolver-binding-win32-x64-msvc@1.11.1':
optional: true
- abitype@1.1.0(typescript@5.9.2):
+ '@vercel/ncc@0.38.4': {}
+
+ abitype@1.1.0(typescript@5.9.2)(zod@4.3.6):
optionalDependencies:
typescript: 5.9.2
+ zod: 4.3.6
- acorn-jsx@5.3.2(acorn@8.15.0):
+ acorn-jsx@5.3.2(acorn@8.16.0):
dependencies:
- acorn: 8.15.0
+ acorn: 8.16.0
acorn-walk@8.3.4:
dependencies:
@@ -3927,6 +4653,8 @@ snapshots:
acorn@8.15.0: {}
+ acorn@8.16.0: {}
+
aes-js@4.0.0-beta.5: {}
agentkeepalive@4.6.0:
@@ -3940,6 +4668,8 @@ snapshots:
json-schema-traverse: 0.4.1
uri-js: 4.4.1
+ ansi-colors@4.1.3: {}
+
ansi-escapes@4.3.2:
dependencies:
type-fest: 0.21.3
@@ -3952,6 +4682,8 @@ snapshots:
ansi-regex@6.2.0: {}
+ ansi-regex@6.2.2: {}
+
ansi-styles@4.3.0:
dependencies:
color-convert: 2.0.1
@@ -3960,6 +4692,8 @@ snapshots:
ansi-styles@6.2.1: {}
+ ansi-styles@6.2.3: {}
+
anymatch@3.1.3:
dependencies:
normalize-path: 3.0.0
@@ -3973,23 +4707,25 @@ snapshots:
argparse@2.0.1: {}
+ array-union@2.1.0: {}
+
asynckit@0.4.0: {}
- axios@1.11.0:
+ axios@1.13.5:
dependencies:
follow-redirects: 1.15.11
- form-data: 4.0.4
+ form-data: 4.0.5
proxy-from-env: 1.1.0
transitivePeerDependencies:
- debug
- babel-jest@30.1.2(@babel/core@7.28.4):
+ babel-jest@30.2.0(@babel/core@7.29.0):
dependencies:
- '@babel/core': 7.28.4
- '@jest/transform': 30.1.2
+ '@babel/core': 7.29.0
+ '@jest/transform': 30.2.0
'@types/babel__core': 7.20.5
babel-plugin-istanbul: 7.0.1
- babel-preset-jest: 30.0.1(@babel/core@7.28.4)
+ babel-preset-jest: 30.2.0(@babel/core@7.29.0)
chalk: 4.1.2
graceful-fs: 4.2.11
slash: 3.0.0
@@ -3998,46 +4734,44 @@ snapshots:
babel-plugin-istanbul@7.0.1:
dependencies:
- '@babel/helper-plugin-utils': 7.27.1
+ '@babel/helper-plugin-utils': 7.28.6
'@istanbuljs/load-nyc-config': 1.1.0
'@istanbuljs/schema': 0.1.3
istanbul-lib-instrument: 6.0.3
- test-exclude: 6.0.0
+ test-exclude: 8.0.0
transitivePeerDependencies:
- supports-color
- babel-plugin-jest-hoist@30.0.1:
+ babel-plugin-jest-hoist@30.2.0:
dependencies:
- '@babel/template': 7.27.2
- '@babel/types': 7.28.4
'@types/babel__core': 7.20.5
- babel-preset-current-node-syntax@1.2.0(@babel/core@7.28.4):
- dependencies:
- '@babel/core': 7.28.4
- '@babel/plugin-syntax-async-generators': 7.8.4(@babel/core@7.28.4)
- '@babel/plugin-syntax-bigint': 7.8.3(@babel/core@7.28.4)
- '@babel/plugin-syntax-class-properties': 7.12.13(@babel/core@7.28.4)
- '@babel/plugin-syntax-class-static-block': 7.14.5(@babel/core@7.28.4)
- '@babel/plugin-syntax-import-attributes': 7.27.1(@babel/core@7.28.4)
- '@babel/plugin-syntax-import-meta': 7.10.4(@babel/core@7.28.4)
- '@babel/plugin-syntax-json-strings': 7.8.3(@babel/core@7.28.4)
- '@babel/plugin-syntax-logical-assignment-operators': 7.10.4(@babel/core@7.28.4)
- '@babel/plugin-syntax-nullish-coalescing-operator': 7.8.3(@babel/core@7.28.4)
- '@babel/plugin-syntax-numeric-separator': 7.10.4(@babel/core@7.28.4)
- '@babel/plugin-syntax-object-rest-spread': 7.8.3(@babel/core@7.28.4)
- '@babel/plugin-syntax-optional-catch-binding': 7.8.3(@babel/core@7.28.4)
- '@babel/plugin-syntax-optional-chaining': 7.8.3(@babel/core@7.28.4)
- '@babel/plugin-syntax-private-property-in-object': 7.14.5(@babel/core@7.28.4)
- '@babel/plugin-syntax-top-level-await': 7.14.5(@babel/core@7.28.4)
-
- babel-preset-jest@30.0.1(@babel/core@7.28.4):
- dependencies:
- '@babel/core': 7.28.4
- babel-plugin-jest-hoist: 30.0.1
- babel-preset-current-node-syntax: 1.2.0(@babel/core@7.28.4)
-
- balanced-match@1.0.2: {}
+ babel-preset-current-node-syntax@1.2.0(@babel/core@7.29.0):
+ dependencies:
+ '@babel/core': 7.29.0
+ '@babel/plugin-syntax-async-generators': 7.8.4(@babel/core@7.29.0)
+ '@babel/plugin-syntax-bigint': 7.8.3(@babel/core@7.29.0)
+ '@babel/plugin-syntax-class-properties': 7.12.13(@babel/core@7.29.0)
+ '@babel/plugin-syntax-class-static-block': 7.14.5(@babel/core@7.29.0)
+ '@babel/plugin-syntax-import-attributes': 7.28.6(@babel/core@7.29.0)
+ '@babel/plugin-syntax-import-meta': 7.10.4(@babel/core@7.29.0)
+ '@babel/plugin-syntax-json-strings': 7.8.3(@babel/core@7.29.0)
+ '@babel/plugin-syntax-logical-assignment-operators': 7.10.4(@babel/core@7.29.0)
+ '@babel/plugin-syntax-nullish-coalescing-operator': 7.8.3(@babel/core@7.29.0)
+ '@babel/plugin-syntax-numeric-separator': 7.10.4(@babel/core@7.29.0)
+ '@babel/plugin-syntax-object-rest-spread': 7.8.3(@babel/core@7.29.0)
+ '@babel/plugin-syntax-optional-catch-binding': 7.8.3(@babel/core@7.29.0)
+ '@babel/plugin-syntax-optional-chaining': 7.8.3(@babel/core@7.29.0)
+ '@babel/plugin-syntax-private-property-in-object': 7.14.5(@babel/core@7.29.0)
+ '@babel/plugin-syntax-top-level-await': 7.14.5(@babel/core@7.29.0)
+
+ babel-preset-jest@30.2.0(@babel/core@7.29.0):
+ dependencies:
+ '@babel/core': 7.29.0
+ babel-plugin-jest-hoist: 30.2.0
+ babel-preset-current-node-syntax: 1.2.0(@babel/core@7.29.0)
+
+ balanced-match@4.0.3: {}
base-x@3.0.11:
dependencies:
@@ -4045,12 +4779,20 @@ snapshots:
base64-js@1.5.1: {}
+ baseline-browser-mapping@2.10.0: {}
+
+ better-path-resolve@1.0.0:
+ dependencies:
+ is-windows: 1.0.2
+
bigint-buffer@1.1.5:
dependencies:
bindings: 1.5.0
bignumber.js@9.1.2: {}
+ bignumber.js@9.3.1: {}
+
bindings@1.5.0:
dependencies:
file-uri-to-path: 1.0.0
@@ -4061,33 +4803,29 @@ snapshots:
inherits: 2.0.4
readable-stream: 3.6.2
- bn.js@5.2.2: {}
+ bn.js@5.2.3: {}
borsh@0.7.0:
dependencies:
- bn.js: 5.2.2
+ bn.js: 5.2.3
bs58: 4.0.1
text-encoding-utf-8: 1.0.2
- brace-expansion@1.1.12:
- dependencies:
- balanced-match: 1.0.2
- concat-map: 0.0.1
-
- brace-expansion@2.0.2:
+ brace-expansion@5.0.2:
dependencies:
- balanced-match: 1.0.2
+ balanced-match: 4.0.3
braces@3.0.3:
dependencies:
fill-range: 7.1.1
- browserslist@4.25.4:
+ browserslist@4.28.1:
dependencies:
- caniuse-lite: 1.0.30001741
- electron-to-chromium: 1.5.214
- node-releases: 2.0.20
- update-browserslist-db: 1.1.3(browserslist@4.25.4)
+ baseline-browser-mapping: 2.10.0
+ caniuse-lite: 1.0.30001770
+ electron-to-chromium: 1.5.286
+ node-releases: 2.0.27
+ update-browserslist-db: 1.2.3(browserslist@4.28.1)
bs-logger@0.2.6:
dependencies:
@@ -4131,7 +4869,7 @@ snapshots:
camelcase@6.3.0: {}
- caniuse-lite@1.0.30001741: {}
+ caniuse-lite@1.0.30001770: {}
chalk@4.1.2:
dependencies:
@@ -4140,13 +4878,19 @@ snapshots:
chalk@5.6.0: {}
+ chalk@5.6.2: {}
+
char-regex@1.0.2: {}
chardet@0.7.0: {}
- ci-info@4.3.0: {}
+ chardet@2.1.1: {}
+
+ ci-info@3.9.0: {}
+
+ ci-info@4.4.0: {}
- cjs-module-lexer@2.1.0: {}
+ cjs-module-lexer@2.2.0: {}
cli-cursor@3.1.0:
dependencies:
@@ -4169,6 +4913,8 @@ snapshots:
slice-ansi: 5.0.0
string-width: 7.2.0
+ cli-width@3.0.0: {}
+
cli-width@4.1.0: {}
cliui@8.0.1:
@@ -4181,7 +4927,7 @@ snapshots:
co@4.6.0: {}
- collect-v8-coverage@1.0.2: {}
+ collect-v8-coverage@1.0.3: {}
color-convert@2.0.1:
dependencies:
@@ -4195,16 +4941,27 @@ snapshots:
dependencies:
delayed-stream: 1.0.0
+ commander@11.1.0: {}
+
commander@12.1.0: {}
commander@14.0.0: {}
commander@2.20.3: {}
- concat-map@0.0.1: {}
+ consola@3.4.2: {}
convert-source-map@2.0.0: {}
+ cosmiconfig@8.3.6(typescript@5.9.2):
+ dependencies:
+ import-fresh: 3.3.1
+ js-yaml: 4.1.1
+ parse-json: 5.2.0
+ path-type: 4.0.0
+ optionalDependencies:
+ typescript: 5.9.2
+
create-require@1.1.1: {}
cross-fetch@3.2.0:
@@ -4223,7 +4980,11 @@ snapshots:
dependencies:
ms: 2.1.3
- dedent@1.7.0: {}
+ debug@4.4.3:
+ dependencies:
+ ms: 2.1.3
+
+ dedent@1.7.1: {}
deep-is@0.1.4: {}
@@ -4237,12 +4998,24 @@ snapshots:
delayed-stream@1.0.0: {}
+ detect-indent@6.1.0: {}
+
detect-newline@3.1.0: {}
diff@4.0.2: {}
+ dir-glob@3.0.1:
+ dependencies:
+ path-type: 4.0.0
+
+ dotenv-expand@12.0.3:
+ dependencies:
+ dotenv: 16.6.1
+
dotenv@16.6.1: {}
+ dotenv@17.2.3: {}
+
dunder-proto@1.0.1:
dependencies:
call-bind-apply-helpers: 1.0.2
@@ -4251,7 +5024,7 @@ snapshots:
eastasianwidth@0.2.0: {}
- electron-to-chromium@1.5.214: {}
+ electron-to-chromium@1.5.286: {}
emittery@0.13.1: {}
@@ -4261,9 +5034,16 @@ snapshots:
emoji-regex@9.2.2: {}
+ enquirer@2.4.1:
+ dependencies:
+ ansi-colors: 4.1.3
+ strip-ansi: 6.0.1
+
+ entities@4.5.0: {}
+
environment@1.1.0: {}
- error-ex@1.3.2:
+ error-ex@1.3.4:
dependencies:
is-arrayish: 0.2.1
@@ -4319,60 +5099,60 @@ snapshots:
escalade@3.2.0: {}
+ escape-string-regexp@1.0.5: {}
+
escape-string-regexp@2.0.0: {}
escape-string-regexp@4.0.0: {}
- eslint-config-prettier@9.1.2(eslint@9.35.0):
+ eslint-config-prettier@9.1.2(eslint@10.0.0):
dependencies:
- eslint: 9.35.0
+ eslint: 10.0.0
- eslint-plugin-prettier@5.5.4(eslint-config-prettier@9.1.2(eslint@9.35.0))(eslint@9.35.0)(prettier@3.6.2):
+ eslint-plugin-prettier@5.5.4(eslint-config-prettier@9.1.2(eslint@10.0.0))(eslint@10.0.0)(prettier@3.6.2):
dependencies:
- eslint: 9.35.0
+ eslint: 10.0.0
prettier: 3.6.2
prettier-linter-helpers: 1.0.0
synckit: 0.11.11
optionalDependencies:
- eslint-config-prettier: 9.1.2(eslint@9.35.0)
+ eslint-config-prettier: 9.1.2(eslint@10.0.0)
- eslint-plugin-simple-import-sort@12.1.1(eslint@9.35.0):
+ eslint-plugin-simple-import-sort@12.1.1(eslint@10.0.0):
dependencies:
- eslint: 9.35.0
+ eslint: 10.0.0
- eslint-scope@8.4.0:
+ eslint-scope@9.1.0:
dependencies:
+ '@types/esrecurse': 4.3.1
+ '@types/estree': 1.0.8
esrecurse: 4.3.0
estraverse: 5.3.0
eslint-visitor-keys@3.4.3: {}
- eslint-visitor-keys@4.2.1: {}
+ eslint-visitor-keys@5.0.0: {}
- eslint@9.35.0:
+ eslint@10.0.0:
dependencies:
- '@eslint-community/eslint-utils': 4.8.0(eslint@9.35.0)
- '@eslint-community/regexpp': 4.12.1
- '@eslint/config-array': 0.21.0
- '@eslint/config-helpers': 0.3.1
- '@eslint/core': 0.15.2
- '@eslint/eslintrc': 3.3.1
- '@eslint/js': 9.35.0
- '@eslint/plugin-kit': 0.3.5
+ '@eslint-community/eslint-utils': 4.9.1(eslint@10.0.0)
+ '@eslint-community/regexpp': 4.12.2
+ '@eslint/config-array': 0.23.1
+ '@eslint/config-helpers': 0.5.2
+ '@eslint/core': 1.1.0
+ '@eslint/plugin-kit': 0.6.0
'@humanfs/node': 0.16.7
'@humanwhocodes/module-importer': 1.0.1
'@humanwhocodes/retry': 0.4.3
'@types/estree': 1.0.8
- '@types/json-schema': 7.0.15
ajv: 6.12.6
- chalk: 4.1.2
cross-spawn: 7.0.6
- debug: 4.4.1
+ debug: 4.4.3
escape-string-regexp: 4.0.0
- eslint-scope: 8.4.0
- eslint-visitor-keys: 4.2.1
- espree: 10.4.0
- esquery: 1.6.0
+ eslint-scope: 9.1.0
+ eslint-visitor-keys: 5.0.0
+ espree: 11.1.0
+ esquery: 1.7.0
esutils: 2.0.3
fast-deep-equal: 3.1.3
file-entry-cache: 8.0.0
@@ -4382,22 +5162,21 @@ snapshots:
imurmurhash: 0.1.4
is-glob: 4.0.3
json-stable-stringify-without-jsonify: 1.0.1
- lodash.merge: 4.6.2
- minimatch: 3.1.2
+ minimatch: 10.2.2
natural-compare: 1.4.0
optionator: 0.9.4
transitivePeerDependencies:
- supports-color
- espree@10.4.0:
+ espree@11.1.0:
dependencies:
- acorn: 8.15.0
- acorn-jsx: 5.3.2(acorn@8.15.0)
- eslint-visitor-keys: 4.2.1
+ acorn: 8.16.0
+ acorn-jsx: 5.3.2(acorn@8.16.0)
+ eslint-visitor-keys: 5.0.0
esprima@4.0.1: {}
- esquery@1.6.0:
+ esquery@1.7.0:
dependencies:
estraverse: 5.3.0
@@ -4447,14 +5226,16 @@ snapshots:
exit-x@0.2.2: {}
- expect@30.1.2:
+ expect@30.2.0:
dependencies:
- '@jest/expect-utils': 30.1.2
+ '@jest/expect-utils': 30.2.0
'@jest/get-type': 30.1.0
- jest-matcher-utils: 30.1.2
- jest-message-util: 30.1.0
- jest-mock: 30.0.5
- jest-util: 30.0.5
+ jest-matcher-utils: 30.2.0
+ jest-message-util: 30.2.0
+ jest-mock: 30.2.0
+ jest-util: 30.2.0
+
+ extendable-error@0.1.7: {}
external-editor@3.1.0:
dependencies:
@@ -4480,11 +5261,13 @@ snapshots:
fast-levenshtein@2.0.6: {}
+ fast-safe-stringify@2.1.1: {}
+
fast-stable-stringify@1.0.0: {}
fastestsmallesttextencoderdecoder@1.0.22: {}
- fastq@1.19.1:
+ fastq@1.20.1:
dependencies:
reusify: 1.1.0
@@ -4492,10 +5275,27 @@ snapshots:
dependencies:
bser: 2.1.1
+ fdir@6.5.0(picomatch@4.0.3):
+ optionalDependencies:
+ picomatch: 4.0.3
+
+ figures@3.2.0:
+ dependencies:
+ escape-string-regexp: 1.0.5
+
file-entry-cache@8.0.0:
dependencies:
flat-cache: 4.0.1
+ file-type@21.3.0:
+ dependencies:
+ '@tokenizer/inflate': 0.4.1
+ strtok3: 10.3.4
+ token-types: 6.1.2
+ uint8array-extras: 1.5.0
+ transitivePeerDependencies:
+ - supports-color
+
file-uri-to-path@1.0.0: {}
fill-range@7.1.1:
@@ -4526,7 +5326,7 @@ snapshots:
cross-spawn: 7.0.6
signal-exit: 4.1.0
- form-data@4.0.4:
+ form-data@4.0.5:
dependencies:
asynckit: 0.4.0
combined-stream: 1.0.8
@@ -4534,7 +5334,17 @@ snapshots:
hasown: 2.0.2
mime-types: 2.1.35
- fs.realpath@1.0.0: {}
+ fs-extra@7.0.1:
+ dependencies:
+ graceful-fs: 4.2.11
+ jsonfile: 4.0.0
+ universalify: 0.1.2
+
+ fs-extra@8.1.0:
+ dependencies:
+ graceful-fs: 4.2.11
+ jsonfile: 4.0.0
+ universalify: 0.1.2
fsevents@2.3.3:
optional: true
@@ -4581,25 +5391,31 @@ snapshots:
dependencies:
is-glob: 4.0.3
- glob@10.4.5:
+ glob@10.5.0:
dependencies:
foreground-child: 3.3.1
jackspeak: 3.4.3
- minimatch: 9.0.5
- minipass: 7.1.2
+ minimatch: 10.2.2
+ minipass: 7.1.3
package-json-from-dist: 1.0.1
path-scurry: 1.11.1
- glob@7.2.3:
+ glob@13.0.6:
dependencies:
- fs.realpath: 1.0.0
- inflight: 1.0.6
- inherits: 2.0.4
- minimatch: 3.1.2
- once: 1.4.0
- path-is-absolute: 1.0.1
+ minimatch: 10.2.2
+ minipass: 7.1.3
+ path-scurry: 2.0.2
- globals@14.0.0: {}
+ globals@17.3.0: {}
+
+ globby@11.1.0:
+ dependencies:
+ array-union: 2.1.0
+ dir-glob: 3.0.1
+ fast-glob: 3.3.3
+ ignore: 5.3.2
+ merge2: 1.4.1
+ slash: 3.0.0
google-protobuf@3.21.4: {}
@@ -4607,8 +5423,6 @@ snapshots:
graceful-fs@4.2.11: {}
- graphemer@1.4.0: {}
-
handlebars@4.7.8:
dependencies:
minimist: 1.2.8
@@ -4632,6 +5446,8 @@ snapshots:
html-escaper@2.0.2: {}
+ human-id@4.1.3: {}
+
human-signals@2.1.0: {}
humanize-ms@1.2.1:
@@ -4644,6 +5460,10 @@ snapshots:
dependencies:
safer-buffer: 2.1.2
+ iconv-lite@0.7.2:
+ dependencies:
+ safer-buffer: 2.1.2
+
ieee754@1.2.1: {}
ignore@5.3.2: {}
@@ -4662,13 +5482,28 @@ snapshots:
imurmurhash@0.1.4: {}
- inflight@1.0.6:
- dependencies:
- once: 1.4.0
- wrappy: 1.0.2
-
inherits@2.0.4: {}
+ inquirer@8.2.7(@types/node@20.19.11):
+ dependencies:
+ '@inquirer/external-editor': 1.0.3(@types/node@20.19.11)
+ ansi-escapes: 4.3.2
+ chalk: 4.1.2
+ cli-cursor: 3.1.0
+ cli-width: 3.0.0
+ figures: 3.2.0
+ lodash: 4.17.23
+ mute-stream: 0.0.8
+ ora: 5.4.1
+ run-async: 2.4.1
+ rxjs: 7.8.2
+ string-width: 4.2.3
+ strip-ansi: 6.0.1
+ through: 2.3.8
+ wrap-ansi: 6.2.0
+ transitivePeerDependencies:
+ - '@types/node'
+
inquirer@9.3.7:
dependencies:
'@inquirer/figures': 1.0.13
@@ -4710,12 +5545,18 @@ snapshots:
is-stream@2.0.1: {}
+ is-subdir@1.2.0:
+ dependencies:
+ better-path-resolve: 1.0.0
+
is-unicode-supported@0.1.0: {}
is-unicode-supported@1.3.0: {}
is-unicode-supported@2.1.0: {}
+ is-windows@1.0.2: {}
+
isexe@2.0.0: {}
isomorphic-ws@4.0.1(ws@7.5.10(bufferutil@4.0.9)(utf-8-validate@5.0.10)):
@@ -4730,11 +5571,11 @@ snapshots:
istanbul-lib-instrument@6.0.3:
dependencies:
- '@babel/core': 7.28.4
- '@babel/parser': 7.28.4
+ '@babel/core': 7.29.0
+ '@babel/parser': 7.29.0
'@istanbuljs/schema': 0.1.3
istanbul-lib-coverage: 3.2.2
- semver: 7.7.1
+ semver: 7.7.4
transitivePeerDependencies:
- supports-color
@@ -4746,8 +5587,8 @@ snapshots:
istanbul-lib-source-maps@5.0.6:
dependencies:
- '@jridgewell/trace-mapping': 0.3.30
- debug: 4.4.1
+ '@jridgewell/trace-mapping': 0.3.31
+ debug: 4.4.3
istanbul-lib-coverage: 3.2.2
transitivePeerDependencies:
- supports-color
@@ -4757,6 +5598,8 @@ snapshots:
html-escaper: 2.0.2
istanbul-lib-report: 3.0.1
+ iterare@1.2.1: {}
+
jackspeak@3.4.3:
dependencies:
'@isaacs/cliui': 8.0.2
@@ -4781,31 +5624,31 @@ snapshots:
- bufferutil
- utf-8-validate
- jest-changed-files@30.0.5:
+ jest-changed-files@30.2.0:
dependencies:
execa: 5.1.1
- jest-util: 30.0.5
+ jest-util: 30.2.0
p-limit: 3.1.0
- jest-circus@30.1.3:
+ jest-circus@30.2.0:
dependencies:
- '@jest/environment': 30.1.2
- '@jest/expect': 30.1.2
- '@jest/test-result': 30.1.3
- '@jest/types': 30.0.5
+ '@jest/environment': 30.2.0
+ '@jest/expect': 30.2.0
+ '@jest/test-result': 30.2.0
+ '@jest/types': 30.2.0
'@types/node': 20.19.11
chalk: 4.1.2
co: 4.6.0
- dedent: 1.7.0
+ dedent: 1.7.1
is-generator-fn: 2.1.0
- jest-each: 30.1.0
- jest-matcher-utils: 30.1.2
- jest-message-util: 30.1.0
- jest-runtime: 30.1.3
- jest-snapshot: 30.1.2
- jest-util: 30.0.5
+ jest-each: 30.2.0
+ jest-matcher-utils: 30.2.0
+ jest-message-util: 30.2.0
+ jest-runtime: 30.2.0
+ jest-snapshot: 30.2.0
+ jest-util: 30.2.0
p-limit: 3.1.0
- pretty-format: 30.0.5
+ pretty-format: 30.2.0
pure-rand: 7.0.1
slash: 3.0.0
stack-utils: 2.0.6
@@ -4813,17 +5656,17 @@ snapshots:
- babel-plugin-macros
- supports-color
- jest-cli@30.1.3(@types/node@20.19.11)(ts-node@10.9.2(@types/node@20.19.11)(typescript@5.9.2)):
+ jest-cli@30.2.0(@types/node@20.19.11)(ts-node@10.9.2(@types/node@20.19.11)(typescript@5.9.2)):
dependencies:
- '@jest/core': 30.1.3(ts-node@10.9.2(@types/node@20.19.11)(typescript@5.9.2))
- '@jest/test-result': 30.1.3
- '@jest/types': 30.0.5
+ '@jest/core': 30.2.0(ts-node@10.9.2(@types/node@20.19.11)(typescript@5.9.2))
+ '@jest/test-result': 30.2.0
+ '@jest/types': 30.2.0
chalk: 4.1.2
exit-x: 0.2.2
import-local: 3.2.0
- jest-config: 30.1.3(@types/node@20.19.11)(ts-node@10.9.2(@types/node@20.19.11)(typescript@5.9.2))
- jest-util: 30.0.5
- jest-validate: 30.1.0
+ jest-config: 30.2.0(@types/node@20.19.11)(ts-node@10.9.2(@types/node@20.19.11)(typescript@5.9.2))
+ jest-util: 30.2.0
+ jest-validate: 30.2.0
yargs: 17.7.2
transitivePeerDependencies:
- '@types/node'
@@ -4832,30 +5675,30 @@ snapshots:
- supports-color
- ts-node
- jest-config@30.1.3(@types/node@20.19.11)(ts-node@10.9.2(@types/node@20.19.11)(typescript@5.9.2)):
+ jest-config@30.2.0(@types/node@20.19.11)(ts-node@10.9.2(@types/node@20.19.11)(typescript@5.9.2)):
dependencies:
- '@babel/core': 7.28.4
+ '@babel/core': 7.29.0
'@jest/get-type': 30.1.0
'@jest/pattern': 30.0.1
- '@jest/test-sequencer': 30.1.3
- '@jest/types': 30.0.5
- babel-jest: 30.1.2(@babel/core@7.28.4)
+ '@jest/test-sequencer': 30.2.0
+ '@jest/types': 30.2.0
+ babel-jest: 30.2.0(@babel/core@7.29.0)
chalk: 4.1.2
- ci-info: 4.3.0
+ ci-info: 4.4.0
deepmerge: 4.3.1
- glob: 10.4.5
+ glob: 10.5.0
graceful-fs: 4.2.11
- jest-circus: 30.1.3
- jest-docblock: 30.0.1
- jest-environment-node: 30.1.2
+ jest-circus: 30.2.0
+ jest-docblock: 30.2.0
+ jest-environment-node: 30.2.0
jest-regex-util: 30.0.1
- jest-resolve: 30.1.3
- jest-runner: 30.1.3
- jest-util: 30.0.5
- jest-validate: 30.1.0
+ jest-resolve: 30.2.0
+ jest-runner: 30.2.0
+ jest-util: 30.2.0
+ jest-validate: 30.2.0
micromatch: 4.0.8
parse-json: 5.2.0
- pretty-format: 30.0.5
+ pretty-format: 30.2.0
slash: 3.0.0
strip-json-comments: 3.1.1
optionalDependencies:
@@ -4865,227 +5708,227 @@ snapshots:
- babel-plugin-macros
- supports-color
- jest-diff@30.1.2:
+ jest-diff@30.2.0:
dependencies:
'@jest/diff-sequences': 30.0.1
'@jest/get-type': 30.1.0
chalk: 4.1.2
- pretty-format: 30.0.5
+ pretty-format: 30.2.0
- jest-docblock@30.0.1:
+ jest-docblock@30.2.0:
dependencies:
detect-newline: 3.1.0
- jest-each@30.1.0:
+ jest-each@30.2.0:
dependencies:
'@jest/get-type': 30.1.0
- '@jest/types': 30.0.5
+ '@jest/types': 30.2.0
chalk: 4.1.2
- jest-util: 30.0.5
- pretty-format: 30.0.5
+ jest-util: 30.2.0
+ pretty-format: 30.2.0
- jest-environment-node@30.1.2:
+ jest-environment-node@30.2.0:
dependencies:
- '@jest/environment': 30.1.2
- '@jest/fake-timers': 30.1.2
- '@jest/types': 30.0.5
+ '@jest/environment': 30.2.0
+ '@jest/fake-timers': 30.2.0
+ '@jest/types': 30.2.0
'@types/node': 20.19.11
- jest-mock: 30.0.5
- jest-util: 30.0.5
- jest-validate: 30.1.0
+ jest-mock: 30.2.0
+ jest-util: 30.2.0
+ jest-validate: 30.2.0
- jest-haste-map@30.1.0:
+ jest-haste-map@30.2.0:
dependencies:
- '@jest/types': 30.0.5
+ '@jest/types': 30.2.0
'@types/node': 20.19.11
anymatch: 3.1.3
fb-watchman: 2.0.2
graceful-fs: 4.2.11
jest-regex-util: 30.0.1
- jest-util: 30.0.5
- jest-worker: 30.1.0
+ jest-util: 30.2.0
+ jest-worker: 30.2.0
micromatch: 4.0.8
walker: 1.0.8
optionalDependencies:
fsevents: 2.3.3
- jest-leak-detector@30.1.0:
+ jest-leak-detector@30.2.0:
dependencies:
'@jest/get-type': 30.1.0
- pretty-format: 30.0.5
+ pretty-format: 30.2.0
- jest-matcher-utils@30.1.2:
+ jest-matcher-utils@30.2.0:
dependencies:
'@jest/get-type': 30.1.0
chalk: 4.1.2
- jest-diff: 30.1.2
- pretty-format: 30.0.5
+ jest-diff: 30.2.0
+ pretty-format: 30.2.0
- jest-message-util@30.1.0:
+ jest-message-util@30.2.0:
dependencies:
- '@babel/code-frame': 7.27.1
- '@jest/types': 30.0.5
+ '@babel/code-frame': 7.29.0
+ '@jest/types': 30.2.0
'@types/stack-utils': 2.0.3
chalk: 4.1.2
graceful-fs: 4.2.11
micromatch: 4.0.8
- pretty-format: 30.0.5
+ pretty-format: 30.2.0
slash: 3.0.0
stack-utils: 2.0.6
- jest-mock@30.0.5:
+ jest-mock@30.2.0:
dependencies:
- '@jest/types': 30.0.5
+ '@jest/types': 30.2.0
'@types/node': 20.19.11
- jest-util: 30.0.5
+ jest-util: 30.2.0
- jest-pnp-resolver@1.2.3(jest-resolve@30.1.3):
+ jest-pnp-resolver@1.2.3(jest-resolve@30.2.0):
optionalDependencies:
- jest-resolve: 30.1.3
+ jest-resolve: 30.2.0
jest-regex-util@30.0.1: {}
- jest-resolve-dependencies@30.1.3:
+ jest-resolve-dependencies@30.2.0:
dependencies:
jest-regex-util: 30.0.1
- jest-snapshot: 30.1.2
+ jest-snapshot: 30.2.0
transitivePeerDependencies:
- supports-color
- jest-resolve@30.1.3:
+ jest-resolve@30.2.0:
dependencies:
chalk: 4.1.2
graceful-fs: 4.2.11
- jest-haste-map: 30.1.0
- jest-pnp-resolver: 1.2.3(jest-resolve@30.1.3)
- jest-util: 30.0.5
- jest-validate: 30.1.0
+ jest-haste-map: 30.2.0
+ jest-pnp-resolver: 1.2.3(jest-resolve@30.2.0)
+ jest-util: 30.2.0
+ jest-validate: 30.2.0
slash: 3.0.0
unrs-resolver: 1.11.1
- jest-runner@30.1.3:
+ jest-runner@30.2.0:
dependencies:
- '@jest/console': 30.1.2
- '@jest/environment': 30.1.2
- '@jest/test-result': 30.1.3
- '@jest/transform': 30.1.2
- '@jest/types': 30.0.5
+ '@jest/console': 30.2.0
+ '@jest/environment': 30.2.0
+ '@jest/test-result': 30.2.0
+ '@jest/transform': 30.2.0
+ '@jest/types': 30.2.0
'@types/node': 20.19.11
chalk: 4.1.2
emittery: 0.13.1
exit-x: 0.2.2
graceful-fs: 4.2.11
- jest-docblock: 30.0.1
- jest-environment-node: 30.1.2
- jest-haste-map: 30.1.0
- jest-leak-detector: 30.1.0
- jest-message-util: 30.1.0
- jest-resolve: 30.1.3
- jest-runtime: 30.1.3
- jest-util: 30.0.5
- jest-watcher: 30.1.3
- jest-worker: 30.1.0
+ jest-docblock: 30.2.0
+ jest-environment-node: 30.2.0
+ jest-haste-map: 30.2.0
+ jest-leak-detector: 30.2.0
+ jest-message-util: 30.2.0
+ jest-resolve: 30.2.0
+ jest-runtime: 30.2.0
+ jest-util: 30.2.0
+ jest-watcher: 30.2.0
+ jest-worker: 30.2.0
p-limit: 3.1.0
source-map-support: 0.5.13
transitivePeerDependencies:
- supports-color
- jest-runtime@30.1.3:
+ jest-runtime@30.2.0:
dependencies:
- '@jest/environment': 30.1.2
- '@jest/fake-timers': 30.1.2
- '@jest/globals': 30.1.2
+ '@jest/environment': 30.2.0
+ '@jest/fake-timers': 30.2.0
+ '@jest/globals': 30.2.0
'@jest/source-map': 30.0.1
- '@jest/test-result': 30.1.3
- '@jest/transform': 30.1.2
- '@jest/types': 30.0.5
+ '@jest/test-result': 30.2.0
+ '@jest/transform': 30.2.0
+ '@jest/types': 30.2.0
'@types/node': 20.19.11
chalk: 4.1.2
- cjs-module-lexer: 2.1.0
- collect-v8-coverage: 1.0.2
- glob: 10.4.5
+ cjs-module-lexer: 2.2.0
+ collect-v8-coverage: 1.0.3
+ glob: 10.5.0
graceful-fs: 4.2.11
- jest-haste-map: 30.1.0
- jest-message-util: 30.1.0
- jest-mock: 30.0.5
+ jest-haste-map: 30.2.0
+ jest-message-util: 30.2.0
+ jest-mock: 30.2.0
jest-regex-util: 30.0.1
- jest-resolve: 30.1.3
- jest-snapshot: 30.1.2
- jest-util: 30.0.5
+ jest-resolve: 30.2.0
+ jest-snapshot: 30.2.0
+ jest-util: 30.2.0
slash: 3.0.0
strip-bom: 4.0.0
transitivePeerDependencies:
- supports-color
- jest-snapshot@30.1.2:
+ jest-snapshot@30.2.0:
dependencies:
- '@babel/core': 7.28.4
- '@babel/generator': 7.28.3
- '@babel/plugin-syntax-jsx': 7.27.1(@babel/core@7.28.4)
- '@babel/plugin-syntax-typescript': 7.27.1(@babel/core@7.28.4)
- '@babel/types': 7.28.4
- '@jest/expect-utils': 30.1.2
+ '@babel/core': 7.29.0
+ '@babel/generator': 7.29.1
+ '@babel/plugin-syntax-jsx': 7.28.6(@babel/core@7.29.0)
+ '@babel/plugin-syntax-typescript': 7.28.6(@babel/core@7.29.0)
+ '@babel/types': 7.29.0
+ '@jest/expect-utils': 30.2.0
'@jest/get-type': 30.1.0
- '@jest/snapshot-utils': 30.1.2
- '@jest/transform': 30.1.2
- '@jest/types': 30.0.5
- babel-preset-current-node-syntax: 1.2.0(@babel/core@7.28.4)
+ '@jest/snapshot-utils': 30.2.0
+ '@jest/transform': 30.2.0
+ '@jest/types': 30.2.0
+ babel-preset-current-node-syntax: 1.2.0(@babel/core@7.29.0)
chalk: 4.1.2
- expect: 30.1.2
+ expect: 30.2.0
graceful-fs: 4.2.11
- jest-diff: 30.1.2
- jest-matcher-utils: 30.1.2
- jest-message-util: 30.1.0
- jest-util: 30.0.5
- pretty-format: 30.0.5
- semver: 7.7.2
- synckit: 0.11.11
+ jest-diff: 30.2.0
+ jest-matcher-utils: 30.2.0
+ jest-message-util: 30.2.0
+ jest-util: 30.2.0
+ pretty-format: 30.2.0
+ semver: 7.7.4
+ synckit: 0.11.12
transitivePeerDependencies:
- supports-color
- jest-util@30.0.5:
+ jest-util@30.2.0:
dependencies:
- '@jest/types': 30.0.5
+ '@jest/types': 30.2.0
'@types/node': 20.19.11
chalk: 4.1.2
- ci-info: 4.3.0
+ ci-info: 4.4.0
graceful-fs: 4.2.11
picomatch: 4.0.3
- jest-validate@30.1.0:
+ jest-validate@30.2.0:
dependencies:
'@jest/get-type': 30.1.0
- '@jest/types': 30.0.5
+ '@jest/types': 30.2.0
camelcase: 6.3.0
chalk: 4.1.2
leven: 3.1.0
- pretty-format: 30.0.5
+ pretty-format: 30.2.0
- jest-watcher@30.1.3:
+ jest-watcher@30.2.0:
dependencies:
- '@jest/test-result': 30.1.3
- '@jest/types': 30.0.5
+ '@jest/test-result': 30.2.0
+ '@jest/types': 30.2.0
'@types/node': 20.19.11
ansi-escapes: 4.3.2
chalk: 4.1.2
emittery: 0.13.1
- jest-util: 30.0.5
+ jest-util: 30.2.0
string-length: 4.0.2
- jest-worker@30.1.0:
+ jest-worker@30.2.0:
dependencies:
'@types/node': 20.19.11
'@ungap/structured-clone': 1.3.0
- jest-util: 30.0.5
+ jest-util: 30.2.0
merge-stream: 2.0.0
supports-color: 8.1.1
- jest@30.1.3(@types/node@20.19.11)(ts-node@10.9.2(@types/node@20.19.11)(typescript@5.9.2)):
+ jest@30.2.0(@types/node@20.19.11)(ts-node@10.9.2(@types/node@20.19.11)(typescript@5.9.2)):
dependencies:
- '@jest/core': 30.1.3(ts-node@10.9.2(@types/node@20.19.11)(typescript@5.9.2))
- '@jest/types': 30.0.5
+ '@jest/core': 30.2.0(ts-node@10.9.2(@types/node@20.19.11)(typescript@5.9.2))
+ '@jest/types': 30.2.0
import-local: 3.2.0
- jest-cli: 30.1.3(@types/node@20.19.11)(ts-node@10.9.2(@types/node@20.19.11)(typescript@5.9.2))
+ jest-cli: 30.2.0(@types/node@20.19.11)(ts-node@10.9.2(@types/node@20.19.11)(typescript@5.9.2))
transitivePeerDependencies:
- '@types/node'
- babel-plugin-macros
@@ -5095,12 +5938,12 @@ snapshots:
js-tokens@4.0.0: {}
- js-yaml@3.14.1:
+ js-yaml@3.14.2:
dependencies:
argparse: 1.0.10
esprima: 4.0.1
- js-yaml@4.1.0:
+ js-yaml@4.1.1:
dependencies:
argparse: 2.0.1
@@ -5118,6 +5961,10 @@ snapshots:
json5@2.2.3: {}
+ jsonfile@4.0.0:
+ optionalDependencies:
+ graceful-fs: 4.2.11
+
keyv@4.5.4:
dependencies:
json-buffer: 3.0.1
@@ -5133,6 +5980,10 @@ snapshots:
lines-and-columns@1.2.4: {}
+ linkify-it@5.0.0:
+ dependencies:
+ uc.micro: 2.1.0
+
lint-staged@16.1.6:
dependencies:
chalk: 5.6.0
@@ -5157,6 +6008,8 @@ snapshots:
rfdc: 1.4.1
wrap-ansi: 9.0.2
+ load-esm@1.0.3: {}
+
locate-path@5.0.0:
dependencies:
p-locate: 4.1.0
@@ -5167,7 +6020,9 @@ snapshots:
lodash.memoize@4.1.2: {}
- lodash.merge@4.6.2: {}
+ lodash.startcase@4.4.0: {}
+
+ lodash@4.17.23: {}
log-symbols@4.1.0:
dependencies:
@@ -5189,13 +6044,17 @@ snapshots:
lru-cache@10.4.3: {}
+ lru-cache@11.2.6: {}
+
lru-cache@5.1.1:
dependencies:
yallist: 3.1.1
+ lunr@2.3.9: {}
+
make-dir@4.0.0:
dependencies:
- semver: 7.7.1
+ semver: 7.7.4
make-error@1.3.6: {}
@@ -5203,8 +6062,19 @@ snapshots:
dependencies:
tmpl: 1.0.5
+ markdown-it@14.1.1:
+ dependencies:
+ argparse: 2.0.1
+ entities: 4.5.0
+ linkify-it: 5.0.0
+ mdurl: 2.0.0
+ punycode.js: 2.3.1
+ uc.micro: 2.1.0
+
math-intrinsics@1.1.0: {}
+ mdurl@2.0.0: {}
+
merge-stream@2.0.0: {}
merge2@1.4.1: {}
@@ -5224,30 +6094,44 @@ snapshots:
mimic-function@5.0.1: {}
- minimatch@3.1.2:
+ minimatch@10.2.2:
dependencies:
- brace-expansion: 1.1.12
-
- minimatch@9.0.5:
- dependencies:
- brace-expansion: 2.0.2
+ brace-expansion: 5.0.2
minimist@1.2.8: {}
- minipass@7.1.2: {}
+ minipass@7.1.3: {}
+
+ mri@1.2.0: {}
ms@2.1.3: {}
+ mute-stream@0.0.8: {}
+
mute-stream@1.0.0: {}
nano-spawn@1.0.3: {}
- napi-postinstall@0.3.3: {}
+ napi-postinstall@0.3.4: {}
natural-compare@1.4.0: {}
neo-async@2.6.2: {}
+ nest-commander@3.20.1(@nestjs/common@11.1.14(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.14(@nestjs/common@11.1.14(reflect-metadata@0.2.2)(rxjs@7.8.2))(reflect-metadata@0.2.2)(rxjs@7.8.2))(@types/inquirer@9.0.9)(@types/node@20.19.11)(typescript@5.9.2):
+ dependencies:
+ '@fig/complete-commander': 3.2.0(commander@11.1.0)
+ '@golevelup/nestjs-discovery': 5.0.0(@nestjs/common@11.1.14(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.14(@nestjs/common@11.1.14(reflect-metadata@0.2.2)(rxjs@7.8.2))(reflect-metadata@0.2.2)(rxjs@7.8.2))
+ '@nestjs/common': 11.1.14(reflect-metadata@0.2.2)(rxjs@7.8.2)
+ '@nestjs/core': 11.1.14(@nestjs/common@11.1.14(reflect-metadata@0.2.2)(rxjs@7.8.2))(reflect-metadata@0.2.2)(rxjs@7.8.2)
+ '@types/inquirer': 9.0.9
+ commander: 11.1.0
+ cosmiconfig: 8.3.6(typescript@5.9.2)
+ inquirer: 8.2.7(@types/node@20.19.11)
+ transitivePeerDependencies:
+ - '@types/node'
+ - typescript
+
node-fetch@2.7.0:
dependencies:
whatwg-url: 5.0.0
@@ -5257,7 +6141,7 @@ snapshots:
node-int64@0.4.0: {}
- node-releases@2.0.20: {}
+ node-releases@2.0.27: {}
normalize-path@3.0.0: {}
@@ -5265,10 +6149,6 @@ snapshots:
dependencies:
path-key: 3.1.1
- once@1.4.0:
- dependencies:
- wrappy: 1.0.2
-
onetime@5.1.2:
dependencies:
mimic-fn: 2.1.0
@@ -5312,21 +6192,27 @@ snapshots:
os-tmpdir@1.0.2: {}
- ox@0.9.6(typescript@5.9.2):
+ outdent@0.5.0: {}
+
+ ox@0.9.6(typescript@5.9.2)(zod@4.3.6):
dependencies:
- '@adraffy/ens-normalize': 1.11.0
+ '@adraffy/ens-normalize': 1.11.1
'@noble/ciphers': 1.3.0
'@noble/curves': 1.9.1
'@noble/hashes': 1.8.0
'@scure/bip32': 1.7.0
'@scure/bip39': 1.6.0
- abitype: 1.1.0(typescript@5.9.2)
+ abitype: 1.1.0(typescript@5.9.2)(zod@4.3.6)
eventemitter3: 5.0.1
optionalDependencies:
typescript: 5.9.2
transitivePeerDependencies:
- zod
+ p-filter@2.1.0:
+ dependencies:
+ p-map: 2.1.0
+
p-limit@2.3.0:
dependencies:
p-try: 2.2.0
@@ -5343,10 +6229,16 @@ snapshots:
dependencies:
p-limit: 3.1.0
+ p-map@2.1.0: {}
+
p-try@2.2.0: {}
package-json-from-dist@1.0.1: {}
+ package-manager-detector@0.2.11:
+ dependencies:
+ quansync: 0.2.11
+
pako@2.1.0: {}
parent-module@1.0.1:
@@ -5355,21 +6247,28 @@ snapshots:
parse-json@5.2.0:
dependencies:
- '@babel/code-frame': 7.27.1
- error-ex: 1.3.2
+ '@babel/code-frame': 7.29.0
+ error-ex: 1.3.4
json-parse-even-better-errors: 2.3.1
lines-and-columns: 1.2.4
path-exists@4.0.0: {}
- path-is-absolute@1.0.1: {}
-
path-key@3.1.1: {}
path-scurry@1.11.1:
dependencies:
lru-cache: 10.4.3
- minipass: 7.1.2
+ minipass: 7.1.3
+
+ path-scurry@2.0.2:
+ dependencies:
+ lru-cache: 11.2.6
+ minipass: 7.1.3
+
+ path-to-regexp@8.3.0: {}
+
+ path-type@4.0.0: {}
picocolors@1.1.1: {}
@@ -5379,6 +6278,8 @@ snapshots:
pidtree@0.6.0: {}
+ pify@4.0.1: {}
+
pirates@4.0.7: {}
pkg-dir@4.2.0:
@@ -5391,9 +6292,11 @@ snapshots:
dependencies:
fast-diff: 1.3.0
+ prettier@2.8.8: {}
+
prettier@3.6.2: {}
- pretty-format@30.0.5:
+ pretty-format@30.2.0:
dependencies:
'@jest/schemas': 30.0.5
ansi-styles: 5.2.0
@@ -5401,20 +6304,33 @@ snapshots:
proxy-from-env@1.1.0: {}
+ punycode.js@2.3.1: {}
+
punycode@2.3.1: {}
pure-rand@7.0.1: {}
+ quansync@0.2.11: {}
+
queue-microtask@1.2.3: {}
react-is@18.3.1: {}
+ read-yaml-file@1.1.0:
+ dependencies:
+ graceful-fs: 4.2.11
+ js-yaml: 3.14.2
+ pify: 4.0.1
+ strip-bom: 3.0.0
+
readable-stream@3.6.2:
dependencies:
inherits: 2.0.4
string_decoder: 1.3.0
util-deprecate: 1.0.2
+ reflect-metadata@0.2.2: {}
+
regenerator-runtime@0.14.1: {}
require-directory@2.1.1: {}
@@ -5456,6 +6372,8 @@ snapshots:
bufferutil: 4.0.9
utf-8-validate: 5.0.10
+ run-async@2.4.1: {}
+
run-async@3.0.0: {}
run-parallel@1.2.0:
@@ -5474,7 +6392,7 @@ snapshots:
semver@7.7.1: {}
- semver@7.7.2: {}
+ semver@7.7.4: {}
shebang-command@2.0.0:
dependencies:
@@ -5505,6 +6423,11 @@ snapshots:
source-map@0.6.1: {}
+ spawndamnit@3.0.1:
+ dependencies:
+ cross-spawn: 7.0.6
+ signal-exit: 4.1.0
+
sprintf-js@1.0.3: {}
stack-utils@2.0.6:
@@ -5536,7 +6459,7 @@ snapshots:
dependencies:
eastasianwidth: 0.2.0
emoji-regex: 9.2.2
- strip-ansi: 7.1.0
+ strip-ansi: 7.1.2
string-width@7.2.0:
dependencies:
@@ -5556,6 +6479,10 @@ snapshots:
dependencies:
ansi-regex: 6.2.0
+ strip-ansi@7.1.2:
+ dependencies:
+ ansi-regex: 6.2.2
+
strip-bom@3.0.0: {}
strip-bom@4.0.0: {}
@@ -5564,6 +6491,10 @@ snapshots:
strip-json-comments@3.1.1: {}
+ strtok3@10.3.4:
+ dependencies:
+ '@tokenizer/token': 0.3.0
+
superstruct@0.15.5: {}
superstruct@2.0.2: {}
@@ -5580,14 +6511,27 @@ snapshots:
dependencies:
'@pkgr/core': 0.2.9
- test-exclude@6.0.0:
+ synckit@0.11.12:
+ dependencies:
+ '@pkgr/core': 0.2.9
+
+ term-size@2.2.1: {}
+
+ test-exclude@8.0.0:
dependencies:
'@istanbuljs/schema': 0.1.3
- glob: 7.2.3
- minimatch: 3.1.2
+ glob: 13.0.6
+ minimatch: 10.2.2
text-encoding-utf-8@1.0.2: {}
+ through@2.3.8: {}
+
+ tinyglobby@0.2.15:
+ dependencies:
+ fdir: 6.5.0(picomatch@4.0.3)
+ picomatch: 4.0.3
+
tmp@0.0.33:
dependencies:
os-tmpdir: 1.0.2
@@ -5598,49 +6542,55 @@ snapshots:
dependencies:
is-number: 7.0.0
+ token-types@6.1.2:
+ dependencies:
+ '@borewit/text-codec': 0.2.1
+ '@tokenizer/token': 0.3.0
+ ieee754: 1.2.1
+
toml@3.0.0: {}
tr46@0.0.3: {}
- tronweb@6.0.4(bufferutil@4.0.9)(utf-8-validate@5.0.10):
+ tronweb@6.2.0(bufferutil@4.0.9)(utf-8-validate@5.0.10):
dependencies:
'@babel/runtime': 7.26.10
- axios: 1.11.0
+ axios: 1.13.5
bignumber.js: 9.1.2
ethereum-cryptography: 2.2.1
ethers: 6.13.5(bufferutil@4.0.9)(utf-8-validate@5.0.10)
eventemitter3: 5.0.1
google-protobuf: 3.21.4
semver: 7.7.1
- validator: 13.12.0
+ validator: 13.15.23
transitivePeerDependencies:
- bufferutil
- debug
- utf-8-validate
- ts-api-utils@2.1.0(typescript@5.9.2):
+ ts-api-utils@2.4.0(typescript@5.9.2):
dependencies:
typescript: 5.9.2
- ts-jest@29.4.1(@babel/core@7.28.4)(@jest/transform@30.1.2)(@jest/types@30.0.5)(babel-jest@30.1.2(@babel/core@7.28.4))(jest-util@30.0.5)(jest@30.1.3(@types/node@20.19.11)(ts-node@10.9.2(@types/node@20.19.11)(typescript@5.9.2)))(typescript@5.9.2):
+ ts-jest@29.4.6(@babel/core@7.29.0)(@jest/transform@30.2.0)(@jest/types@30.2.0)(babel-jest@30.2.0(@babel/core@7.29.0))(jest-util@30.2.0)(jest@30.2.0(@types/node@20.19.11)(ts-node@10.9.2(@types/node@20.19.11)(typescript@5.9.2)))(typescript@5.9.2):
dependencies:
bs-logger: 0.2.6
fast-json-stable-stringify: 2.1.0
handlebars: 4.7.8
- jest: 30.1.3(@types/node@20.19.11)(ts-node@10.9.2(@types/node@20.19.11)(typescript@5.9.2))
+ jest: 30.2.0(@types/node@20.19.11)(ts-node@10.9.2(@types/node@20.19.11)(typescript@5.9.2))
json5: 2.2.3
lodash.memoize: 4.1.2
make-error: 1.3.6
- semver: 7.7.2
+ semver: 7.7.4
type-fest: 4.41.0
typescript: 5.9.2
yargs-parser: 21.1.1
optionalDependencies:
- '@babel/core': 7.28.4
- '@jest/transform': 30.1.2
- '@jest/types': 30.0.5
- babel-jest: 30.1.2(@babel/core@7.28.4)
- jest-util: 30.0.5
+ '@babel/core': 7.29.0
+ '@jest/transform': 30.2.0
+ '@jest/types': 30.2.0
+ babel-jest: 30.2.0(@babel/core@7.29.0)
+ jest-util: 30.2.0
ts-node@10.9.2(@types/node@20.19.11)(typescript@5.9.2):
dependencies:
@@ -5687,18 +6637,37 @@ snapshots:
type-fest@4.41.0: {}
+ typedoc@0.28.17(typescript@5.9.2):
+ dependencies:
+ '@gerrit0/mini-shiki': 3.22.0
+ lunr: 2.3.9
+ markdown-it: 14.1.1
+ minimatch: 10.2.2
+ typescript: 5.9.2
+ yaml: 2.8.1
+
typescript@5.9.2: {}
+ uc.micro@2.1.0: {}
+
uglify-js@3.19.3:
optional: true
+ uid@2.0.2:
+ dependencies:
+ '@lukeed/csprng': 1.1.0
+
+ uint8array-extras@1.5.0: {}
+
undici-types@6.19.8: {}
undici-types@6.21.0: {}
+ universalify@0.1.2: {}
+
unrs-resolver@1.11.1:
dependencies:
- napi-postinstall: 0.3.3
+ napi-postinstall: 0.3.4
optionalDependencies:
'@unrs/resolver-binding-android-arm-eabi': 1.11.1
'@unrs/resolver-binding-android-arm64': 1.11.1
@@ -5720,9 +6689,9 @@ snapshots:
'@unrs/resolver-binding-win32-ia32-msvc': 1.11.1
'@unrs/resolver-binding-win32-x64-msvc': 1.11.1
- update-browserslist-db@1.1.3(browserslist@4.25.4):
+ update-browserslist-db@1.2.3(browserslist@4.28.1):
dependencies:
- browserslist: 4.25.4
+ browserslist: 4.28.1
escalade: 3.2.0
picocolors: 1.1.1
@@ -5743,21 +6712,21 @@ snapshots:
v8-to-istanbul@9.3.0:
dependencies:
- '@jridgewell/trace-mapping': 0.3.30
+ '@jridgewell/trace-mapping': 0.3.31
'@types/istanbul-lib-coverage': 2.0.6
convert-source-map: 2.0.0
- validator@13.12.0: {}
+ validator@13.15.23: {}
- viem@2.40.4(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10):
+ viem@2.40.4(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@4.3.6):
dependencies:
'@noble/curves': 1.9.1
'@noble/hashes': 1.8.0
'@scure/bip32': 1.7.0
'@scure/bip39': 1.6.0
- abitype: 1.1.0(typescript@5.9.2)
+ abitype: 1.1.0(typescript@5.9.2)(zod@4.3.6)
isows: 1.0.7(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10))
- ox: 0.9.6(typescript@5.9.2)
+ ox: 0.9.6(typescript@5.9.2)(zod@4.3.6)
ws: 8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10)
optionalDependencies:
typescript: 5.9.2
@@ -5803,9 +6772,9 @@ snapshots:
wrap-ansi@8.1.0:
dependencies:
- ansi-styles: 6.2.1
+ ansi-styles: 6.2.3
string-width: 5.1.2
- strip-ansi: 7.1.0
+ strip-ansi: 7.1.2
wrap-ansi@9.0.2:
dependencies:
@@ -5813,8 +6782,6 @@ snapshots:
string-width: 7.2.0
strip-ansi: 7.1.0
- wrappy@1.0.2: {}
-
write-file-atomic@5.0.1:
dependencies:
imurmurhash: 0.1.4
@@ -5858,3 +6825,5 @@ snapshots:
yocto-queue@0.1.0: {}
yoctocolors-cjs@2.1.3: {}
+
+ zod@4.3.6: {}
diff --git a/src/app.module.ts b/src/app.module.ts
new file mode 100644
index 0000000..e637196
--- /dev/null
+++ b/src/app.module.ts
@@ -0,0 +1,13 @@
+import { Module } from '@nestjs/common';
+
+import { BlockchainModule } from './blockchain/blockchain.module';
+import { CliModule } from './cli/cli.module';
+import { ConfigModule } from './config/config.module';
+import { IntentModule } from './intent/intent.module';
+import { QuoteModule } from './quote/quote.module';
+import { StatusModule } from './status/status.module';
+
+@Module({
+ imports: [ConfigModule, BlockchainModule, IntentModule, QuoteModule, StatusModule, CliModule],
+})
+export class AppModule {}
diff --git a/src/blockchain/address-normalizer.service.ts b/src/blockchain/address-normalizer.service.ts
new file mode 100644
index 0000000..2f80283
--- /dev/null
+++ b/src/blockchain/address-normalizer.service.ts
@@ -0,0 +1,37 @@
+import { Injectable } from '@nestjs/common';
+
+import {
+ BlockchainAddress,
+ ChainType,
+ EvmAddress,
+ SvmAddress,
+ TronAddress,
+ UniversalAddress,
+} from '@/shared/types';
+
+import { ChainRegistryService } from './chain-registry.service';
+
+@Injectable()
+export class AddressNormalizerService {
+ constructor(private readonly registry: ChainRegistryService) {}
+
+ normalize(address: BlockchainAddress, chainType: ChainType): UniversalAddress {
+ return this.registry.get(chainType).normalize(address as string);
+ }
+
+ denormalize(address: UniversalAddress, chainType: ChainType): BlockchainAddress {
+ return this.registry.get(chainType).denormalize(address);
+ }
+
+ denormalizeToEvm(address: UniversalAddress): EvmAddress {
+ return this.registry.get(ChainType.EVM).denormalize(address) as EvmAddress;
+ }
+
+ denormalizeToTvm(address: UniversalAddress): TronAddress {
+ return this.registry.get(ChainType.TVM).denormalize(address) as TronAddress;
+ }
+
+ denormalizeToSvm(address: UniversalAddress): SvmAddress {
+ return this.registry.get(ChainType.SVM).denormalize(address) as SvmAddress;
+ }
+}
diff --git a/src/blockchain/base-publisher.ts b/src/blockchain/base-publisher.ts
deleted file mode 100644
index 7ae0899..0000000
--- a/src/blockchain/base-publisher.ts
+++ /dev/null
@@ -1,117 +0,0 @@
-/**
- * Base Publisher Abstract Class
- *
- * Defines the common interface and structure for all blockchain publishers
- * in the Routes CLI system. Publishers are responsible for taking cross-chain
- * intents and publishing them to specific blockchain networks.
- *
- * Each concrete implementation (EVMPublisher, TVMPublisher, SVMPublisher) must
- * handle the blockchain-specific details while maintaining this common interface.
- *
- * @example
- * ```typescript
- * class CustomPublisher extends BasePublisher {
- * async publish(intent: Intent, privateKey: string): Promise {
- * // Implementation specific to your blockchain
- * return { success: true, transactionHash: '0x...' };
- * }
- *
- * // ... implement other abstract methods
- * }
- * ```
- */
-
-import { UniversalAddress } from '@/core/types/universal-address';
-
-import { Intent } from '../core/interfaces/intent';
-
-/**
- * Result object returned by publisher operations.
- *
- * Contains the outcome of intent publishing with optional transaction details
- * and error information for debugging and user feedback.
- */
-export interface PublishResult {
- /** Whether the publish operation was successful */
- success: boolean;
- /** Transaction hash on the blockchain (if successful) */
- transactionHash?: string;
- /** Computed intent hash for tracking purposes */
- intentHash?: string;
- /** Error message if the operation failed */
- error?: string;
- /** Vault or contract address created (if applicable) */
- vaultAddress?: string;
- /** Decoded program data (SVM only) */
- decodedData?: any;
-}
-
-/**
- * Abstract base class for blockchain publishers.
- *
- * Provides common structure and interface for publishing cross-chain intents
- * to different blockchain networks. Each implementation handles the specific
- * blockchain protocol details while maintaining consistent behavior.
- */
-export abstract class BasePublisher {
- protected rpcUrl: string;
-
- /**
- * Creates a new publisher instance.
- *
- * @param rpcUrl - RPC endpoint URL for blockchain communication
- */
- constructor(rpcUrl: string) {
- this.rpcUrl = rpcUrl;
- }
-
- /**
- * Publishes an intent to the blockchain.
- *
- * Takes a cross-chain intent with UniversalAddresses and publishes it to the
- * specific blockchain network. Implementations must handle address denormalization,
- * transaction signing, and error handling appropriate for their blockchain type.
- *
- * @param privateKey - Private key for transaction signing (format depends on blockchain)
- * @returns Promise resolving to PublishResult with transaction details or error info
- * @throws {Error} When publishing fails due to network, validation, or other issues
- *
- * @example
- * ```typescript
- * const result = await publisher.publish(intent, privateKey);
- * if (result.success) {
- * console.log(`Published: ${result.transactionHash}`);
- * } else {
- * console.error(`Failed: ${result.error}`);
- * }
- * ```
- */
- abstract publish(
- source: bigint,
- destination: bigint,
- reward: Intent['reward'],
- encodedRoute: string,
- privateKey: string,
- portalAddress?: UniversalAddress,
- proverAddress?: UniversalAddress
- ): Promise;
-
- /**
- * Gets the native token balance of an address.
- *
- * Retrieves the balance of the blockchain's native token (ETH, TRX, SOL) for
- * a given address. Used for validation and user information display.
- *
- * @param address - Address to check balance for (in chain-native format)
- * @param chainId - Optional chain ID for chain-specific balance checks
- * @returns Promise resolving to balance in smallest unit (wei, sun, lamports)
- * @throws {Error} When balance query fails
- *
- * @example
- * ```typescript
- * const balance = await publisher.getBalance('0x742d35Cc...');
- * console.log(`Balance: ${balance} wei`);
- * ```
- */
- abstract getBalance(address: string, chainId?: bigint): Promise;
-}
diff --git a/src/blockchain/base.publisher.ts b/src/blockchain/base.publisher.ts
new file mode 100644
index 0000000..95ec825
--- /dev/null
+++ b/src/blockchain/base.publisher.ts
@@ -0,0 +1,82 @@
+import { Injectable } from '@nestjs/common';
+
+import { RoutesCliError } from '@/shared/errors';
+import { KeyHandle } from '@/shared/security';
+import { ChainConfig, Intent, UniversalAddress } from '@/shared/types';
+import { logger } from '@/utils/logger';
+
+import { ChainRegistryService } from './chain-registry.service';
+
+export interface IntentStatus {
+ fulfilled: boolean;
+ solver?: string;
+ fulfillmentTxHash?: string;
+ blockNumber?: bigint;
+ timestamp?: number;
+}
+
+export interface PublishResult {
+ success: boolean;
+ transactionHash?: string;
+ intentHash?: string;
+ error?: string;
+ vaultAddress?: string;
+ decodedData?: unknown;
+}
+
+export interface ValidationResult {
+ valid: boolean;
+ errors: string[];
+}
+
+@Injectable()
+export abstract class BasePublisher {
+ constructor(
+ protected readonly rpcUrl: string,
+ protected readonly registry: ChainRegistryService
+ ) {}
+
+ abstract publish(
+ source: bigint,
+ destination: bigint,
+ reward: Intent['reward'],
+ encodedRoute: string,
+ keyHandle: KeyHandle,
+ portalAddress?: UniversalAddress,
+ proverAddress?: UniversalAddress
+ ): Promise;
+
+ abstract getBalance(address: string, chainId?: bigint): Promise;
+
+ abstract validate(
+ reward: Intent['reward'],
+ senderAddress: string,
+ chainId: bigint
+ ): Promise;
+
+ abstract getStatus(
+ intentHash: string,
+ chain: ChainConfig,
+ portalAddress?: UniversalAddress
+ ): Promise;
+
+ protected handleError(error: unknown): PublishResult {
+ const message = error instanceof Error ? error.message : String(error);
+ logger.stopSpinner();
+ return { success: false, error: message };
+ }
+
+ protected async runSafely(fn: () => Promise): Promise {
+ try {
+ return await fn();
+ } catch (error: unknown) {
+ return this.handleError(error);
+ }
+ }
+
+ protected runPreflightChecks(sourceChainId: bigint): void {
+ if (!this.registry.isRegistered(sourceChainId)) {
+ throw RoutesCliError.unsupportedChain(sourceChainId);
+ }
+ }
+}
diff --git a/src/blockchain/blockchain.module.ts b/src/blockchain/blockchain.module.ts
new file mode 100644
index 0000000..090bf2e
--- /dev/null
+++ b/src/blockchain/blockchain.module.ts
@@ -0,0 +1,35 @@
+import { Global, Module } from '@nestjs/common';
+
+import { ConfigModule } from '@/config/config.module';
+
+import { IntentConverterService } from './encoding/intent-converter.service';
+import { PortalEncoderService } from './encoding/portal-encoder.service';
+import { AddressNormalizerService } from './address-normalizer.service';
+import { ChainRegistryService } from './chain-registry.service';
+import { ChainsService } from './chains.service';
+import { PublisherFactory } from './publisher-factory.service';
+import { RpcService } from './rpc.service';
+
+@Global()
+@Module({
+ imports: [ConfigModule],
+ providers: [
+ ChainRegistryService,
+ AddressNormalizerService,
+ ChainsService,
+ RpcService,
+ PublisherFactory,
+ PortalEncoderService,
+ IntentConverterService,
+ ],
+ exports: [
+ ChainRegistryService,
+ AddressNormalizerService,
+ ChainsService,
+ RpcService,
+ PublisherFactory,
+ PortalEncoderService,
+ IntentConverterService,
+ ],
+})
+export class BlockchainModule {}
diff --git a/src/blockchain/chain-handler.interface.ts b/src/blockchain/chain-handler.interface.ts
new file mode 100644
index 0000000..fbfbd19
--- /dev/null
+++ b/src/blockchain/chain-handler.interface.ts
@@ -0,0 +1,38 @@
+import { ChainType } from '@/shared/types';
+import { BlockchainAddress } from '@/shared/types';
+import { UniversalAddress } from '@/shared/types';
+
+/**
+ * Chain handler interface for pluggable chain-type support.
+ *
+ * Implementing this interface and registering via `ChainRegistryService.bootstrap()` is all
+ * that is needed to add support for a new blockchain type — no switch statements to update.
+ */
+export interface ChainHandler {
+ /** The blockchain type this handler is responsible for. */
+ readonly chainType: ChainType;
+
+ /**
+ * Returns true if the given address string is valid for this chain type.
+ * Used to gate user input before normalization.
+ */
+ validateAddress(address: string): boolean;
+
+ /**
+ * Converts a chain-native address string to UniversalAddress format.
+ * @throws {RoutesCliError} When the address is invalid for this chain type.
+ */
+ normalize(address: string): UniversalAddress;
+
+ /**
+ * Converts a UniversalAddress back to the chain-native address format.
+ * @throws {Error} When denormalization fails.
+ */
+ denormalize(address: UniversalAddress): BlockchainAddress;
+
+ /**
+ * Returns a human-readable description of the expected address format.
+ * Used in error messages and CLI prompts.
+ */
+ getAddressFormat(): string;
+}
diff --git a/src/blockchain/chain-registry.service.ts b/src/blockchain/chain-registry.service.ts
new file mode 100644
index 0000000..08d1391
--- /dev/null
+++ b/src/blockchain/chain-registry.service.ts
@@ -0,0 +1,43 @@
+import { Injectable, OnModuleInit } from '@nestjs/common';
+
+import { RoutesCliError } from '@/shared/errors';
+import { ChainType } from '@/shared/types';
+
+import { EvmChainHandler } from './evm/evm-chain-handler';
+import { SvmChainHandler } from './svm/svm-chain-handler';
+import { TvmChainHandler } from './tvm/tvm-chain-handler';
+import { ChainHandler } from './chain-handler.interface';
+
+@Injectable()
+export class ChainRegistryService implements OnModuleInit {
+ private readonly handlers = new Map();
+ private readonly registeredChainIds = new Set();
+
+ onModuleInit(): void {
+ this.bootstrap([new EvmChainHandler(), new TvmChainHandler(), new SvmChainHandler()]);
+ }
+
+ bootstrap(handlers: ChainHandler[]): void {
+ for (const handler of handlers) {
+ this.handlers.set(handler.chainType, handler);
+ }
+ }
+
+ get(chainType: ChainType): ChainHandler {
+ const handler = this.handlers.get(chainType);
+ if (!handler) throw RoutesCliError.unsupportedChain(chainType);
+ return handler;
+ }
+
+ getAll(): ChainHandler[] {
+ return [...this.handlers.values()];
+ }
+
+ registerChainId(chainId: bigint): void {
+ this.registeredChainIds.add(chainId);
+ }
+
+ isRegistered(chainId: bigint): boolean {
+ return this.registeredChainIds.has(chainId);
+ }
+}
diff --git a/src/blockchain/chains.config.ts b/src/blockchain/chains.config.ts
new file mode 100644
index 0000000..0c0b02b
--- /dev/null
+++ b/src/blockchain/chains.config.ts
@@ -0,0 +1,164 @@
+import { arbitrum, bsc, hyperEvm, mainnet, polygon, ronin, sonic } from 'viem/chains';
+
+import { ChainType } from '@/shared/types';
+
+export interface RawChainConfig {
+ id: bigint;
+ name: string;
+ env: 'production' | 'development';
+ type: ChainType;
+ rpcUrl: string;
+ portalAddress?: string; // raw string, normalized lazily by ChainsService
+ proverAddress?: string;
+ nativeCurrency: { name: string; symbol: string; decimals: number };
+}
+
+export const RAW_CHAIN_CONFIGS: RawChainConfig[] = [
+ // EVM - Production
+ {
+ id: BigInt(mainnet.id),
+ name: 'Ethereum',
+ type: ChainType.EVM,
+ env: 'production',
+ rpcUrl: mainnet.rpcUrls.default.http[0],
+ nativeCurrency: mainnet.nativeCurrency,
+ },
+ {
+ id: 10n,
+ name: 'Optimism',
+ type: ChainType.EVM,
+ env: 'production',
+ rpcUrl: 'https://mainnet.optimism.io',
+ nativeCurrency: { name: 'Ether', symbol: 'ETH', decimals: 18 },
+ },
+ {
+ id: BigInt(bsc.id),
+ name: bsc.name,
+ type: ChainType.EVM,
+ env: 'production',
+ rpcUrl: bsc.rpcUrls.default.http[0],
+ nativeCurrency: bsc.nativeCurrency,
+ },
+ {
+ id: 8453n,
+ name: 'Base',
+ type: ChainType.EVM,
+ env: 'production',
+ rpcUrl: 'https://mainnet.base.org',
+ nativeCurrency: { name: 'Ether', symbol: 'ETH', decimals: 18 },
+ },
+ {
+ id: BigInt(arbitrum.id),
+ name: arbitrum.name,
+ type: ChainType.EVM,
+ env: 'production',
+ rpcUrl: arbitrum.rpcUrls.default.http[0],
+ nativeCurrency: { name: 'Ether', symbol: 'ETH', decimals: 18 },
+ },
+ {
+ id: BigInt(polygon.id),
+ name: polygon.name,
+ type: ChainType.EVM,
+ env: 'production',
+ rpcUrl: polygon.rpcUrls.default.http[0],
+ nativeCurrency: polygon.nativeCurrency,
+ },
+ {
+ id: BigInt(ronin.id),
+ name: ronin.name,
+ type: ChainType.EVM,
+ env: 'production',
+ rpcUrl: ronin.rpcUrls.default.http[0],
+ nativeCurrency: ronin.nativeCurrency,
+ },
+ {
+ id: BigInt(sonic.id),
+ name: sonic.name,
+ type: ChainType.EVM,
+ env: 'production',
+ rpcUrl: sonic.rpcUrls.default.http[0],
+ nativeCurrency: sonic.nativeCurrency,
+ },
+ {
+ id: BigInt(hyperEvm.id),
+ name: hyperEvm.name,
+ type: ChainType.EVM,
+ env: 'production',
+ rpcUrl: hyperEvm.rpcUrls.default.http[0],
+ nativeCurrency: hyperEvm.nativeCurrency,
+ },
+
+ // EVM - Development
+ {
+ id: 84532n,
+ name: 'Base Sepolia',
+ type: ChainType.EVM,
+ env: 'development',
+ rpcUrl: 'https://sepolia.base.org',
+ proverAddress: '0x9523b6c0caac8122dbd5dd1c1d336ceba637038d',
+ nativeCurrency: { name: 'Ether', symbol: 'ETH', decimals: 18 },
+ },
+ {
+ id: 11155420n,
+ name: 'Optimism Sepolia',
+ type: ChainType.EVM,
+ env: 'development',
+ rpcUrl: 'https://sepolia.optimism.io',
+ proverAddress: '0x9523b6c0caac8122dbd5dd1c1d336ceba637038d',
+ nativeCurrency: { name: 'Ether', symbol: 'ETH', decimals: 18 },
+ },
+ {
+ id: 9746n,
+ name: 'Plasma Testnet',
+ type: ChainType.EVM,
+ env: 'development',
+ rpcUrl: 'https://rpc.testnet.plasm.technology',
+ proverAddress: '0x9523b6c0caac8122dbd5dd1c1d336ceba637038d',
+ nativeCurrency: { name: 'Ether', symbol: 'ETH', decimals: 18 },
+ },
+ {
+ id: 11155111n,
+ name: 'Sepolia',
+ type: ChainType.EVM,
+ env: 'development',
+ rpcUrl: 'https://rpc.sepolia.org',
+ proverAddress: '0x9523b6c0caac8122dbd5dd1c1d336ceba637038d',
+ nativeCurrency: { name: 'Ether', symbol: 'ETH', decimals: 18 },
+ },
+
+ // TVM
+ {
+ id: 728126428n,
+ name: 'Tron',
+ type: ChainType.TVM,
+ env: 'production',
+ rpcUrl: 'https://api.trongrid.io',
+ nativeCurrency: { name: 'Tron', symbol: 'TRX', decimals: 6 },
+ },
+ {
+ id: 2494104990n,
+ name: 'Tron Shasta',
+ type: ChainType.TVM,
+ env: 'development',
+ rpcUrl: 'https://api.shasta.trongrid.io',
+ nativeCurrency: { name: 'Tron', symbol: 'TRX', decimals: 6 },
+ },
+
+ // SVM
+ {
+ id: 1399811149n,
+ name: 'Solana',
+ type: ChainType.SVM,
+ env: 'production',
+ rpcUrl: 'https://api.mainnet-beta.solana.com',
+ nativeCurrency: { name: 'Solana', symbol: 'SOL', decimals: 9 },
+ },
+ {
+ id: 1399811150n,
+ name: 'Solana Devnet',
+ type: ChainType.SVM,
+ env: 'development',
+ rpcUrl: 'https://api.devnet.solana.com',
+ nativeCurrency: { name: 'Solana', symbol: 'SOL', decimals: 9 },
+ },
+];
diff --git a/src/blockchain/chains.service.ts b/src/blockchain/chains.service.ts
new file mode 100644
index 0000000..c7086f1
--- /dev/null
+++ b/src/blockchain/chains.service.ts
@@ -0,0 +1,70 @@
+import { Injectable, OnModuleInit } from '@nestjs/common';
+
+import { ConfigService } from '@/config/config.service';
+import { RoutesCliError } from '@/shared/errors';
+import { ChainConfig } from '@/shared/types';
+
+import { AddressNormalizerService } from './address-normalizer.service';
+import { ChainRegistryService } from './chain-registry.service';
+import { RAW_CHAIN_CONFIGS, RawChainConfig } from './chains.config';
+
+@Injectable()
+export class ChainsService implements OnModuleInit {
+ private chains: ChainConfig[] = [];
+
+ constructor(
+ private readonly config: ConfigService,
+ private readonly normalizer: AddressNormalizerService,
+ private readonly registry: ChainRegistryService
+ ) {}
+
+ onModuleInit(): void {
+ const env = this.config.getChainsEnv();
+ this.chains = RAW_CHAIN_CONFIGS.filter(c => c.env === env || c.env === 'production').map(c =>
+ this.normalizeChain(c)
+ );
+
+ for (const chain of this.chains) {
+ this.registry.registerChainId(chain.id);
+ }
+ }
+
+ private normalizeChain(raw: RawChainConfig): ChainConfig {
+ return {
+ ...raw,
+ portalAddress: raw.portalAddress
+ ? // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ this.normalizer.normalize(raw.portalAddress as any, raw.type)
+ : undefined,
+ proverAddress: raw.proverAddress
+ ? // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ this.normalizer.normalize(raw.proverAddress as any, raw.type)
+ : undefined,
+ };
+ }
+
+ listChains(): ChainConfig[] {
+ return this.chains;
+ }
+
+ getChainById(id: bigint): ChainConfig {
+ const chain = this.chains.find(c => c.id === id);
+ if (!chain) throw RoutesCliError.unsupportedChain(id);
+ return chain;
+ }
+
+ findChainById(id: bigint): ChainConfig | undefined {
+ return this.chains.find(c => c.id === id);
+ }
+
+ getChainByName(name: string): ChainConfig {
+ const chain = this.chains.find(c => c.name.toLowerCase() === name.toLowerCase());
+ if (!chain) throw RoutesCliError.unsupportedChain(name);
+ return chain;
+ }
+
+ resolveChain(nameOrId: string): ChainConfig {
+ if (/^\d+$/.test(nameOrId)) return this.getChainById(BigInt(nameOrId));
+ return this.getChainByName(nameOrId);
+ }
+}
diff --git a/src/blockchain/encoding/intent-converter.service.ts b/src/blockchain/encoding/intent-converter.service.ts
new file mode 100644
index 0000000..6d6ce65
--- /dev/null
+++ b/src/blockchain/encoding/intent-converter.service.ts
@@ -0,0 +1,72 @@
+import { Injectable } from '@nestjs/common';
+
+import { Hex } from 'viem';
+
+import { EvmAddress, Intent } from '@/shared/types';
+
+import { AddressNormalizerService } from '../address-normalizer.service';
+
+@Injectable()
+export class IntentConverterService {
+ constructor(private readonly addrNorm: AddressNormalizerService) {}
+
+ toEVMIntent(intent: Intent): {
+ intentHash: Hex | undefined;
+ destination: bigint;
+ sourceChainId: bigint;
+ route: ReturnType;
+ reward: ReturnType;
+ } {
+ return {
+ intentHash: intent.intentHash,
+ destination: intent.destination,
+ sourceChainId: intent.sourceChainId,
+ route: this.toRouteEVMIntent(intent.route),
+ reward: this.toRewardEVMIntent(intent.reward),
+ };
+ }
+
+ toRewardEVMIntent(reward: Intent['reward']): {
+ deadline: bigint;
+ creator: EvmAddress;
+ prover: EvmAddress;
+ nativeAmount: bigint;
+ tokens: { amount: bigint; token: EvmAddress }[];
+ } {
+ return {
+ deadline: reward.deadline,
+ creator: this.addrNorm.denormalizeToEvm(reward.creator),
+ prover: this.addrNorm.denormalizeToEvm(reward.prover),
+ nativeAmount: reward.nativeAmount,
+ tokens: reward.tokens.map(token => ({
+ amount: token.amount,
+ token: this.addrNorm.denormalizeToEvm(token.token),
+ })),
+ };
+ }
+
+ toRouteEVMIntent(route: Intent['route']): {
+ salt: Hex;
+ deadline: bigint;
+ portal: EvmAddress;
+ nativeAmount: bigint;
+ tokens: { amount: bigint; token: EvmAddress }[];
+ calls: { data: Hex; target: EvmAddress; value: bigint }[];
+ } {
+ return {
+ salt: route.salt,
+ deadline: route.deadline,
+ portal: this.addrNorm.denormalizeToEvm(route.portal),
+ nativeAmount: route.nativeAmount,
+ tokens: route.tokens.map(token => ({
+ amount: token.amount,
+ token: this.addrNorm.denormalizeToEvm(token.token),
+ })),
+ calls: route.calls.map(call => ({
+ data: call.data,
+ target: this.addrNorm.denormalizeToEvm(call.target),
+ value: call.value,
+ })),
+ };
+ }
+}
diff --git a/src/blockchain/encoding/portal-encoder.service.ts b/src/blockchain/encoding/portal-encoder.service.ts
new file mode 100644
index 0000000..b2aa115
--- /dev/null
+++ b/src/blockchain/encoding/portal-encoder.service.ts
@@ -0,0 +1,237 @@
+import { Injectable } from '@nestjs/common';
+
+import { decodeAbiParameters, encodeAbiParameters, Hex } from 'viem';
+
+import { EVMRewardAbiItem, EVMRouteAbiItem } from '@/commons/abis/portal.abi';
+import { RewardInstruction, RouteInstruction } from '@/commons/types/portal-idl-coder.type';
+import { bufferToBytes, bytes32ToAddress } from '@/commons/utils/converter';
+import { toSvmRewardForCoder, toSvmRouteForCoder } from '@/commons/utils/instruments';
+import { portalBorshCoder } from '@/commons/utils/portal-borsh-coder';
+import { TvmUtils } from '@/commons/utils/tvm-utils';
+import { ChainType, Intent, SvmAddress } from '@/shared/types';
+
+import { AddressNormalizerService } from '../address-normalizer.service';
+
+@Injectable()
+export class PortalEncoderService {
+ constructor(private readonly addrNorm: AddressNormalizerService) {}
+
+ encode(data: Intent['route'] | Intent['reward'], chainType: ChainType): Hex {
+ switch (chainType) {
+ case ChainType.EVM:
+ case ChainType.TVM:
+ return this.encodeEvm(data);
+ case ChainType.SVM:
+ return this.encodeSvm(data);
+ default:
+ throw new Error(`Unsupported chain type: ${chainType}`);
+ }
+ }
+
+ decode(
+ data: Buffer | string,
+ chainType: ChainType,
+ dataType: Type
+ ): Type extends 'route' ? Intent['route'] : Intent['reward'] {
+ switch (chainType) {
+ case ChainType.EVM:
+ return this.decodeEvm(data, dataType);
+ case ChainType.TVM:
+ return this.decodeTvm(data, dataType);
+ case ChainType.SVM:
+ return this.decodeSvm(data, dataType);
+ default:
+ throw new Error(`Unsupported chain type: ${chainType}`);
+ }
+ }
+
+ isRoute(data: Intent['route'] | Intent['reward']): data is Intent['route'] {
+ return 'salt' in data && 'portal' in data && 'calls' in data;
+ }
+
+ private encodeEvm(data: Intent['route'] | Intent['reward']): Hex {
+ if (this.isRoute(data)) {
+ return encodeAbiParameters(
+ [EVMRouteAbiItem],
+ [
+ {
+ salt: data.salt,
+ deadline: data.deadline,
+ nativeAmount: data.nativeAmount,
+ portal: this.addrNorm.denormalizeToEvm(data.portal),
+ tokens: data.tokens.map(t => ({
+ token: this.addrNorm.denormalizeToEvm(t.token),
+ amount: t.amount,
+ })),
+ calls: data.calls.map(c => ({
+ target: this.addrNorm.denormalizeToEvm(c.target),
+ data: c.data,
+ value: c.value,
+ })),
+ },
+ ]
+ );
+ } else {
+ return encodeAbiParameters(
+ [EVMRewardAbiItem],
+ [
+ {
+ deadline: data.deadline,
+ creator: this.addrNorm.denormalizeToEvm(data.creator),
+ prover: this.addrNorm.denormalizeToEvm(data.prover),
+ nativeAmount: data.nativeAmount,
+ tokens: data.tokens.map(t => ({
+ token: this.addrNorm.denormalizeToEvm(t.token),
+ amount: t.amount,
+ })),
+ },
+ ]
+ );
+ }
+ }
+
+ private encodeSvm(data: Intent['route'] | Intent['reward']): Hex {
+ if (this.isRoute(data)) {
+ return bufferToBytes(
+ portalBorshCoder.types.encode('Route', toSvmRouteForCoder(data))
+ );
+ } else {
+ return bufferToBytes(
+ portalBorshCoder.types.encode('Reward', toSvmRewardForCoder(data))
+ );
+ }
+ }
+
+ private decodeEvm(
+ data: Buffer | string,
+ dataType: Type
+ ): Type extends 'route' ? Intent['route'] : Intent['reward'] {
+ const dataString = typeof data === 'string' ? data : '0x' + data.toString('hex');
+
+ if (dataType === 'reward') {
+ const decoded = decodeAbiParameters([EVMRewardAbiItem], dataString as Hex)[0];
+
+ return {
+ deadline: decoded.deadline,
+ creator: this.addrNorm.normalize(decoded.creator, ChainType.EVM),
+ prover: this.addrNorm.normalize(decoded.prover, ChainType.EVM),
+ nativeAmount: decoded.nativeAmount,
+ tokens: decoded.tokens.map(t => ({
+ token: this.addrNorm.normalize(t.token, ChainType.EVM),
+ amount: t.amount,
+ })),
+ } as Intent['reward'] as Type extends 'route' ? Intent['route'] : Intent['reward'];
+ }
+
+ const decoded = decodeAbiParameters([EVMRouteAbiItem], dataString as Hex)[0];
+ return {
+ salt: decoded.salt,
+ deadline: decoded.deadline,
+ portal: this.addrNorm.normalize(decoded.portal, ChainType.EVM),
+ nativeAmount: decoded.nativeAmount || 0n,
+ tokens: decoded.tokens.map(t => ({
+ token: this.addrNorm.normalize(t.token, ChainType.EVM),
+ amount: t.amount,
+ })),
+ calls: decoded.calls.map(c => ({
+ target: this.addrNorm.normalize(c.target, ChainType.EVM),
+ data: c.data,
+ value: c.value,
+ })),
+ } as Intent['route'] as Type extends 'route' ? Intent['route'] : Intent['reward'];
+ }
+
+ private decodeTvm(
+ data: Buffer | string,
+ dataType: Type
+ ): Type extends 'route' ? Intent['route'] : Intent['reward'] {
+ const dataString = typeof data === 'string' ? data : '0x' + data.toString('hex');
+
+ if (dataType === 'reward') {
+ const decoded = decodeAbiParameters([EVMRewardAbiItem], dataString as Hex)[0];
+
+ return {
+ deadline: decoded.deadline,
+ creator: this.addrNorm.normalize(TvmUtils.fromEvm(decoded.creator), ChainType.TVM),
+ prover: this.addrNorm.normalize(TvmUtils.fromEvm(decoded.prover), ChainType.TVM),
+ nativeAmount: decoded.nativeAmount,
+ tokens: decoded.tokens.map(t => ({
+ token: this.addrNorm.normalize(TvmUtils.fromEvm(t.token), ChainType.TVM),
+ amount: t.amount,
+ })),
+ } as Intent['reward'] as Type extends 'route' ? Intent['route'] : Intent['reward'];
+ }
+
+ const decoded = decodeAbiParameters([EVMRouteAbiItem], dataString as Hex)[0];
+ return {
+ salt: decoded.salt,
+ deadline: decoded.deadline,
+ portal: this.addrNorm.normalize(TvmUtils.fromEvm(decoded.portal), ChainType.TVM),
+ nativeAmount: decoded.nativeAmount || 0n,
+ tokens: decoded.tokens.map(t => ({
+ token: this.addrNorm.normalize(TvmUtils.fromEvm(t.token), ChainType.TVM),
+ amount: t.amount,
+ })),
+ calls: decoded.calls.map(c => ({
+ target: this.addrNorm.normalize(TvmUtils.fromEvm(c.target), ChainType.TVM),
+ data: c.data,
+ value: c.value,
+ })),
+ } as Intent['route'] as Type extends 'route' ? Intent['route'] : Intent['reward'];
+ }
+
+ private decodeSvm(
+ data: Buffer | string,
+ dataType: Type
+ ): Type extends 'route' ? Intent['route'] : Intent['reward'] {
+ const buffer =
+ typeof data === 'string'
+ ? Buffer.from(data.startsWith('0x') ? data.substring(2) : data, 'hex')
+ : data;
+
+ if (dataType === 'route') {
+ const decoded = portalBorshCoder.types.decode('Route', buffer);
+
+ if (decoded === null) {
+ throw new Error('Unable to decode SVM route');
+ }
+
+ const route: Intent['route'] = {
+ salt: bufferToBytes(decoded.salt[0]),
+ deadline: BigInt(decoded.deadline.toString()),
+ portal: this.addrNorm.normalize(bytes32ToAddress(decoded.portal[0]), ChainType.SVM),
+ nativeAmount: BigInt(decoded.native_amount.toString()),
+ tokens: decoded.tokens.map(t => ({
+ token: this.addrNorm.normalize(t.token.toBase58() as SvmAddress, ChainType.SVM),
+ amount: BigInt(t.amount.toString()),
+ })),
+ calls: decoded.calls.map(c => ({
+ target: this.addrNorm.normalize(bytes32ToAddress(c.target[0]), ChainType.SVM),
+ data: bufferToBytes(c.data),
+ value: 0n,
+ })),
+ };
+
+ return route as Type extends 'route' ? Intent['route'] : Intent['reward'];
+ }
+
+ const decoded = portalBorshCoder.types.decode('Reward', buffer);
+
+ if (decoded === null) {
+ throw new Error('Unable to decode SVM reward');
+ }
+
+ const reward: Intent['reward'] = {
+ deadline: BigInt(decoded.deadline.toString()),
+ creator: this.addrNorm.normalize(decoded.creator.toBase58() as SvmAddress, ChainType.SVM),
+ prover: this.addrNorm.normalize(decoded.prover.toBase58() as SvmAddress, ChainType.SVM),
+ nativeAmount: BigInt(decoded.native_amount.toString()),
+ tokens: decoded.tokens.map(t => ({
+ token: this.addrNorm.normalize(t.token.toBase58() as SvmAddress, ChainType.SVM),
+ amount: BigInt(t.amount.toString()),
+ })),
+ };
+
+ return reward as Type extends 'route' ? Intent['route'] : Intent['reward'];
+ }
+}
diff --git a/src/blockchain/evm-publisher.ts b/src/blockchain/evm-publisher.ts
deleted file mode 100644
index bca0444..0000000
--- a/src/blockchain/evm-publisher.ts
+++ /dev/null
@@ -1,296 +0,0 @@
-/**
- * EVM Chain Publisher
- */
-
-import {
- Address,
- Chain,
- createPublicClient,
- createWalletClient,
- encodeFunctionData,
- erc20Abi,
- Hex,
- http,
- maxUint256,
- parseEventLogs,
-} from 'viem';
-import { privateKeyToAccount } from 'viem/accounts';
-import * as chains from 'viem/chains';
-
-import { portalAbi } from '@/commons/abis/portal.abi';
-import { getChainById } from '@/config/chains';
-import { Intent } from '@/core/interfaces/intent';
-import { UniversalAddress } from '@/core/types/universal-address';
-import { AddressNormalizer } from '@/core/utils/address-normalizer';
-import { logger } from '@/utils/logger';
-
-import { BasePublisher, PublishResult } from './base-publisher';
-
-export class EvmPublisher extends BasePublisher {
- async publish(
- source: bigint,
- destination: bigint,
- reward: Intent['reward'],
- encodedRoute: string,
- privateKey: string,
- portalAddress?: UniversalAddress,
- proverAddress?: UniversalAddress
- ): Promise {
- try {
- const account = privateKeyToAccount(privateKey as Hex);
- const chain = this.getChain(source);
-
- const walletClient = createWalletClient({
- account,
- chain,
- transport: http(this.rpcUrl),
- });
-
- const publicClient = createPublicClient({
- chain,
- transport: http(this.rpcUrl),
- });
-
- // Get Portal address
- const sourceChainConfig = getChainById(source);
- const destinationChainConfig = getChainById(destination);
-
- const portalAddrUniversal = portalAddress ?? sourceChainConfig?.portalAddress;
-
- if (!portalAddrUniversal) {
- throw new Error(`No Portal address configured for chain ${source}`);
- }
-
- const finalPortalAddress = AddressNormalizer.denormalizeToEvm(portalAddrUniversal);
-
- if (!destinationChainConfig) {
- throw new Error(`Destination chain is not configured ${destination}`);
- }
-
- // Check native balance if required
- if (reward.nativeAmount > 0n) {
- logger.spinner('Checking native balance...');
- const balance = await publicClient.getBalance({
- address: account.address,
- });
-
- if (balance < reward.nativeAmount) {
- logger.fail(
- `Insufficient native balance. Required: ${reward.nativeAmount}, Available: ${balance}`
- );
- throw new Error(
- `Insufficient native balance. Required: ${reward.nativeAmount}, Available: ${balance}`
- );
- }
- logger.succeed(`Native balance sufficient: ${balance} wei`);
- }
-
- // Check and approve tokens for the reward
- if (reward.tokens.length > 0) {
- logger.info('Checking token balances and approvals...');
- }
-
- for (let i = 0; i < reward.tokens.length; i++) {
- const token = reward.tokens[i];
- const tokenAddress = AddressNormalizer.denormalizeToEvm(token.token);
-
- // Check token balance first
- logger.spinner(
- `Checking balance for token ${i + 1}/${reward.tokens.length}: ${tokenAddress}`
- );
-
- const tokenBalance = await publicClient.readContract({
- address: tokenAddress,
- abi: erc20Abi,
- functionName: 'balanceOf',
- args: [account.address],
- });
-
- if (tokenBalance < token.amount) {
- logger.fail(`Insufficient token balance for ${tokenAddress}`);
- throw new Error(
- `Insufficient token balance for ${tokenAddress}. Required: ${token.amount}, Available: ${tokenBalance}`
- );
- }
-
- logger.succeed(`Token balance sufficient: ${tokenBalance}`);
-
- // Check current allowance
- const allowance = await publicClient.readContract({
- address: tokenAddress,
- abi: erc20Abi,
- functionName: 'allowance',
- args: [account.address, finalPortalAddress],
- });
-
- if (allowance < token.amount) {
- logger.spinner(`Approving token ${tokenAddress}...`);
-
- // Approve max amount to avoid future approvals
- const approveTx = await walletClient.writeContract({
- address: tokenAddress,
- abi: erc20Abi,
- functionName: 'approve',
- args: [finalPortalAddress, maxUint256],
- });
-
- // Wait for approval confirmation
- logger.updateSpinner('Waiting for approval confirmation...');
- const approvalReceipt = await publicClient.waitForTransactionReceipt({
- hash: approveTx,
- confirmations: 2,
- });
-
- if (approvalReceipt.status !== 'success') {
- logger.fail(`Token approval failed for ${tokenAddress}`);
- throw new Error(`Token approval failed for ${tokenAddress}`);
- }
-
- logger.succeed(`Token approved: ${tokenAddress}`);
- } else {
- logger.info(`Token already approved: ${tokenAddress}`);
- }
- }
-
- // Prepare reward struct
- const evmReward = {
- deadline: reward.deadline,
- nativeAmount: reward.nativeAmount,
- creator: AddressNormalizer.denormalizeToEvm(reward.creator),
- prover: AddressNormalizer.denormalizeToEvm(proverAddress ?? reward.prover),
- tokens: reward.tokens.map(t => ({
- token: AddressNormalizer.denormalizeToEvm(t.token),
- amount: t.amount,
- })),
- };
-
- // Encode the function call
- const data = encodeFunctionData({
- abi: portalAbi,
- functionName: 'publishAndFund',
- args: [destination, encodedRoute as Hex, evmReward, false],
- });
-
- // Send transaction with native value if required
- logger.spinner('Publishing intent to Portal contract...');
- const hash = await walletClient.sendTransaction({
- to: finalPortalAddress,
- data,
- value: reward.nativeAmount,
- });
-
- // Wait for transaction receipt
- logger.updateSpinner('Waiting for transaction confirmation...');
- const receipt = await publicClient.waitForTransactionReceipt({ hash });
- logger.succeed('Transaction confirmed');
-
- if (receipt.status === 'success') {
- const [intentPublishEvent] = parseEventLogs({
- abi: portalAbi,
- strict: true,
- eventName: 'IntentPublished',
- logs: receipt.logs,
- });
-
- return {
- success: true,
- transactionHash: hash,
- intentHash: intentPublishEvent.args.intentHash,
- };
- } else {
- return {
- success: false,
- error: 'Transaction failed',
- };
- }
- } catch (error: unknown) {
- logger.stopSpinner();
- const errorMessage = error instanceof Error ? error.message : 'Unknown error';
- return {
- success: false,
- error: errorMessage,
- };
- }
- }
-
- async getBalance(address: string, chainId?: bigint): Promise {
- // Use the provided chainId to get the correct chain configuration
- // If no chainId is provided, default to mainnet (though this shouldn't happen in normal usage)
- const chain = chainId ? this.getChain(chainId) : chains.mainnet;
-
- const publicClient = createPublicClient({
- chain,
- transport: http(this.rpcUrl),
- });
-
- return await publicClient.getBalance({ address: address as Address });
- }
-
- async validate(
- intent: Intent,
- senderAddress: string
- ): Promise<{ valid: boolean; error?: string }> {
- try {
- const chain = this.getChain(intent.sourceChainId);
- const publicClient = createPublicClient({
- chain,
- transport: http(this.rpcUrl),
- });
-
- // Check if sender has enough balance for reward native amount on the source chain
- if (intent.reward.nativeAmount > 0n) {
- const balance = await this.getBalance(senderAddress, intent.sourceChainId);
-
- if (balance < intent.reward.nativeAmount) {
- return {
- valid: false,
- error: `Insufficient native balance. Required: ${intent.reward.nativeAmount}, Available: ${balance}`,
- };
- }
- }
-
- // Check token balances
- for (const token of intent.reward.tokens) {
- const tokenAddress = AddressNormalizer.denormalizeToEvm(token.token);
-
- const tokenBalance = await publicClient.readContract({
- address: tokenAddress,
- abi: erc20Abi,
- functionName: 'balanceOf',
- args: [senderAddress as Address],
- });
-
- if (tokenBalance < token.amount) {
- return {
- valid: false,
- error: `Insufficient token balance for ${tokenAddress}. Required: ${token.amount}, Available: ${tokenBalance}`,
- };
- }
- }
-
- return { valid: true };
- } catch (error: unknown) {
- const errorMessage = error instanceof Error ? error.message : 'Validation failed';
- return {
- valid: false,
- error: errorMessage,
- };
- }
- }
-
- private getChain(chainId: bigint) {
- const id = Number(chainId);
-
- // Find viem chain by ID
- const viemChain = Object.values(chains).find((chain: Chain) => chain.id === id);
-
- if (!viemChain) {
- throw new Error(
- `Chain ID ${id} is not supported. Please use a chain that exists in viem/chains. ` +
- `Popular chains include: Ethereum (1), Optimism (10), Base (8453), Arbitrum (42161), Polygon (137), BSC (56).`
- );
- }
-
- return viemChain;
- }
-}
diff --git a/src/blockchain/evm/evm-chain-handler.ts b/src/blockchain/evm/evm-chain-handler.ts
new file mode 100644
index 0000000..60d68e6
--- /dev/null
+++ b/src/blockchain/evm/evm-chain-handler.ts
@@ -0,0 +1,29 @@
+import type { ChainHandler } from '@/blockchain/chain-handler.interface';
+import { AddressNormalizer } from '@/blockchain/utils/address-normalizer';
+import { EvmAddressSchema } from '@/blockchain/validation';
+import { RoutesCliError } from '@/shared/errors';
+import { BlockchainAddress, ChainType, EvmAddress, UniversalAddress } from '@/shared/types';
+
+export class EvmChainHandler implements ChainHandler {
+ readonly chainType = ChainType.EVM;
+
+ validateAddress(address: string): boolean {
+ return EvmAddressSchema.safeParse(address).success;
+ }
+
+ normalize(address: string): UniversalAddress {
+ const result = EvmAddressSchema.safeParse(address);
+ if (!result.success) {
+ throw RoutesCliError.invalidAddress(address, 'EVM');
+ }
+ return AddressNormalizer.normalizeEvm(address as EvmAddress);
+ }
+
+ denormalize(address: UniversalAddress): BlockchainAddress {
+ return AddressNormalizer.denormalizeToEvm(address);
+ }
+
+ getAddressFormat(): string {
+ return '0x followed by 40 hex characters (e.g., 0x742d35Cc6634C0532925a3b8D65C32c2b3f6dE1b)';
+ }
+}
diff --git a/src/blockchain/evm/evm-client-factory.ts b/src/blockchain/evm/evm-client-factory.ts
new file mode 100644
index 0000000..c2142f9
--- /dev/null
+++ b/src/blockchain/evm/evm-client-factory.ts
@@ -0,0 +1,48 @@
+/**
+ * EVM Client Factory
+ *
+ * Injectable factory for creating viem clients, enabling dependency injection
+ * in EvmPublisher for testability without live RPC connections.
+ */
+
+import {
+ Account,
+ Chain,
+ createPublicClient,
+ createWalletClient,
+ http,
+ type PublicClient,
+ Transport,
+ type WalletClient,
+} from 'viem';
+
+export interface EvmClientFactory {
+ createPublicClient(config: { chain: Chain; rpcUrl: string }): PublicClient;
+ createWalletClient(config: {
+ chain: Chain;
+ rpcUrl: string;
+ account: Account;
+ }): WalletClient;
+}
+
+export class DefaultEvmClientFactory implements EvmClientFactory {
+ createPublicClient({ chain, rpcUrl }: { chain: Chain; rpcUrl: string }): PublicClient {
+ return createPublicClient({ chain, transport: http(rpcUrl) }) as PublicClient;
+ }
+
+ createWalletClient({
+ chain,
+ rpcUrl,
+ account,
+ }: {
+ chain: Chain;
+ rpcUrl: string;
+ account: Account;
+ }): WalletClient {
+ return createWalletClient({
+ account,
+ chain,
+ transport: http(rpcUrl),
+ }) as WalletClient;
+ }
+}
diff --git a/src/blockchain/evm/evm.publisher.ts b/src/blockchain/evm/evm.publisher.ts
new file mode 100644
index 0000000..a1d6385
--- /dev/null
+++ b/src/blockchain/evm/evm.publisher.ts
@@ -0,0 +1,333 @@
+/**
+ * EVM Chain Publisher (NestJS injectable)
+ */
+
+import { Injectable } from '@nestjs/common';
+
+import {
+ Account,
+ Address,
+ Chain,
+ encodeFunctionData,
+ erc20Abi,
+ Hex,
+ maxUint256,
+ parseEventLogs,
+ type PublicClient,
+ Transport,
+ type WalletClient,
+} from 'viem';
+import { privateKeyToAccount } from 'viem/accounts';
+import * as chains from 'viem/chains';
+
+import { AddressNormalizer } from '@/blockchain/utils/address-normalizer';
+import { portalAbi } from '@/commons/abis/portal.abi';
+import { KeyHandle } from '@/shared/security';
+import { ChainConfig, Intent, UniversalAddress } from '@/shared/types';
+import { logger } from '@/utils/logger';
+
+import { BasePublisher, IntentStatus, PublishResult, ValidationResult } from '../base.publisher';
+import { ChainRegistryService } from '../chain-registry.service';
+import { ChainsService } from '../chains.service';
+
+import { DefaultEvmClientFactory, EvmClientFactory } from './evm-client-factory';
+
+@Injectable()
+export class EvmPublisher extends BasePublisher {
+ private readonly clientFactory: EvmClientFactory;
+ private _publicClients: Map = new Map();
+
+ constructor(
+ rpcUrl: string,
+ registry: ChainRegistryService,
+ private readonly chains: ChainsService,
+ clientFactory: EvmClientFactory = new DefaultEvmClientFactory()
+ ) {
+ super(rpcUrl, registry);
+ this.clientFactory = clientFactory;
+ }
+
+ override async publish(
+ source: bigint,
+ destination: bigint,
+ reward: Intent['reward'],
+ encodedRoute: string,
+ keyHandle: KeyHandle,
+ portalAddress?: UniversalAddress,
+ proverAddress?: UniversalAddress
+ ): Promise {
+ this.runPreflightChecks(source);
+ return keyHandle.useAsync(async rawKey => {
+ const account = privateKeyToAccount(rawKey as Hex);
+ return this.runSafely(async () => {
+ const chain = this.getChain(source);
+
+ const walletClient: WalletClient =
+ this.clientFactory.createWalletClient({
+ chain,
+ rpcUrl: this.rpcUrl,
+ account,
+ });
+
+ const publicClient = this.getPublicClient(chain);
+
+ const sourceChainConfig = this.chains.findChainById(source);
+ const destinationChainConfig = this.chains.findChainById(destination);
+
+ const portalAddrUniversal = portalAddress ?? sourceChainConfig?.portalAddress;
+
+ if (!portalAddrUniversal) {
+ throw new Error(`No Portal address configured for chain ${source}`);
+ }
+
+ const finalPortalAddress = AddressNormalizer.denormalizeToEvm(portalAddrUniversal);
+
+ if (!destinationChainConfig) {
+ throw new Error(`Destination chain is not configured ${destination}`);
+ }
+
+ if (reward.nativeAmount > 0n) {
+ logger.spinner('Checking native balance...');
+ const balance = await publicClient.getBalance({
+ address: account.address,
+ });
+
+ if (balance < reward.nativeAmount) {
+ logger.fail(
+ `Insufficient native balance. Required: ${reward.nativeAmount}, Available: ${balance}`
+ );
+ throw new Error(
+ `Insufficient native balance. Required: ${reward.nativeAmount}, Available: ${balance}`
+ );
+ }
+ logger.succeed(`Native balance sufficient: ${balance} wei`);
+ }
+
+ if (reward.tokens.length > 0) {
+ logger.info('Checking token balances and approvals...');
+ }
+
+ for (let i = 0; i < reward.tokens.length; i++) {
+ const token = reward.tokens[i];
+ const tokenAddress = AddressNormalizer.denormalizeToEvm(token.token);
+
+ logger.spinner(
+ `Checking balance for token ${i + 1}/${reward.tokens.length}: ${tokenAddress}`
+ );
+
+ const tokenBalance = await publicClient.readContract({
+ address: tokenAddress,
+ abi: erc20Abi,
+ functionName: 'balanceOf',
+ args: [account.address],
+ });
+
+ if (tokenBalance < token.amount) {
+ logger.fail(`Insufficient token balance for ${tokenAddress}`);
+ throw new Error(
+ `Insufficient token balance for ${tokenAddress}. Required: ${token.amount}, Available: ${tokenBalance}`
+ );
+ }
+
+ logger.succeed(`Token balance sufficient: ${tokenBalance}`);
+
+ const allowance = await publicClient.readContract({
+ address: tokenAddress,
+ abi: erc20Abi,
+ functionName: 'allowance',
+ args: [account.address, finalPortalAddress],
+ });
+
+ if (allowance < token.amount) {
+ logger.spinner(`Approving token ${tokenAddress}...`);
+
+ const approveTx = await walletClient.writeContract({
+ address: tokenAddress,
+ abi: erc20Abi,
+ functionName: 'approve',
+ args: [finalPortalAddress, maxUint256],
+ });
+
+ logger.updateSpinner('Waiting for approval confirmation...');
+ const approvalReceipt = await publicClient.waitForTransactionReceipt({
+ hash: approveTx,
+ confirmations: 2,
+ });
+
+ if (approvalReceipt.status !== 'success') {
+ logger.fail(`Token approval failed for ${tokenAddress}`);
+ throw new Error(`Token approval failed for ${tokenAddress}`);
+ }
+
+ logger.succeed(`Token approved: ${tokenAddress}`);
+ } else {
+ logger.info(`Token already approved: ${tokenAddress}`);
+ }
+ }
+
+ const evmReward = {
+ deadline: reward.deadline,
+ nativeAmount: reward.nativeAmount,
+ creator: AddressNormalizer.denormalizeToEvm(reward.creator),
+ prover: AddressNormalizer.denormalizeToEvm(proverAddress ?? reward.prover),
+ tokens: reward.tokens.map(t => ({
+ token: AddressNormalizer.denormalizeToEvm(t.token),
+ amount: t.amount,
+ })),
+ };
+
+ const data = encodeFunctionData({
+ abi: portalAbi,
+ functionName: 'publishAndFund',
+ args: [destination, encodedRoute as Hex, evmReward, false],
+ });
+
+ logger.spinner('Publishing intent to Portal contract...');
+ const hash = await walletClient.sendTransaction({
+ to: finalPortalAddress,
+ data,
+ value: reward.nativeAmount,
+ });
+
+ logger.updateSpinner('Waiting for transaction confirmation...');
+ const receipt = await publicClient.waitForTransactionReceipt({ hash });
+ logger.succeed('Transaction confirmed');
+
+ if (receipt.status === 'success') {
+ const [intentPublishEvent] = parseEventLogs({
+ abi: portalAbi,
+ strict: true,
+ eventName: 'IntentPublished',
+ logs: receipt.logs,
+ });
+
+ return {
+ success: true,
+ transactionHash: hash,
+ intentHash: intentPublishEvent.args.intentHash,
+ };
+ } else {
+ return {
+ success: false,
+ error: 'Transaction failed',
+ };
+ }
+ });
+ });
+ }
+
+ override async getBalance(address: string, chainId?: bigint): Promise {
+ const chain = chainId ? this.getChain(chainId) : this.getChain(BigInt(chains.mainnet.id));
+ return await this.getPublicClient(chain).getBalance({ address: address as Address });
+ }
+
+ override async validate(
+ reward: Intent['reward'],
+ senderAddress: string,
+ chainId: bigint
+ ): Promise {
+ const errors: string[] = [];
+ try {
+ const publicClient = this.getPublicClient(this.getChain(chainId));
+
+ if (reward.nativeAmount > 0n) {
+ const balance = await publicClient.getBalance({ address: senderAddress as Address });
+ if (balance < reward.nativeAmount) {
+ errors.push(
+ `Insufficient native balance. Required: ${reward.nativeAmount}, Available: ${balance}`
+ );
+ }
+ }
+
+ for (const token of reward.tokens) {
+ const tokenAddress = AddressNormalizer.denormalizeToEvm(token.token);
+ const tokenBalance = await publicClient.readContract({
+ address: tokenAddress,
+ abi: erc20Abi,
+ functionName: 'balanceOf',
+ args: [senderAddress as Address],
+ });
+ if (tokenBalance < token.amount) {
+ errors.push(
+ `Insufficient token balance for ${tokenAddress}. Required: ${token.amount}, Available: ${tokenBalance}`
+ );
+ }
+ }
+ } catch (error: unknown) {
+ const message = error instanceof Error ? error.message : 'Validation failed';
+ errors.push(message);
+ }
+ return { valid: errors.length === 0, errors };
+ }
+
+ override async getStatus(
+ intentHash: string,
+ chain: ChainConfig,
+ portalAddress?: UniversalAddress
+ ): Promise {
+ const resolvedPortal = portalAddress ?? chain.portalAddress;
+ if (!resolvedPortal) {
+ throw new Error(`No portal address configured for chain ${chain.id}`);
+ }
+
+ const evmPortalAddress = AddressNormalizer.denormalizeToEvm(resolvedPortal);
+ const viemChain = this.getChain(chain.id);
+ const publicClient = this.getPublicClient(viemChain);
+
+ try {
+ const currentBlock = await publicClient.getBlockNumber();
+
+ const events = await publicClient.getContractEvents({
+ address: evmPortalAddress,
+ abi: portalAbi,
+ eventName: 'IntentFulfilled',
+ fromBlock: currentBlock - 1_000n,
+ args: { intentHash: intentHash as Hex },
+ });
+
+ const event = events[0];
+ if (!event) {
+ return { fulfilled: false };
+ }
+
+ const status: IntentStatus = {
+ fulfilled: true,
+ solver: AddressNormalizer.denormalizeToEvm(event.args.claimant as UniversalAddress),
+ fulfillmentTxHash: event.transactionHash ?? undefined,
+ blockNumber: event.blockNumber ?? undefined,
+ };
+
+ if (event.blockNumber) {
+ const block = await publicClient.getBlock({ blockNumber: event.blockNumber });
+ status.timestamp = Number(block.timestamp);
+ }
+
+ return status;
+ } catch (error) {
+ console.error(error);
+ return { fulfilled: false };
+ }
+ }
+
+ private getPublicClient(chain: Chain): PublicClient {
+ const cached = this._publicClients.get(chain.id);
+ if (cached) return cached;
+ const client = this.clientFactory.createPublicClient({ chain, rpcUrl: this.rpcUrl });
+ this._publicClients.set(chain.id, client);
+ return client;
+ }
+
+ private getChain(chainId: bigint): Chain {
+ const id = Number(chainId);
+ const viemChain = Object.values(chains).find((chain: Chain) => chain.id === id);
+
+ if (!viemChain) {
+ throw new Error(
+ `Chain ID ${id} is not supported. Please use a chain that exists in viem/chains. ` +
+ `Popular chains include: Ethereum (1), Optimism (10), Base (8453), Arbitrum (42161), Polygon (137), BSC (56).`
+ );
+ }
+
+ return viemChain;
+ }
+}
diff --git a/src/blockchain/publisher-factory.service.ts b/src/blockchain/publisher-factory.service.ts
new file mode 100644
index 0000000..9089469
--- /dev/null
+++ b/src/blockchain/publisher-factory.service.ts
@@ -0,0 +1,35 @@
+import { Injectable } from '@nestjs/common';
+
+import { ChainType } from '@/shared/types';
+import { ChainConfig } from '@/shared/types';
+
+import { EvmPublisher } from './evm/evm.publisher';
+import { SvmPublisher } from './svm/svm.publisher';
+import { TvmPublisher } from './tvm/tvm.publisher';
+import { BasePublisher } from './base.publisher';
+import { ChainRegistryService } from './chain-registry.service';
+import { ChainsService } from './chains.service';
+import { RpcService } from './rpc.service';
+
+@Injectable()
+export class PublisherFactory {
+ constructor(
+ private readonly registry: ChainRegistryService,
+ private readonly rpcService: RpcService,
+ private readonly chains: ChainsService
+ ) {}
+
+ create(chain: ChainConfig): BasePublisher {
+ const rpcUrl = this.rpcService.getUrl(chain);
+ switch (chain.type) {
+ case ChainType.EVM:
+ return new EvmPublisher(rpcUrl, this.registry, this.chains);
+ case ChainType.TVM:
+ return new TvmPublisher(rpcUrl, this.registry, this.chains);
+ case ChainType.SVM:
+ return new SvmPublisher(rpcUrl, this.registry, this.chains);
+ default:
+ throw new Error(`Unsupported chain type: ${chain.type}`);
+ }
+ }
+}
diff --git a/src/blockchain/rpc.service.ts b/src/blockchain/rpc.service.ts
new file mode 100644
index 0000000..1603db5
--- /dev/null
+++ b/src/blockchain/rpc.service.ts
@@ -0,0 +1,36 @@
+import { Injectable } from '@nestjs/common';
+
+import { ConfigService } from '@/config/config.service';
+import { ChainConfig, ChainType } from '@/shared/types';
+
+import 'dotenv/config';
+
+@Injectable()
+export class RpcService {
+ constructor(private readonly config: ConfigService) {}
+
+ getUrl(chain: ChainConfig): string {
+ // 1. Per-chain override: EVM_RPC_URL_{CHAIN_ID} (e.g. EVM_RPC_URL_8453)
+ // Uses process.env directly because Zod strips unknown keys during validation.
+ if (chain.type === ChainType.EVM) {
+ const perChainUrl = process.env[`EVM_RPC_URL_${chain.id}`];
+ if (perChainUrl) return perChainUrl;
+ }
+ // 2. Chain-type override (TVM_RPC_URL, SVM_RPC_URL)
+ const envOverride = this.config.getRpcUrl(chain.type, 'primary');
+ // 3. Hardcoded chain default
+ return envOverride || chain.rpcUrl;
+ }
+
+ getFallbackUrl(chain: ChainConfig): string | undefined {
+ return this.config.getRpcUrl(chain.type, 'fallback') || undefined;
+ }
+
+ async withFallback(primary: () => Promise, fallback: () => Promise): Promise {
+ try {
+ return await primary();
+ } catch {
+ return fallback();
+ }
+ }
+}
diff --git a/src/blockchain/svm-publisher.ts b/src/blockchain/svm-publisher.ts
deleted file mode 100644
index 2f07778..0000000
--- a/src/blockchain/svm-publisher.ts
+++ /dev/null
@@ -1,215 +0,0 @@
-/**
- * SVM (Solana) Chain Publisher - Refactored for maintainability
- * Main publisher class that orchestrates Solana-specific intent publishing
- */
-
-import { Connection, Keypair, PublicKey } from '@solana/web3.js';
-import { Hex } from 'viem';
-
-import { PortalHashUtils } from '@/commons/utils/portal-hash.utils';
-import { getChainById } from '@/config/chains';
-import { ChainType, Intent } from '@/core/interfaces/intent';
-import { UniversalAddress } from '@/core/types/universal-address';
-import { AddressNormalizer } from '@/core/utils/address-normalizer';
-import { logger } from '@/utils/logger';
-
-import { SVM_CONNECTION_CONFIG, SVM_ERROR_MESSAGES, SVM_LOG_MESSAGES } from './svm/svm-constants';
-import { executeFunding } from './svm/svm-transaction';
-import { PublishContext, SvmError, SvmErrorType } from './svm/svm-types';
-import { BasePublisher, PublishResult } from './base-publisher';
-
-export class SvmPublisher extends BasePublisher {
- private connection: Connection;
-
- constructor(rpcUrl: string) {
- super(rpcUrl);
- this.connection = new Connection(rpcUrl, SVM_CONNECTION_CONFIG);
- }
-
- /**
- * Publishes an intent to the Solana blockchain
- * Simplified main method that delegates to helper functions
- */
- async publish(
- source: bigint,
- destination: bigint,
- reward: Intent['reward'],
- encodedRoute: string,
- privateKey: string,
- portalAddress?: UniversalAddress
- ): Promise {
- try {
- // Parse private key and validate configuration
- const keypair = this.parsePrivateKey(privateKey);
- const portalProgramId = portalAddress
- ? new PublicKey(AddressNormalizer.denormalize(portalAddress, ChainType.SVM))
- : this.getPortalProgramId(source);
-
- // Calculate hashes
- const { intentHash, routeHash } = PortalHashUtils.getIntentHashFromReward(
- source,
- destination,
- encodedRoute as Hex,
- reward
- );
-
- // Log initial information
- this.logPublishInfo(portalProgramId, keypair, destination);
-
- // Create publish context for all operations
- const context: PublishContext = {
- source,
- destination,
- reward,
- encodedRoute,
- privateKey,
- intentHash,
- routeHash,
- keypair,
- portalProgramId,
- };
-
- // Execute funding (tokens must be present)
- const fundingResult = await this.fundIntent(context);
-
- if (fundingResult.success) {
- logger.info(SVM_LOG_MESSAGES.FUNDING_SUCCESS(fundingResult.transactionHash!));
- }
-
- return fundingResult;
- } catch (error: any) {
- return this.handleError(error);
- }
- }
-
- /**
- * Funds an intent if reward tokens are present
- */
- private async fundIntent(context: PublishContext): Promise {
- // Funding requires tokens in reward
- if (context.reward.tokens.length === 0) {
- const errorMsg = 'Cannot fund intent: No reward tokens specified';
- logger.error(errorMsg);
- return {
- success: false,
- error: errorMsg,
- };
- }
-
- try {
- const fundingResult = await executeFunding(this.connection, context);
-
- if (!fundingResult.success) {
- logger.error(`Funding failed: ${fundingResult.error}`);
- return fundingResult;
- }
-
- logger.info(SVM_LOG_MESSAGES.FUNDING_SUCCESS(fundingResult.transactionHash!));
- return fundingResult;
- } catch (error: any) {
- if (error instanceof SvmError) {
- return {
- success: false,
- error: error.message,
- };
- }
- throw error;
- }
- }
-
- /**
- * Gets the native SOL balance for an address
- */
- async getBalance(address: string, _chainId?: bigint): Promise {
- try {
- const publicKey = new PublicKey(address);
- const balance = await this.connection.getBalance(publicKey);
- return BigInt(balance);
- } catch {
- return 0n;
- }
- }
-
- /**
- * Parses a private key in various formats (Base58, array, comma-separated)
- */
- private parsePrivateKey(privateKey: string): Keypair {
- try {
- // Array format: [1,2,3,...]
- if (privateKey.startsWith('[') && privateKey.endsWith(']')) {
- const bytes = JSON.parse(privateKey);
- return Keypair.fromSecretKey(new Uint8Array(bytes));
- }
-
- // Comma-separated format: 1,2,3,...
- if (privateKey.includes(',')) {
- const bytes = privateKey.split(',').map(b => parseInt(b.trim()));
- return Keypair.fromSecretKey(new Uint8Array(bytes));
- }
-
- // Base58 format (default)
- // eslint-disable-next-line @typescript-eslint/no-require-imports
- const bs58 = require('bs58');
- const bytes = bs58.decode(privateKey);
- return Keypair.fromSecretKey(bytes);
- } catch (error: any) {
- throw new SvmError(
- SvmErrorType.INVALID_CONFIG,
- SVM_ERROR_MESSAGES.INVALID_PRIVATE_KEY,
- error
- );
- }
- }
-
- /**
- * Gets the Portal program ID for a given chain
- */
- private getPortalProgramId(chainId: bigint): PublicKey {
- const chainConfig = getChainById(chainId);
-
- if (!chainConfig?.portalAddress) {
- throw new SvmError(
- SvmErrorType.INVALID_CONFIG,
- SVM_ERROR_MESSAGES.NO_PORTAL_ADDRESS(chainId)
- );
- }
-
- return new PublicKey(AddressNormalizer.denormalize(chainConfig.portalAddress, ChainType.SVM));
- }
-
- /**
- * Logs initial publishing information
- */
- private logPublishInfo(portalProgramId: PublicKey, keypair: Keypair, destination: bigint): void {
- logger.info(SVM_LOG_MESSAGES.PORTAL_PROGRAM(portalProgramId.toString()));
- logger.info(SVM_LOG_MESSAGES.CREATOR(keypair.publicKey.toString()));
- logger.info(SVM_LOG_MESSAGES.DESTINATION_CHAIN(destination));
- }
-
- /**
- * Handles errors with proper formatting and logging
- */
- private handleError(error: any): PublishResult {
- logger.stopSpinner();
-
- let errorMessage = error.message || 'Unknown error';
-
- // Add additional error context if available
- if (error.logs) {
- errorMessage += `\nLogs: ${error.logs.join('\n')}`;
- }
- if (error.err) {
- errorMessage += `\nError: ${JSON.stringify(error.err)}`;
- }
- if (error.details) {
- errorMessage += `\nDetails: ${JSON.stringify(error.details)}`;
- }
-
- logger.error(`Transaction failed: ${errorMessage}`);
-
- return {
- success: false,
- error: errorMessage,
- };
- }
-}
diff --git a/src/blockchain/svm/pda-manager.ts b/src/blockchain/svm/pda-manager.ts
new file mode 100644
index 0000000..88546b4
--- /dev/null
+++ b/src/blockchain/svm/pda-manager.ts
@@ -0,0 +1,24 @@
+/**
+ * SVM PDA Manager
+ * Consolidates all Program Derived Address (PDA) derivations for Solana operations.
+ * All PDA seeds and derivation logic lives here.
+ */
+
+import { PublicKey } from '@solana/web3.js';
+
+import { createPdaSeedBuffer, hexToBuffer } from './svm-buffer-utils';
+import { SVM_PDA_SEEDS } from './svm-constants';
+
+/**
+ * Calculates the vault PDA for an intent.
+ * Vault PDA: ["vault", intent_hash] — stores reward tokens.
+ */
+export function calculateVaultPDA(intentHash: string, portalProgramId: PublicKey): PublicKey {
+ const intentHashBytes = hexToBuffer(intentHash);
+ const [vaultPda] = PublicKey.findProgramAddressSync(
+ [createPdaSeedBuffer(SVM_PDA_SEEDS.VAULT), intentHashBytes],
+ portalProgramId
+ );
+
+ return vaultPda;
+}
diff --git a/src/blockchain/svm/solana-client.ts b/src/blockchain/svm/solana-client.ts
new file mode 100644
index 0000000..89501bb
--- /dev/null
+++ b/src/blockchain/svm/solana-client.ts
@@ -0,0 +1,39 @@
+/**
+ * Solana Client
+ * Wraps Solana Connection setup and Anchor program initialization.
+ * Provides the injectable SvmClientFactory interface for testability.
+ */
+
+import { AnchorProvider, Program, Wallet } from '@coral-xyz/anchor';
+import { Connection } from '@solana/web3.js';
+
+import { getPortalIdl } from '@/commons/idls/portal.idl';
+
+import { SVM_CONNECTION_CONFIG, SVM_PROVIDER_CONFIG } from './svm-constants';
+import { AnchorSetupResult, PublishContext } from './svm-types';
+
+export interface SvmClientFactory {
+ createConnection(rpcUrl: string): Connection;
+}
+
+export class DefaultSvmClientFactory implements SvmClientFactory {
+ createConnection(rpcUrl: string): Connection {
+ return new Connection(rpcUrl, SVM_CONNECTION_CONFIG);
+ }
+}
+
+/**
+ * Sets up Anchor provider and program for Solana interactions.
+ */
+export function setupAnchorProgram(
+ connection: Connection,
+ context: PublishContext
+): AnchorSetupResult {
+ const wallet = new Wallet(context.keypair);
+ const provider = new AnchorProvider(connection, wallet, SVM_PROVIDER_CONFIG);
+
+ const idl = getPortalIdl(context.portalProgramId.toBase58());
+ const program = new Program(idl, provider);
+
+ return { program, provider };
+}
diff --git a/src/blockchain/svm/svm-chain-handler.ts b/src/blockchain/svm/svm-chain-handler.ts
new file mode 100644
index 0000000..4a11b69
--- /dev/null
+++ b/src/blockchain/svm/svm-chain-handler.ts
@@ -0,0 +1,29 @@
+import type { ChainHandler } from '@/blockchain/chain-handler.interface';
+import { AddressNormalizer } from '@/blockchain/utils/address-normalizer';
+import { SvmAddressSchema } from '@/blockchain/validation';
+import { RoutesCliError } from '@/shared/errors';
+import { BlockchainAddress, ChainType, SvmAddress, UniversalAddress } from '@/shared/types';
+
+export class SvmChainHandler implements ChainHandler {
+ readonly chainType = ChainType.SVM;
+
+ validateAddress(address: string): boolean {
+ return SvmAddressSchema.safeParse(address).success;
+ }
+
+ normalize(address: string): UniversalAddress {
+ const result = SvmAddressSchema.safeParse(address);
+ if (!result.success) {
+ throw RoutesCliError.invalidAddress(address, 'SVM');
+ }
+ return AddressNormalizer.normalizeSvm(address as SvmAddress);
+ }
+
+ denormalize(address: UniversalAddress): BlockchainAddress {
+ return AddressNormalizer.denormalizeToSvm(address);
+ }
+
+ getAddressFormat(): string {
+ return 'Base58 public key, 32–44 characters (e.g., So11111111111111111111111111111111111111112)';
+ }
+}
diff --git a/src/blockchain/svm/svm-client-factory.ts b/src/blockchain/svm/svm-client-factory.ts
new file mode 100644
index 0000000..d36d33e
--- /dev/null
+++ b/src/blockchain/svm/svm-client-factory.ts
@@ -0,0 +1,5 @@
+/**
+ * @deprecated Import directly from './solana-client' for new code.
+ * Re-exported here for backward compatibility with publisher-factory.ts and test mocks.
+ */
+export { DefaultSvmClientFactory, SvmClientFactory } from './solana-client';
diff --git a/src/blockchain/svm/svm-decode.ts b/src/blockchain/svm/svm-decode.ts
index 8c27a1e..e060c3f 100644
--- a/src/blockchain/svm/svm-decode.ts
+++ b/src/blockchain/svm/svm-decode.ts
@@ -36,8 +36,9 @@ export async function decodeTransactionLogs(
const events = parseEventsFromLogs(logs, program);
return events;
- } catch (error: any) {
- logger.warn(`Failed to decode transaction logs: ${error.message}`);
+ } catch (error: unknown) {
+ const message = error instanceof Error ? error.message : String(error);
+ logger.warn(`Failed to decode transaction logs: ${message}`);
// Return empty array if decoding fails - this is non-critical
return [];
}
@@ -61,8 +62,9 @@ function parseEventsFromLogs(logs: string[], program: Program): DecodedEvent[] {
}
return events;
- } catch (error: any) {
- logger.warn(`Failed to parse events from logs: ${error.message}`);
+ } catch (error: unknown) {
+ const message = error instanceof Error ? error.message : String(error);
+ logger.warn(`Failed to parse events from logs: ${message}`);
return [];
}
}
@@ -88,44 +90,64 @@ export async function extractIntentPublishedEvent(
// Transform the event data to our format
return transformIntentPublishedEvent(intentPublishedEvent.data);
- } catch (error: any) {
- logger.warn(`Failed to extract IntentPublished event: ${error.message}`);
+ } catch (error: unknown) {
+ const message = error instanceof Error ? error.message : String(error);
+ logger.warn(`Failed to extract IntentPublished event: ${message}`);
return null;
}
}
+/** Shape of the raw Anchor IntentPublished event data before transformation. */
+interface RawIntentPublishedData {
+ intentHash?: number[] | string;
+ destination?: { toString(): string } | string | number;
+ route?: number[] | string;
+ reward?: {
+ deadline?: { toString(): string };
+ creator?: { toString(): string };
+ prover?: { toString(): string };
+ nativeAmount?: { toString(): string };
+ tokens?: Array<{
+ token?: { toString(): string };
+ amount?: { toString(): string };
+ }>;
+ };
+}
+
/**
* Transforms raw event data to DecodedIntentPublished format
*/
-function transformIntentPublishedEvent(eventData: any): DecodedIntentPublished {
+function transformIntentPublishedEvent(eventData: Record): DecodedIntentPublished {
try {
+ const data = eventData as RawIntentPublishedData;
+
// Convert intent_hash array to hex string if it's an array
- const intentHash = Array.isArray(eventData.intentHash)
- ? bufferToHex(Buffer.from(eventData.intentHash))
- : eventData.intentHash;
+ const intentHash = Array.isArray(data.intentHash)
+ ? bufferToHex(Buffer.from(data.intentHash as number[]))
+ : (data.intentHash as string) || '';
// Convert route bytes to hex string if it's an array
- const route = Array.isArray(eventData.route)
- ? bufferToHex(Buffer.from(eventData.route))
- : eventData.route;
+ const route = Array.isArray(data.route)
+ ? bufferToHex(Buffer.from(data.route as number[]))
+ : (data.route as string) || '';
return {
intentHash,
- destination: eventData.destination?.toString() || '',
+ destination: data.destination?.toString() || '',
route,
reward: {
- deadline: eventData.reward?.deadline?.toString() || '0',
- creator: eventData.reward?.creator?.toString() || '',
- prover: eventData.reward?.prover?.toString() || '',
- nativeAmount: eventData.reward?.nativeAmount?.toString() || '0',
+ deadline: data.reward?.deadline?.toString() || '0',
+ creator: data.reward?.creator?.toString() || '',
+ prover: data.reward?.prover?.toString() || '',
+ nativeAmount: data.reward?.nativeAmount?.toString() || '0',
tokens:
- eventData.reward?.tokens?.map((token: any) => ({
+ data.reward?.tokens?.map(token => ({
token: token.token?.toString() || '',
amount: token.amount?.toString() || '0',
})) || [],
},
};
- } catch (error: any) {
+ } catch (error: unknown) {
throw new SvmError(
SvmErrorType.TRANSACTION_FAILED,
'Failed to transform IntentPublished event',
@@ -137,18 +159,19 @@ function transformIntentPublishedEvent(eventData: any): DecodedIntentPublished {
/**
* Decodes instruction data from a transaction
*/
-export async function decodeInstructionData(
- program: Program,
- instructionData: Buffer,
+export function decodeInstructionData(
+ _program: Program,
+ _instructionData: Buffer,
instructionName: string
-): Promise {
+): { name: string } | null {
try {
// For now, we'll just log the instruction name
// Actual decoding would require the instruction discriminator
logger.info(`Processing ${instructionName} instruction`);
return { name: instructionName };
- } catch (error: any) {
- logger.warn(`Failed to decode instruction data: ${error.message}`);
+ } catch (error: unknown) {
+ const message = error instanceof Error ? error.message : String(error);
+ logger.warn(`Failed to decode instruction data: ${message}`);
return null;
}
}
@@ -189,8 +212,8 @@ export async function logTransactionDetails(
logger.info(` Event ${index + 1}: ${event.name}`);
if ('intentHash' in event.data)
- event.data.intentHash = arrayToHex(event.data.intentHash[0]);
- if ('route' in event.data) event.data.route = bufferToHex(event.data.route);
+ event.data.intentHash = arrayToHex((event.data.intentHash as number[][])[0]);
+ if ('route' in event.data) event.data.route = bufferToHex(event.data.route as Buffer);
logger.info(` Data: ${JSON.stringify(event.data, null, 2)}`);
});
@@ -200,7 +223,8 @@ export async function logTransactionDetails(
if (parsedTx.meta?.err) {
logger.error(`Transaction Error: ${JSON.stringify(parsedTx.meta.err)}`);
}
- } catch (error: any) {
- logger.warn(`Failed to log transaction details: ${error.message}`);
+ } catch (error: unknown) {
+ const message = error instanceof Error ? error.message : String(error);
+ logger.warn(`Failed to log transaction details: ${message}`);
}
}
diff --git a/src/blockchain/svm/svm-types.ts b/src/blockchain/svm/svm-types.ts
index 3f2bfed..8c0beeb 100644
--- a/src/blockchain/svm/svm-types.ts
+++ b/src/blockchain/svm/svm-types.ts
@@ -3,11 +3,12 @@
* Provides type safety and clear interfaces for Solana-specific operations
*/
-import { BN } from '@coral-xyz/anchor';
-import { Commitment, Keypair, PublicKey } from '@solana/web3.js';
+import { AnchorProvider, BN, Program } from '@coral-xyz/anchor';
+import { Commitment, Keypair, PublicKey, Transaction } from '@solana/web3.js';
import { Hex } from 'viem';
-import { Intent } from '@/core/interfaces/intent';
+import { Intent } from '@/shared/types';
+import { UniversalAddress } from '@/shared/types';
/**
* Solana-specific portal reward format
@@ -67,11 +68,12 @@ export interface PublishContext {
destination: bigint;
reward: Intent['reward'];
encodedRoute: string;
- privateKey: string;
intentHash: string;
routeHash: Hex;
keypair: Keypair;
portalProgramId: PublicKey;
+ /** Optional prover address override — uses reward.prover when absent */
+ proverAddress?: UniversalAddress;
}
/**
@@ -88,15 +90,15 @@ export interface TokenAccountResult {
* Anchor program setup result
*/
export interface AnchorSetupResult {
- program: any; // Program type from Anchor
- provider: any; // AnchorProvider type
+ program: Program;
+ provider: AnchorProvider;
}
/**
* Transaction building result
*/
export interface TransactionBuildResult {
- transaction: any; // Transaction type from Solana
+ transaction: Transaction;
signers: Keypair[];
}
@@ -119,7 +121,7 @@ export class SvmError extends Error {
constructor(
public readonly type: SvmErrorType,
message: string,
- public readonly details?: any
+ public readonly details?: unknown
) {
super(message);
this.name = 'SvmError';
@@ -140,7 +142,7 @@ export interface TransactionSendOptions {
*/
export interface DecodedEvent {
name: string;
- data: any;
+ data: Record;
}
/**
diff --git a/src/blockchain/svm/svm.publisher.ts b/src/blockchain/svm/svm.publisher.ts
new file mode 100644
index 0000000..0f4da67
--- /dev/null
+++ b/src/blockchain/svm/svm.publisher.ts
@@ -0,0 +1,229 @@
+/**
+ * SVM (Solana) Chain Publisher (NestJS injectable)
+ */
+
+import { Injectable } from '@nestjs/common';
+
+import { getAccount, getAssociatedTokenAddressSync } from '@solana/spl-token';
+import { Connection, Keypair, PublicKey } from '@solana/web3.js';
+import { Hex } from 'viem';
+
+import { AddressNormalizer } from '@/blockchain/utils/address-normalizer';
+import { PortalHashUtils } from '@/commons/utils/portal-hash.utils';
+import { KeyHandle } from '@/shared/security';
+import { ChainConfig, ChainType, Intent, UniversalAddress } from '@/shared/types';
+import { logger } from '@/utils/logger';
+
+import { BasePublisher, IntentStatus, PublishResult, ValidationResult } from '../base.publisher';
+import { ChainRegistryService } from '../chain-registry.service';
+import { ChainsService } from '../chains.service';
+
+import { DefaultSvmClientFactory, SvmClientFactory } from './solana-client';
+import { PublishContext, SvmError, SvmErrorType } from './svm-types';
+import { executeFunding } from './transaction-builder';
+
+@Injectable()
+export class SvmPublisher extends BasePublisher {
+ private connection: Connection;
+
+ constructor(
+ rpcUrl: string,
+ registry: ChainRegistryService,
+ private readonly chains: ChainsService,
+ factory: SvmClientFactory = new DefaultSvmClientFactory()
+ ) {
+ super(rpcUrl, registry);
+ this.connection = factory.createConnection(rpcUrl);
+ }
+
+ override async publish(
+ source: bigint,
+ destination: bigint,
+ reward: Intent['reward'],
+ encodedRoute: string,
+ keyHandle: KeyHandle,
+ portalAddress?: UniversalAddress,
+ proverAddress?: UniversalAddress
+ ): Promise {
+ this.runPreflightChecks(source);
+ return keyHandle.useAsync(async rawKey => {
+ const keypair = this.parsePrivateKey(rawKey);
+ return this.runSafely(async () => {
+ const portalProgramId = portalAddress
+ ? new PublicKey(AddressNormalizer.denormalize(portalAddress, ChainType.SVM))
+ : this.getPortalProgramId(source);
+
+ const { intentHash, routeHash } = PortalHashUtils.getIntentHashFromReward(
+ source,
+ destination,
+ encodedRoute as Hex,
+ reward
+ );
+
+ this.logPublishInfo(portalProgramId, keypair, destination);
+
+ const context: PublishContext = {
+ source,
+ destination,
+ reward,
+ encodedRoute,
+ intentHash,
+ routeHash,
+ keypair,
+ portalProgramId,
+ proverAddress,
+ };
+
+ const fundingResult = await this.fundIntent(context);
+
+ if (fundingResult.success) {
+ logger.info(`Funding successful: ${fundingResult.transactionHash!}`);
+ }
+
+ return fundingResult;
+ });
+ });
+ }
+
+ private async fundIntent(context: PublishContext): Promise {
+ if (context.reward.tokens.length === 0) {
+ const errorMsg = 'Cannot fund intent: No reward tokens specified';
+ logger.error(errorMsg);
+ return { success: false, error: errorMsg };
+ }
+
+ try {
+ const fundingResult = await executeFunding(this.connection, context);
+
+ if (!fundingResult.success) {
+ logger.error(`Funding failed: ${fundingResult.error}`);
+ return fundingResult;
+ }
+
+ logger.info(`Funding successful: ${fundingResult.transactionHash!}`);
+ return fundingResult;
+ } catch (error: unknown) {
+ if (error instanceof SvmError) {
+ return { success: false, error: error.message };
+ }
+ throw error;
+ }
+ }
+
+ override async getBalance(address: string, _chainId?: bigint): Promise {
+ try {
+ const publicKey = new PublicKey(address);
+ const balance = await this.connection.getBalance(publicKey);
+ return BigInt(balance);
+ } catch {
+ return 0n;
+ }
+ }
+
+ override async validate(
+ reward: Intent['reward'],
+ senderAddress: string,
+ _chainId: bigint
+ ): Promise {
+ const errors: string[] = [];
+
+ if (reward.nativeAmount > 0n) {
+ const balance = await this.getBalance(senderAddress);
+ if (balance < reward.nativeAmount) {
+ errors.push(
+ `Insufficient SOL balance. Required: ${reward.nativeAmount} lamports, Available: ${balance}`
+ );
+ }
+ }
+
+ const walletPubkey = new PublicKey(senderAddress);
+ for (const token of reward.tokens) {
+ try {
+ const tokenMint = new PublicKey(AddressNormalizer.denormalize(token.token, ChainType.SVM));
+ const ata = getAssociatedTokenAddressSync(tokenMint, walletPubkey);
+ const tokenAccount = await getAccount(this.connection, ata);
+ if (tokenAccount.amount < token.amount) {
+ errors.push(
+ `Insufficient SPL token balance for ${tokenMint}. Required: ${token.amount}, Available: ${tokenAccount.amount}`
+ );
+ }
+ } catch {
+ errors.push(
+ `Could not verify SPL token balance for ${AddressNormalizer.denormalize(token.token, ChainType.SVM)}`
+ );
+ }
+ }
+
+ return { valid: errors.length === 0, errors };
+ }
+
+ protected override handleError(error: unknown): PublishResult {
+ logger.stopSpinner();
+
+ let errorMessage = error instanceof Error ? error.message : String(error);
+
+ if (typeof error === 'object' && error !== null) {
+ const solanaError = error as { logs?: string[]; err?: unknown; details?: unknown };
+ if (solanaError.logs) {
+ errorMessage += `\nLogs: ${solanaError.logs.join('\n')}`;
+ }
+ if (solanaError.err) {
+ errorMessage += `\nError: ${JSON.stringify(solanaError.err)}`;
+ }
+ if (solanaError.details) {
+ errorMessage += `\nDetails: ${JSON.stringify(solanaError.details)}`;
+ }
+ }
+
+ logger.error(`Transaction failed: ${errorMessage}`);
+ return { success: false, error: errorMessage };
+ }
+
+ private parsePrivateKey(privateKey: string): Keypair {
+ try {
+ if (privateKey.startsWith('[') && privateKey.endsWith(']')) {
+ const bytes = JSON.parse(privateKey);
+ return Keypair.fromSecretKey(new Uint8Array(bytes));
+ }
+
+ if (privateKey.includes(',')) {
+ const bytes = privateKey.split(',').map(b => parseInt(b.trim()));
+ return Keypair.fromSecretKey(new Uint8Array(bytes));
+ }
+
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
+ const bs58 = require('bs58');
+ const bytes = bs58.decode(privateKey);
+ return Keypair.fromSecretKey(bytes);
+ } catch (error: unknown) {
+ throw new SvmError(SvmErrorType.INVALID_CONFIG, 'Invalid private key format', error);
+ }
+ }
+
+ override getStatus(
+ _intentHash: string,
+ _chain: ChainConfig,
+ _portalAddress?: UniversalAddress
+ ): Promise {
+ return Promise.reject(new Error('getStatus not yet implemented for SVM'));
+ }
+
+ private getPortalProgramId(chainId: bigint): PublicKey {
+ const chainConfig = this.chains.findChainById(chainId);
+
+ if (!chainConfig?.portalAddress) {
+ throw new SvmError(
+ SvmErrorType.INVALID_CONFIG,
+ `No Portal address configured for chain ${chainId}`
+ );
+ }
+
+ return new PublicKey(AddressNormalizer.denormalize(chainConfig.portalAddress, ChainType.SVM));
+ }
+
+ private logPublishInfo(portalProgramId: PublicKey, keypair: Keypair, destination: bigint): void {
+ logger.info(`Using Portal Program: ${portalProgramId.toString()}`);
+ logger.info(`Creator: ${keypair.publicKey.toString()}`);
+ logger.info(`Destination Chain: ${destination}`);
+ }
+}
diff --git a/src/blockchain/svm/svm-transaction.ts b/src/blockchain/svm/transaction-builder.ts
similarity index 70%
rename from src/blockchain/svm/svm-transaction.ts
rename to src/blockchain/svm/transaction-builder.ts
index abf9219..1b08f2b 100644
--- a/src/blockchain/svm/svm-transaction.ts
+++ b/src/blockchain/svm/transaction-builder.ts
@@ -1,58 +1,37 @@
/**
- * SVM (Solana) Transaction Building and Management
- * Handles transaction construction, sending, and confirmation for Solana
+ * SVM Transaction Builder
+ * Builds and executes Solana transactions for the Portal program.
+ * Depends on solana-client.ts (program setup) and pda-manager.ts (PDA derivations).
*/
-import { AnchorProvider, BN, Program, Wallet } from '@coral-xyz/anchor';
+import { BN, Program } from '@coral-xyz/anchor';
import { getAssociatedTokenAddress } from '@solana/spl-token';
import { Connection, Keypair, PublicKey, Transaction } from '@solana/web3.js';
-import { getPortalIdl } from '@/commons/idls/portal.idl';
-import { PortalIdl } from '@/commons/types/portal-idl.type';
-import { ChainType, Intent } from '@/core/interfaces/intent';
-import { AddressNormalizer } from '@/core/utils/address-normalizer';
+import { AddressNormalizer } from '@/blockchain/utils/address-normalizer';
+import { ChainType, Intent } from '@/shared/types';
import { logger } from '@/utils/logger';
-import { PublishResult } from '../base-publisher';
+import { PublishResult } from '../base.publisher';
-import { createPdaSeedBuffer, hexToArray, hexToBuffer } from './svm-buffer-utils';
-import {
- SVM_CONFIRMATION_CONFIG,
- SVM_ERROR_MESSAGES,
- SVM_LOG_MESSAGES,
- SVM_PDA_SEEDS,
- SVM_PROVIDER_CONFIG,
-} from './svm-constants';
+import { calculateVaultPDA } from './pda-manager';
+import { setupAnchorProgram } from './solana-client';
+import { hexToArray, hexToBuffer } from './svm-buffer-utils';
+import { SVM_CONFIRMATION_CONFIG, SVM_ERROR_MESSAGES, SVM_LOG_MESSAGES } from './svm-constants';
import { extractIntentPublishedEvent, logTransactionDetails } from './svm-decode';
import { prepareTokenTransferAccounts } from './svm-token-operations';
-import {
- AnchorSetupResult,
- PublishContext,
- SvmError,
- SvmErrorType,
- TransactionResultWithDecoding,
-} from './svm-types';
+import { PublishContext, SvmError, SvmErrorType, TransactionResultWithDecoding } from './svm-types';
/**
- * Sets up Anchor provider and program for Solana interactions
+ * Converts Intent reward to Solana-specific format.
*/
-export function setupAnchorProgram(
- connection: Connection,
- context: PublishContext
-): AnchorSetupResult {
- const wallet = new Wallet(context.keypair);
- const provider = new AnchorProvider(connection, wallet, SVM_PROVIDER_CONFIG);
-
- const idl = getPortalIdl(context.portalProgramId.toBase58());
- const program = new Program(idl, provider);
-
- return { program, provider };
-}
-
-/**
- * Converts Intent reward to Solana-specific format
- */
-export function buildPortalReward(reward: Intent['reward']) {
+export function buildPortalReward(reward: Intent['reward']): {
+ deadline: BN;
+ creator: PublicKey;
+ prover: PublicKey;
+ nativeAmount: BN;
+ tokens: { token: PublicKey; amount: BN }[];
+} {
return {
deadline: new BN(reward.deadline),
creator: new PublicKey(AddressNormalizer.denormalize(reward.creator, ChainType.SVM)),
@@ -66,20 +45,7 @@ export function buildPortalReward(reward: Intent['reward']) {
}
/**
- * Calculates the vault PDA for an intent
- */
-export function calculateVaultPDA(intentHash: string, portalProgramId: PublicKey): PublicKey {
- const intentHashBytes = hexToBuffer(intentHash);
- const [vaultPda] = PublicKey.findProgramAddressSync(
- [createPdaSeedBuffer(SVM_PDA_SEEDS.VAULT), intentHashBytes],
- portalProgramId
- );
-
- return vaultPda;
-}
-
-/**
- * Builds a publish transaction for Solana
+ * Builds a publish transaction for Solana.
*/
export async function buildPublishTransaction(
program: Program,
@@ -103,38 +69,30 @@ export async function buildPublishTransaction(
}
/**
- * Builds a funding transaction for Solana
+ * Builds a funding transaction for Solana.
*/
export async function buildFundingTransaction(
- connection: Connection,
- program: Program,
+ _connection: Connection,
+ program: Program,
context: PublishContext
): Promise {
if (context.reward.tokens.length === 0) {
throw new SvmError(SvmErrorType.INVALID_CONFIG, SVM_ERROR_MESSAGES.NO_REWARD_TOKENS);
}
- // Calculate vault PDA
const vaultPda = calculateVaultPDA(context.intentHash, context.portalProgramId);
logger.info(SVM_LOG_MESSAGES.VAULT_PDA(vaultPda.toString()));
- // Get token mint and accounts
const tokenMint = new PublicKey(
AddressNormalizer.denormalizeToSvm(context.reward.tokens[0].token)
);
const funderTokenAccount = await getAssociatedTokenAddress(tokenMint, context.keypair.publicKey);
-
- // Get vault token account address (must already exist)
const vaultTokenAccount = await getAssociatedTokenAddress(
tokenMint,
vaultPda,
true // allowOwnerOffCurve for PDA
);
- // Build portal reward
- // const portalReward = buildPortalReward(context.reward);
-
- // Prepare token transfer accounts
const tokenTransferAccounts = prepareTokenTransferAccounts(
funderTokenAccount,
vaultTokenAccount,
@@ -143,7 +101,6 @@ export async function buildFundingTransaction(
logger.info(SVM_LOG_MESSAGES.BUILD_FUNDING_TX);
- // Build the funding transaction
const transaction = await program.methods
.fund({
destination: new BN(context.destination),
@@ -151,7 +108,9 @@ export async function buildFundingTransaction(
reward: {
deadline: new BN(context.reward.deadline),
creator: new PublicKey(AddressNormalizer.denormalizeToSvm(context.reward.creator)),
- prover: new PublicKey(AddressNormalizer.denormalizeToSvm(context.reward.prover)),
+ prover: new PublicKey(
+ AddressNormalizer.denormalizeToSvm(context.proverAddress ?? context.reward.prover)
+ ),
nativeAmount: new BN(context.reward.nativeAmount),
tokens: context.reward.tokens.map(token => ({
token: new PublicKey(AddressNormalizer.denormalizeToSvm(token.token)),
@@ -168,16 +127,11 @@ export async function buildFundingTransaction(
.remainingAccounts(tokenTransferAccounts)
.transaction();
- // Fix route hash encoding in instruction data
- // const instructionData = Buffer.from(transaction.instructions[0].data);
- // copyBufferAt(hexToBuffer(context.routeHash), instructionData, 16);
- // transaction.instructions[0].data = instructionData;
-
return transaction;
}
/**
- * Sends and confirms a transaction on Solana
+ * Sends and confirms a transaction on Solana.
*/
export async function sendAndConfirmTransaction(
connection: Connection,
@@ -197,42 +151,34 @@ export async function sendAndConfirmTransaction(
logger.info(SVM_LOG_MESSAGES.TX_SIGNATURE(signature));
- // Wait for confirmation
await waitForTransactionConfirmation(connection, signature);
- // Decode transaction data if program is provided
const result: TransactionResultWithDecoding = { signature };
if (program) {
try {
- // Log detailed transaction information
await logTransactionDetails(connection, signature, program);
-
- // Extract IntentPublished event if present
const intentPublished = await extractIntentPublishedEvent(connection, signature, program);
if (intentPublished) {
result.intentPublished = intentPublished;
logger.info(`Decoded IntentPublished event: ${JSON.stringify(intentPublished, null, 2)}`);
}
- } catch (decodeError: any) {
- // Decoding is non-critical, log but don't fail
- logger.warn(`Failed to decode transaction events: ${decodeError.message}`);
+ } catch (decodeError: unknown) {
+ const message = decodeError instanceof Error ? decodeError.message : String(decodeError);
+ logger.warn(`Failed to decode transaction events: ${message}`);
}
}
return result;
- } catch (error: any) {
+ } catch (error: unknown) {
logger.stopSpinner();
- throw new SvmError(
- SvmErrorType.TRANSACTION_FAILED,
- `Transaction failed: ${error.message}`,
- error
- );
+ const message = error instanceof Error ? error.message : String(error);
+ throw new SvmError(SvmErrorType.TRANSACTION_FAILED, `Transaction failed: ${message}`, error);
}
}
/**
- * Waits for transaction confirmation with improved error handling
+ * Waits for transaction confirmation with improved error handling.
*/
export async function waitForTransactionConfirmation(
connection: Connection,
@@ -282,7 +228,7 @@ export async function waitForTransactionConfirmation(
}
/**
- * Executes a funding operation for an intent
+ * Executes a funding operation for an intent.
*/
export async function executeFunding(
connection: Connection,
@@ -308,7 +254,7 @@ export async function executeFunding(
transactionHash: result.signature,
intentHash: context.intentHash,
};
- } catch (error: any) {
+ } catch (error: unknown) {
logger.stopSpinner();
if (error instanceof SvmError) {
@@ -320,7 +266,7 @@ export async function executeFunding(
}
/**
- * Executes a publish operation for an intent
+ * Executes a publish operation for an intent.
*/
export async function executePublish(
connection: Connection,
@@ -341,7 +287,6 @@ export async function executePublish(
logger.succeed(SVM_LOG_MESSAGES.PUBLISH_SUCCESS);
- // Log decoded event data if available
if (result.intentPublished) {
logger.info('Intent Published Successfully with data:');
logger.info(` Intent Hash: ${result.intentPublished.intentHash}`);
@@ -356,7 +301,7 @@ export async function executePublish(
intentHash: context.intentHash,
decodedData: result.intentPublished,
};
- } catch (error: any) {
+ } catch (error: unknown) {
logger.stopSpinner();
if (error instanceof SvmError) {
diff --git a/src/blockchain/tvm-publisher.ts b/src/blockchain/tvm-publisher.ts
deleted file mode 100644
index 34d7c40..0000000
--- a/src/blockchain/tvm-publisher.ts
+++ /dev/null
@@ -1,167 +0,0 @@
-/**
- * TVM (Tron) Chain Publisher
- */
-
-import { TronWeb } from 'tronweb';
-import { erc20Abi, Hex } from 'viem';
-
-import { portalAbi } from '@/commons/abis/portal.abi';
-import { PortalHashUtils } from '@/commons/utils/portal-hash.utils';
-import { getChainById } from '@/config/chains';
-import { ChainType, Intent } from '@/core/interfaces/intent';
-import { UniversalAddress } from '@/core/types/universal-address';
-import { AddressNormalizer } from '@/core/utils/address-normalizer';
-import { logger } from '@/utils/logger';
-
-import { BasePublisher, PublishResult } from './base-publisher';
-
-export class TvmPublisher extends BasePublisher {
- private tronWeb: TronWeb;
-
- constructor(rpcUrl: string) {
- super(rpcUrl);
- this.tronWeb = new TronWeb({
- fullHost: rpcUrl,
- });
- }
-
- async publish(
- source: bigint,
- destination: bigint,
- reward: Intent['reward'],
- encodedRoute: string,
- privateKey: string,
- _portalAddress?: UniversalAddress
- ): Promise {
- try {
- // Set private key
- this.tronWeb.setPrivateKey(privateKey);
- const senderAddress = this.tronWeb.address.fromPrivateKey(privateKey);
-
- // Get Portal address
- const chainConfig = getChainById(source);
- const portalAddrUniversal = _portalAddress ?? chainConfig?.portalAddress;
- if (!portalAddrUniversal) {
- throw new Error(`No Portal address configured for chain ${source}`);
- }
- const portalAddress = AddressNormalizer.denormalize(portalAddrUniversal, ChainType.TVM);
-
- // Encode route for destination chain type
- const destChainConfig = getChainById(BigInt(destination));
- if (!destChainConfig) {
- throw new Error(`Unknown destination chain: ${destination}`);
- }
-
- // Get Portal contract with ABI
- const sourceToken = reward.tokens[0];
- const tokenContract = this.tronWeb.contract(
- erc20Abi,
- AddressNormalizer.denormalizeToTvm(sourceToken.token)
- );
-
- logger.spinner('Approving tokens...');
-
- const approvalTxId = await tokenContract
- .approve(portalAddress, sourceToken.amount)
- .send({ from: senderAddress });
-
- logger.updateSpinner('Waiting for approval confirmation...');
-
- const approvalSuccessful = await this.waitForTransaction(approvalTxId);
-
- if (!approvalSuccessful) {
- logger.fail('Token approval failed');
- throw new Error('Approval failed');
- }
-
- logger.succeed('Tokens approved');
-
- const portalContract = this.tronWeb.contract(portalAbi, portalAddress);
-
- // Prepare parameters - TronWeb expects strings for numbers
- const tvmReward: Parameters[0][2] = [
- reward.deadline,
- AddressNormalizer.denormalize(reward.creator, ChainType.TVM),
- AddressNormalizer.denormalize(reward.prover, ChainType.TVM),
- reward.nativeAmount,
- reward.tokens.map(
- t => [AddressNormalizer.denormalize(t.token, ChainType.TVM), t.amount] as const
- ),
- ];
-
- // Call publish function
- // Pass parameters as separate arguments
- logger.spinner('Publishing intent to Portal contract...');
- const tx = await portalContract
- .publishAndFund(destination, encodedRoute, tvmReward, false)
- .send({
- from: senderAddress,
- callValue: Number(reward.nativeAmount), // TRX amount in sun
- });
-
- logger.updateSpinner('Waiting for transaction confirmation...');
-
- const { intentHash } = PortalHashUtils.getIntentHashFromReward(
- destination,
- source,
- encodedRoute as Hex,
- reward
- );
-
- if (tx) {
- logger.succeed('Transaction confirmed');
- return {
- success: true,
- transactionHash: tx,
- intentHash,
- };
- } else {
- logger.fail('Transaction failed');
- return {
- success: false,
- error: 'Transaction failed',
- };
- }
- } catch (error: unknown) {
- logger.stopSpinner();
- const errorMessage = error instanceof Error ? error.message : 'Unknown error';
- return {
- success: false,
- error: errorMessage,
- };
- }
- }
-
- async getBalance(address: string, _chainId?: bigint): Promise {
- try {
- const balance = await this.tronWeb.trx.getBalance(address);
- return BigInt(balance);
- } catch {
- return 0n;
- }
- }
-
- /**
- * Waits for a transaction to be confirmed on the blockchain
- * @param txId - Transaction ID to wait for
- * @returns true if confirmed, false if timeout
- */
- async waitForTransaction(txId: string): Promise {
- for (let i = 0; i < 20; i++) {
- const txInfo = await this.tronWeb.trx.getTransactionInfo(txId);
- if (txInfo && txInfo.blockNumber && txInfo.receipt?.result === 'SUCCESS') {
- return true;
- }
-
- if (txInfo?.receipt?.result === 'FAILED') {
- throw new Error(
- `Transaction failed: ${txInfo.receipt.result || 'Unknown error'}. txId: ${txId}. Received: ${JSON.stringify(txInfo.receipt)}`
- );
- }
-
- // Wait before next attempt
- await new Promise(resolve => setTimeout(resolve, 4_000)); // Wait 4s
- }
- return false;
- }
-}
diff --git a/src/blockchain/tvm/tvm-chain-handler.ts b/src/blockchain/tvm/tvm-chain-handler.ts
new file mode 100644
index 0000000..37b7f4f
--- /dev/null
+++ b/src/blockchain/tvm/tvm-chain-handler.ts
@@ -0,0 +1,29 @@
+import type { ChainHandler } from '@/blockchain/chain-handler.interface';
+import { AddressNormalizer } from '@/blockchain/utils/address-normalizer';
+import { TvmAddressSchema } from '@/blockchain/validation';
+import { RoutesCliError } from '@/shared/errors';
+import { BlockchainAddress, ChainType, TronAddress, UniversalAddress } from '@/shared/types';
+
+export class TvmChainHandler implements ChainHandler {
+ readonly chainType = ChainType.TVM;
+
+ validateAddress(address: string): boolean {
+ return TvmAddressSchema.safeParse(address).success;
+ }
+
+ normalize(address: string): UniversalAddress {
+ const result = TvmAddressSchema.safeParse(address);
+ if (!result.success) {
+ throw RoutesCliError.invalidAddress(address, 'TVM');
+ }
+ return AddressNormalizer.normalizeTvm(address as TronAddress);
+ }
+
+ denormalize(address: UniversalAddress): BlockchainAddress {
+ return AddressNormalizer.denormalizeToTvm(address);
+ }
+
+ getAddressFormat(): string {
+ return 'Base58 starting with T, 34 characters (e.g., TLyqzVGLV1srkB7dToTAEqgDSfPtXRJZYH)';
+ }
+}
diff --git a/src/blockchain/tvm/tvm-client-factory.ts b/src/blockchain/tvm/tvm-client-factory.ts
new file mode 100644
index 0000000..4e8b870
--- /dev/null
+++ b/src/blockchain/tvm/tvm-client-factory.ts
@@ -0,0 +1,18 @@
+/**
+ * TVM Client Factory
+ *
+ * Injectable factory for creating TronWeb instances, enabling dependency injection
+ * in TvmPublisher for testability without live RPC connections.
+ */
+
+import { TronWeb } from 'tronweb';
+
+export interface TvmClientFactory {
+ createClient(rpcUrl: string): TronWeb;
+}
+
+export class DefaultTvmClientFactory implements TvmClientFactory {
+ createClient(rpcUrl: string): TronWeb {
+ return new TronWeb({ fullHost: rpcUrl });
+ }
+}
diff --git a/src/blockchain/tvm/tvm.publisher.ts b/src/blockchain/tvm/tvm.publisher.ts
new file mode 100644
index 0000000..b5a3011
--- /dev/null
+++ b/src/blockchain/tvm/tvm.publisher.ts
@@ -0,0 +1,204 @@
+/**
+ * TVM (Tron) Chain Publisher (NestJS injectable)
+ */
+
+import { Injectable } from '@nestjs/common';
+
+import { TronWeb } from 'tronweb';
+import { erc20Abi, Hex } from 'viem';
+
+import { AddressNormalizer } from '@/blockchain/utils/address-normalizer';
+import { portalAbi } from '@/commons/abis/portal.abi';
+import { PortalHashUtils } from '@/commons/utils/portal-hash.utils';
+import { ErrorCode, RoutesCliError } from '@/shared/errors';
+import { KeyHandle } from '@/shared/security';
+import { ChainConfig, ChainType, Intent, UniversalAddress } from '@/shared/types';
+import { logger } from '@/utils/logger';
+
+import { BasePublisher, IntentStatus, PublishResult, ValidationResult } from '../base.publisher';
+import { ChainRegistryService } from '../chain-registry.service';
+import { ChainsService } from '../chains.service';
+
+import { DefaultTvmClientFactory, TvmClientFactory } from './tvm-client-factory';
+
+@Injectable()
+export class TvmPublisher extends BasePublisher {
+ private readonly factory: TvmClientFactory;
+
+ constructor(
+ rpcUrl: string,
+ registry: ChainRegistryService,
+ private readonly chains: ChainsService,
+ factory: TvmClientFactory = new DefaultTvmClientFactory()
+ ) {
+ super(rpcUrl, registry);
+ this.factory = factory;
+ }
+
+ override async publish(
+ source: bigint,
+ destination: bigint,
+ reward: Intent['reward'],
+ encodedRoute: string,
+ keyHandle: KeyHandle,
+ _portalAddress?: UniversalAddress
+ ): Promise {
+ this.runPreflightChecks(source);
+ return keyHandle.useAsync(async rawKey => {
+ const tronWeb: TronWeb = this.factory.createClient(this.rpcUrl);
+ tronWeb.setPrivateKey(rawKey);
+ const senderAddress = tronWeb.address.fromPrivateKey(rawKey);
+
+ return this.runSafely(async () => {
+ const chainConfig = this.chains.findChainById(source);
+ const portalAddrUniversal = _portalAddress ?? chainConfig?.portalAddress;
+ if (!portalAddrUniversal) {
+ throw new Error(`No Portal address configured for chain ${source}`);
+ }
+ const portalAddress = AddressNormalizer.denormalize(portalAddrUniversal, ChainType.TVM);
+
+ const destChainConfig = this.chains.findChainById(BigInt(destination));
+ if (!destChainConfig) {
+ throw new Error(`Unknown destination chain: ${destination}`);
+ }
+
+ for (const rewardToken of reward.tokens) {
+ const tokenAddress = AddressNormalizer.denormalizeToTvm(rewardToken.token);
+ const tokenContract = tronWeb.contract(erc20Abi, tokenAddress);
+ logger.spinner(`Approving token ${tokenAddress}...`);
+ const approvalTxId = await tokenContract
+ .approve(portalAddress, rewardToken.amount)
+ .send({ from: senderAddress });
+ logger.updateSpinner('Waiting for approval confirmation...');
+ const approved = await this.waitForTransaction(tronWeb, approvalTxId);
+ if (!approved) {
+ throw new RoutesCliError(
+ ErrorCode.TRANSACTION_FAILED,
+ `Approval failed for ${tokenAddress}`
+ );
+ }
+ logger.succeed(`Token approved: ${tokenAddress}`);
+ }
+
+ const portalContract = tronWeb.contract(portalAbi, portalAddress);
+
+ const tvmReward: Parameters[0][2] = [
+ reward.deadline,
+ AddressNormalizer.denormalize(reward.creator, ChainType.TVM),
+ AddressNormalizer.denormalize(reward.prover, ChainType.TVM),
+ reward.nativeAmount,
+ reward.tokens.map(
+ t => [AddressNormalizer.denormalize(t.token, ChainType.TVM), t.amount] as const
+ ),
+ ];
+
+ logger.spinner('Publishing intent to Portal contract...');
+ const tx = await portalContract
+ .publishAndFund(destination, encodedRoute, tvmReward, false)
+ .send({
+ from: senderAddress,
+ callValue: Number(reward.nativeAmount),
+ });
+
+ logger.updateSpinner('Waiting for transaction confirmation...');
+
+ const { intentHash } = PortalHashUtils.getIntentHashFromReward(
+ destination,
+ source,
+ encodedRoute as Hex,
+ reward
+ );
+
+ if (tx) {
+ logger.succeed('Transaction confirmed');
+ return {
+ success: true,
+ transactionHash: tx,
+ intentHash,
+ };
+ } else {
+ logger.fail('Transaction failed');
+ return {
+ success: false,
+ error: 'Transaction failed',
+ };
+ }
+ });
+ });
+ }
+
+ override async getBalance(address: string, _chainId?: bigint): Promise {
+ try {
+ const tronWeb = this.factory.createClient(this.rpcUrl);
+ const balance = await tronWeb.trx.getBalance(address);
+ return BigInt(balance);
+ } catch {
+ return 0n;
+ }
+ }
+
+ override async validate(
+ reward: Intent['reward'],
+ senderAddress: string,
+ _chainId: bigint
+ ): Promise {
+ const errors: string[] = [];
+
+ if (reward.tokens.length === 0) {
+ errors.push('TVM requires at least one reward token');
+ }
+
+ if (reward.nativeAmount > 0n) {
+ const balance = await this.getBalance(senderAddress);
+ if (balance < reward.nativeAmount) {
+ errors.push(
+ `Insufficient TRX balance. Required: ${reward.nativeAmount}, Available: ${balance}`
+ );
+ }
+ }
+
+ const tronWeb = this.factory.createClient(this.rpcUrl);
+ for (const token of reward.tokens) {
+ try {
+ const tokenAddr = AddressNormalizer.denormalizeToTvm(token.token);
+ const contract = tronWeb.contract(erc20Abi, tokenAddr);
+ const balance: bigint = await contract.balanceOf(senderAddress).call();
+ if (BigInt(balance) < token.amount) {
+ errors.push(
+ `Insufficient token balance for ${tokenAddr}. Required: ${token.amount}, Available: ${balance}`
+ );
+ }
+ } catch {
+ // Skip token balance check if contract read fails
+ }
+ }
+
+ return { valid: errors.length === 0, errors };
+ }
+
+ override getStatus(
+ _intentHash: string,
+ _chain: ChainConfig,
+ _portalAddress?: UniversalAddress
+ ): Promise {
+ return Promise.reject(new Error('getStatus not yet implemented for TVM'));
+ }
+
+ private async waitForTransaction(tronWeb: TronWeb, txId: string): Promise {
+ for (let i = 0; i < 20; i++) {
+ const txInfo = await tronWeb.trx.getTransactionInfo(txId);
+ if (txInfo && txInfo.blockNumber && txInfo.receipt?.result === 'SUCCESS') {
+ return true;
+ }
+
+ if (txInfo?.receipt?.result === 'FAILED') {
+ throw new Error(
+ `Transaction failed: ${txInfo.receipt.result || 'Unknown error'}. txId: ${txId}. Received: ${JSON.stringify(txInfo.receipt)}`
+ );
+ }
+
+ await new Promise(resolve => setTimeout(resolve, 4_000));
+ }
+ return false;
+ }
+}
diff --git a/src/blockchain/utils/address-normalizer.ts b/src/blockchain/utils/address-normalizer.ts
new file mode 100644
index 0000000..b492684
--- /dev/null
+++ b/src/blockchain/utils/address-normalizer.ts
@@ -0,0 +1,130 @@
+import { PublicKey } from '@solana/web3.js';
+import { TronWeb } from 'tronweb';
+import { getAddress, isAddress as isViemAddress } from 'viem';
+
+import { getErrorMessage } from '@/commons/utils/error-handler';
+import { RoutesCliError } from '@/shared/errors';
+import { BlockchainAddress, ChainType, EvmAddress, SvmAddress, TronAddress } from '@/shared/types';
+import { padTo32Bytes, UniversalAddress, unpadFrom32Bytes } from '@/shared/types';
+
+export class AddressNormalizer {
+ static normalize(address: BlockchainAddress, chainType: ChainType): UniversalAddress {
+ switch (chainType) {
+ case ChainType.EVM:
+ return AddressNormalizer.normalizeEvm(address as EvmAddress);
+ case ChainType.TVM:
+ return AddressNormalizer.normalizeTvm(address as TronAddress);
+ case ChainType.SVM:
+ return AddressNormalizer.normalizeSvm(address as SvmAddress);
+ default:
+ throw RoutesCliError.unsupportedChain(chainType as string);
+ }
+ }
+
+ static denormalize<
+ chainType extends ChainType,
+ Addr extends chainType extends ChainType.TVM
+ ? TronAddress
+ : chainType extends ChainType.EVM
+ ? EvmAddress
+ : chainType extends ChainType.SVM
+ ? SvmAddress
+ : never,
+ >(address: UniversalAddress, chainType: chainType): Addr {
+ switch (chainType) {
+ case ChainType.EVM:
+ return AddressNormalizer.denormalizeToEvm(address) as Addr;
+ case ChainType.TVM:
+ return AddressNormalizer.denormalizeToTvm(address) as Addr;
+ case ChainType.SVM:
+ return AddressNormalizer.denormalizeToSvm(address) as Addr;
+ default:
+ throw RoutesCliError.unsupportedChain(chainType as string);
+ }
+ }
+
+ static denormalizeToEvm(address: UniversalAddress): EvmAddress {
+ const unpadded = unpadFrom32Bytes(address);
+ const cleanHex = unpadded.substring(2);
+ const evmHex = cleanHex.length > 40 ? cleanHex.substring(cleanHex.length - 40) : cleanHex;
+ const evmAddress = '0x' + evmHex;
+ if (!isViemAddress(evmAddress)) {
+ throw new Error(`Invalid EVM address after denormalization: ${evmAddress}`);
+ }
+ return getAddress(evmAddress);
+ }
+
+ static denormalizeToTvm(address: UniversalAddress): TronAddress {
+ try {
+ const unpadded = unpadFrom32Bytes(address);
+ const hexAddress = unpadded.startsWith('0x41')
+ ? unpadded.substring(2)
+ : '41' + unpadded.substring(2);
+ const base58Address = TronWeb.address.fromHex(hexAddress);
+ if (!TronWeb.isAddress(base58Address)) {
+ throw new Error(`Invalid Tron address after denormalization: ${base58Address}`);
+ }
+ return base58Address as TronAddress;
+ } catch (error) {
+ throw new Error(`Failed to denormalize to TVM address: ${getErrorMessage(error)}`);
+ }
+ }
+
+ static denormalizeToSvm(address: UniversalAddress): SvmAddress {
+ try {
+ const hex = address.startsWith('0x') ? address.slice(2) : address;
+ const bytes = Buffer.from(hex, 'hex');
+ if (bytes.length !== 32) {
+ throw new Error(`Expected 32 bytes, got ${bytes.length}`);
+ }
+ const publicKey = new PublicKey(bytes);
+ return publicKey.toBase58() as SvmAddress;
+ } catch (error) {
+ throw new Error(`Failed to denormalize to SVM address: ${getErrorMessage(error)}`);
+ }
+ }
+
+ static normalizeEvm(address: EvmAddress): UniversalAddress {
+ if (!isViemAddress(address)) {
+ throw RoutesCliError.invalidAddress(address, ChainType.EVM);
+ }
+ const checksummed = getAddress(address);
+ return padTo32Bytes(checksummed) as UniversalAddress;
+ }
+
+ static normalizeTvm(address: TronAddress): UniversalAddress {
+ try {
+ let hexAddress: string;
+ if (address.startsWith('0x')) {
+ const hexTronAddr = address.startsWith('0x41') ? address : '0x41' + address.substring(2);
+ const base58 = TronWeb.address.fromHex(hexTronAddr.substring(2));
+ if (!TronWeb.isAddress(base58)) {
+ throw new Error(`Invalid Tron hex address: ${address}`);
+ }
+ hexAddress = hexTronAddr.toLowerCase();
+ } else {
+ if (!TronWeb.isAddress(address)) {
+ throw new Error(`Invalid Tron base58 address: ${address}`);
+ }
+ const tronHex = TronWeb.address.toHex(address);
+ hexAddress = '0x' + tronHex.toLowerCase();
+ }
+ return padTo32Bytes(hexAddress) as UniversalAddress;
+ } catch (error) {
+ if (error instanceof RoutesCliError) throw error;
+ throw RoutesCliError.invalidAddress(address, ChainType.TVM);
+ }
+ }
+
+ static normalizeSvm(address: SvmAddress | PublicKey): UniversalAddress {
+ try {
+ const publicKey = address instanceof PublicKey ? address : new PublicKey(address);
+ const bytes = publicKey.toBytes();
+ const hex = '0x' + Buffer.from(bytes).toString('hex');
+ return hex as UniversalAddress;
+ } catch {
+ const addrStr = address instanceof PublicKey ? address.toBase58() : String(address);
+ throw RoutesCliError.invalidAddress(addrStr, ChainType.SVM);
+ }
+ }
+}
diff --git a/src/blockchain/utils/portal-encoder.ts b/src/blockchain/utils/portal-encoder.ts
new file mode 100644
index 0000000..f8858dc
--- /dev/null
+++ b/src/blockchain/utils/portal-encoder.ts
@@ -0,0 +1,192 @@
+import { BN, web3 } from '@coral-xyz/anchor';
+import { decodeAbiParameters, encodeAbiParameters, Hex } from 'viem';
+
+import { EVMRewardAbiItem, EVMRouteAbiItem } from '@/commons/abis/portal.abi';
+import { RewardInstruction, RouteInstruction } from '@/commons/types/portal-idl-coder.type';
+import { bufferToBytes } from '@/commons/utils/converter';
+import { toSvmRewardForCoder, toSvmRouteForCoder } from '@/commons/utils/instruments';
+import { portalBorshCoder } from '@/commons/utils/portal-borsh-coder';
+import { ChainType, Intent, UniversalAddress } from '@/shared/types';
+
+import { AddressNormalizer } from './address-normalizer';
+
+export function isRoute(data: Intent['route'] | Intent['reward']): data is Intent['route'] {
+ return 'salt' in data && 'portal' in data && 'calls' in data;
+}
+
+function encodeEvm(data: Intent['route'] | Intent['reward']): Hex {
+ if (isRoute(data)) {
+ return encodeAbiParameters(
+ [EVMRouteAbiItem],
+ [
+ {
+ salt: data.salt,
+ deadline: data.deadline,
+ nativeAmount: data.nativeAmount,
+ portal: AddressNormalizer.denormalizeToEvm(data.portal),
+ tokens: data.tokens.map(t => ({
+ token: AddressNormalizer.denormalizeToEvm(t.token),
+ amount: t.amount,
+ })),
+ calls: data.calls.map(c => ({
+ target: AddressNormalizer.denormalizeToEvm(c.target),
+ data: c.data,
+ value: c.value,
+ })),
+ },
+ ]
+ );
+ } else {
+ return encodeAbiParameters(
+ [EVMRewardAbiItem],
+ [
+ {
+ deadline: data.deadline,
+ creator: AddressNormalizer.denormalizeToEvm(data.creator),
+ prover: AddressNormalizer.denormalizeToEvm(data.prover),
+ nativeAmount: data.nativeAmount,
+ tokens: data.tokens.map(t => ({
+ token: AddressNormalizer.denormalizeToEvm(t.token),
+ amount: t.amount,
+ })),
+ },
+ ]
+ );
+ }
+}
+
+function encodeSvm(data: Intent['route'] | Intent['reward']): Hex {
+ if (isRoute(data)) {
+ return bufferToBytes(
+ portalBorshCoder.types.encode('Route', toSvmRouteForCoder(data))
+ );
+ } else {
+ return bufferToBytes(
+ portalBorshCoder.types.encode('Reward', toSvmRewardForCoder(data))
+ );
+ }
+}
+
+function decodeEvmRoute(hex: Hex): Intent['route'] {
+ const [decoded] = decodeAbiParameters([EVMRouteAbiItem], hex);
+ const d = decoded as {
+ salt: `0x${string}`;
+ deadline: bigint;
+ portal: `0x${string}`;
+ nativeAmount: bigint;
+ tokens: Array<{ token: `0x${string}`; amount: bigint }>;
+ calls: Array<{ target: `0x${string}`; data: `0x${string}`; value: bigint }>;
+ };
+ return {
+ salt: d.salt,
+ deadline: d.deadline,
+ portal: AddressNormalizer.normalizeEvm(d.portal) as UniversalAddress,
+ nativeAmount: d.nativeAmount,
+ tokens: d.tokens.map(t => ({
+ token: AddressNormalizer.normalizeEvm(t.token) as UniversalAddress,
+ amount: t.amount,
+ })),
+ calls: d.calls.map(c => ({
+ target: AddressNormalizer.normalizeEvm(c.target) as UniversalAddress,
+ data: c.data,
+ value: c.value,
+ })),
+ };
+}
+
+function decodeEvmReward(hex: Hex): Intent['reward'] {
+ const [decoded] = decodeAbiParameters([EVMRewardAbiItem], hex);
+ const d = decoded as {
+ deadline: bigint;
+ creator: `0x${string}`;
+ prover: `0x${string}`;
+ nativeAmount: bigint;
+ tokens: Array<{ token: `0x${string}`; amount: bigint }>;
+ };
+ return {
+ deadline: d.deadline,
+ creator: AddressNormalizer.normalizeEvm(d.creator) as UniversalAddress,
+ prover: AddressNormalizer.normalizeEvm(d.prover) as UniversalAddress,
+ nativeAmount: d.nativeAmount,
+ tokens: d.tokens.map(t => ({
+ token: AddressNormalizer.normalizeEvm(t.token) as UniversalAddress,
+ amount: t.amount,
+ })),
+ };
+}
+
+function decodeSvmRoute(hex: Hex): Intent['route'] {
+ const bytes = Buffer.from(hex.slice(2), 'hex');
+ const decoded = portalBorshCoder.types.decode('Route', bytes);
+ const salt = ('0x' + Buffer.from(decoded.salt[0] as number[]).toString('hex')) as Hex;
+ const portalBytes = Buffer.from(decoded.portal[0] as number[]);
+ const portalPk = new web3.PublicKey(portalBytes);
+ return {
+ salt,
+ deadline: BigInt(decoded.deadline.toString()),
+ portal: AddressNormalizer.normalizeSvm(portalPk) as UniversalAddress,
+ nativeAmount: BigInt(decoded.native_amount.toString()),
+ tokens: (decoded.tokens as Array<{ token: web3.PublicKey; amount: BN }>).map(t => ({
+ token: AddressNormalizer.normalizeSvm(t.token) as UniversalAddress,
+ amount: BigInt(t.amount.toString()),
+ })),
+ calls: (decoded.calls as Array<{ target: { 0: number[] }; data: Buffer }>).map(c => ({
+ target: AddressNormalizer.normalizeSvm(
+ new web3.PublicKey(Buffer.from(c.target[0]))
+ ) as UniversalAddress,
+ data: ('0x' + Buffer.from(c.data).toString('hex')) as Hex,
+ value: 0n,
+ })),
+ };
+}
+
+function decodeSvmReward(hex: Hex): Intent['reward'] {
+ const bytes = Buffer.from(hex.slice(2), 'hex');
+ const decoded = portalBorshCoder.types.decode('Reward', bytes);
+ const creatorPk = decoded.creator as web3.PublicKey;
+ const proverPk = decoded.prover as web3.PublicKey;
+ return {
+ deadline: BigInt(decoded.deadline.toString()),
+ creator: AddressNormalizer.normalizeSvm(creatorPk) as UniversalAddress,
+ prover: AddressNormalizer.normalizeSvm(proverPk) as UniversalAddress,
+ nativeAmount: BigInt(decoded.native_amount.toString()),
+ tokens: (decoded.tokens as Array<{ token: web3.PublicKey; amount: BN }>).map(t => ({
+ token: AddressNormalizer.normalizeSvm(t.token) as UniversalAddress,
+ amount: BigInt(t.amount.toString()),
+ })),
+ };
+}
+
+export class PortalEncoder {
+ static isRoute(data: Intent['route'] | Intent['reward']): data is Intent['route'] {
+ return isRoute(data);
+ }
+
+ static encode(data: Intent['route'] | Intent['reward'], chainType: ChainType): Hex {
+ switch (chainType) {
+ case ChainType.EVM:
+ case ChainType.TVM:
+ return encodeEvm(data);
+ case ChainType.SVM:
+ return encodeSvm(data);
+ default:
+ throw new Error(`Unsupported chain type: ${chainType}`);
+ }
+ }
+
+ static decode(hex: Hex, chainType: ChainType, kind: K): Intent[K] {
+ switch (chainType) {
+ case ChainType.EVM:
+ case ChainType.TVM:
+ return kind === 'route'
+ ? (decodeEvmRoute(hex) as Intent[K])
+ : (decodeEvmReward(hex) as Intent[K]);
+ case ChainType.SVM:
+ return kind === 'route'
+ ? (decodeSvmRoute(hex) as Intent[K])
+ : (decodeSvmReward(hex) as Intent[K]);
+ default:
+ throw new Error(`Unsupported chain type: ${chainType}`);
+ }
+ }
+}
diff --git a/src/blockchain/validation/index.ts b/src/blockchain/validation/index.ts
new file mode 100644
index 0000000..47d7270
--- /dev/null
+++ b/src/blockchain/validation/index.ts
@@ -0,0 +1,10 @@
+export {
+ ChainIdSchema,
+ EvmAddressSchema,
+ EvmPrivateKeySchema,
+ SvmAddressSchema,
+ TokenAmountSchema,
+ TvmAddressSchema,
+ TvmPrivateKeySchema,
+ UniversalAddressSchema,
+} from './schemas';
diff --git a/src/blockchain/validation/schemas.ts b/src/blockchain/validation/schemas.ts
new file mode 100644
index 0000000..037c14a
--- /dev/null
+++ b/src/blockchain/validation/schemas.ts
@@ -0,0 +1,43 @@
+import { z } from 'zod';
+
+export const EvmAddressSchema = z.string().regex(/^0x[a-fA-F0-9]{40}$/, {
+ message: 'EVM address must be 0x followed by 40 hex characters',
+});
+
+export const UniversalAddressSchema = z.string().regex(/^0x[a-fA-F0-9]{64}$/, {
+ message: 'Universal address must be 0x followed by 64 hex characters',
+});
+
+// Accepts base58 (T...) or Tron hex (0x41... or 41...) format
+export const TvmAddressSchema = z.union([
+ z.string().regex(/^T[A-Za-z0-9]{33}$/, {
+ message: 'Tron base58 address must start with T and be 34 characters',
+ }),
+ z.string().regex(/^(0x)?41[a-fA-F0-9]{40}$/, {
+ message: 'Tron hex address must start with 41 or 0x41 followed by 40 hex characters',
+ }),
+]);
+
+// Solana base58 public key: 32–44 base58 characters
+export const SvmAddressSchema = z
+ .string()
+ .min(32, { message: 'Solana address must be at least 32 characters' })
+ .max(44, { message: 'Solana address must be at most 44 characters' })
+ .regex(/^[1-9A-HJ-NP-Za-km-z]+$/, {
+ message: 'Solana address must be base58 encoded',
+ });
+
+export const EvmPrivateKeySchema = z.string().regex(/^0x[a-fA-F0-9]{64}$/, {
+ message: 'EVM private key must be 0x followed by 64 hex characters',
+});
+
+export const TvmPrivateKeySchema = z.string().regex(/^[a-fA-F0-9]{64}$/, {
+ message: 'TVM private key must be 64 hex characters (no 0x prefix)',
+});
+
+export const TokenAmountSchema = z
+ .string()
+ .regex(/^\d+(\.\d+)?$/, { message: 'Amount must be a positive number (e.g. "10" or "0.5")' })
+ .refine(v => parseFloat(v) > 0, { message: 'Amount must be greater than zero' });
+
+export const ChainIdSchema = z.bigint().positive({ message: 'Chain ID must be a positive bigint' });
diff --git a/src/cli/cli.module.ts b/src/cli/cli.module.ts
new file mode 100644
index 0000000..b1092d7
--- /dev/null
+++ b/src/cli/cli.module.ts
@@ -0,0 +1,26 @@
+import { Module } from '@nestjs/common';
+
+import { IntentModule } from '@/intent/intent.module';
+import { QuoteModule } from '@/quote/quote.module';
+import { StatusModule } from '@/status/status.module';
+
+import { ChainsCommand } from './commands/chains.command';
+import { ConfigCommand } from './commands/config.command';
+import { PublishCommand } from './commands/publish.command';
+import { StatusCommand } from './commands/status.command';
+import { TokensCommand } from './commands/tokens.command';
+import { DisplayModule } from './services/display.module';
+import { PromptService } from './services/prompt.service';
+
+@Module({
+ imports: [QuoteModule, IntentModule, StatusModule, DisplayModule],
+ providers: [
+ PromptService,
+ PublishCommand,
+ StatusCommand,
+ ConfigCommand,
+ ChainsCommand,
+ TokensCommand,
+ ],
+})
+export class CliModule {}
diff --git a/src/cli/commands/chains.command.ts b/src/cli/commands/chains.command.ts
new file mode 100644
index 0000000..c5813f0
--- /dev/null
+++ b/src/cli/commands/chains.command.ts
@@ -0,0 +1,23 @@
+import { Injectable } from '@nestjs/common';
+
+import { Command, CommandRunner } from 'nest-commander';
+
+import { ChainsService } from '@/blockchain/chains.service';
+
+import { DisplayService } from '../services/display.service';
+
+@Injectable()
+@Command({ name: 'chains', description: 'List supported chains' })
+export class ChainsCommand extends CommandRunner {
+ constructor(
+ private readonly chains: ChainsService,
+ private readonly display: DisplayService
+ ) {
+ super();
+ }
+
+ run(): Promise {
+ this.display.displayChains(this.chains.listChains());
+ return Promise.resolve();
+ }
+}
diff --git a/src/cli/commands/config.command.ts b/src/cli/commands/config.command.ts
new file mode 100644
index 0000000..d3f5c2b
--- /dev/null
+++ b/src/cli/commands/config.command.ts
@@ -0,0 +1,416 @@
+import * as fs from 'fs';
+import * as os from 'os';
+import * as path from 'path';
+
+import { Injectable } from '@nestjs/common';
+
+import { Command, CommandRunner, Option } from 'nest-commander';
+
+import { ConfigService } from '@/config/config.service';
+import { ChainType } from '@/shared/types';
+
+import { PromptService } from '../services/prompt.service';
+
+interface ConfigSettings {
+ defaultSourceChain?: string;
+ defaultDestinationChain?: string;
+ defaultPrivateKeys?: {
+ [ChainType.EVM]?: string;
+ [ChainType.TVM]?: string;
+ [ChainType.SVM]?: string;
+ };
+ rpcUrls?: Record;
+ profiles?: Record;
+ currentProfile?: string;
+}
+
+interface ConfigOptions {
+ interactive?: boolean;
+ profile?: string;
+ force?: boolean;
+}
+
+const CONFIG_DIR = path.join(os.homedir(), '.eco-routes');
+const CONFIG_FILE = path.join(CONFIG_DIR, 'config.json');
+const PROFILES_DIR = path.join(CONFIG_DIR, 'profiles');
+
+@Injectable()
+@Command({
+ name: 'config',
+ description: 'Manage CLI configuration settings',
+ arguments: '[subcommand] [key] [value]',
+})
+export class ConfigCommand extends CommandRunner {
+ constructor(
+ private readonly configService: ConfigService,
+ private readonly prompt: PromptService
+ ) {
+ super();
+ }
+
+ async run(inputs: string[], options: ConfigOptions): Promise {
+ const [subcommand, key, value] = inputs;
+
+ switch (subcommand) {
+ case 'list':
+ this.runList(options.profile);
+ break;
+ case 'set':
+ await this.runSet(key, value, options);
+ break;
+ case 'get':
+ this.runGet(key, options.profile);
+ break;
+ case 'unset':
+ this.runUnset(key, options.profile);
+ break;
+ case 'reset':
+ await this.runReset(options);
+ break;
+ case 'profile':
+ await this.runProfile(key, value, options);
+ break;
+ default:
+ console.log('Usage: config [args]');
+ console.log(' list Show current configuration');
+ console.log(' set Set a configuration value');
+ console.log(' set --interactive Interactive guided setup');
+ console.log(' get Get a configuration value');
+ console.log(' unset Remove a configuration key');
+ console.log(' reset Reset configuration to defaults');
+ console.log(' profile [name]');
+ }
+ }
+
+ private runList(profileName?: string): void {
+ const config = this.loadConfig();
+ if (profileName) {
+ if (!config.profiles?.[profileName]) {
+ console.error(`Profile '${profileName}' not found`);
+ process.exit(1);
+ }
+ console.log(`📋 Profile: ${profileName}`);
+ this.displayConfig(config.profiles[profileName]);
+ } else {
+ console.log('📋 Current Configuration');
+ if (config.currentProfile) console.log(`Active Profile: ${config.currentProfile}\n`);
+ this.displayConfig(config);
+ if (config.profiles && Object.keys(config.profiles).length > 0) {
+ console.log('\nAvailable Profiles:');
+ for (const name of Object.keys(config.profiles)) {
+ console.log(` • ${name}${name === config.currentProfile ? ' (active)' : ''}`);
+ }
+ }
+ }
+ }
+
+ private async runSet(
+ key: string | undefined,
+ value: string | undefined,
+ options: ConfigOptions
+ ): Promise {
+ if (options.interactive || (!key && !value)) {
+ await this.setConfigInteractive(options.profile);
+ } else if (key && value !== undefined) {
+ this.setConfigValue(key, value, options.profile);
+ } else {
+ console.error('Please provide both key and value, or use --interactive mode');
+ process.exit(1);
+ }
+ }
+
+ private runGet(key: string | undefined, profileName?: string): void {
+ if (!key) {
+ console.error('Key is required');
+ process.exit(1);
+ }
+ const config = this.loadConfig();
+ const target = profileName ? (config.profiles?.[profileName] ?? {}) : config;
+ const val = this.getNestedValue(target, key);
+ if (val !== undefined) {
+ console.log(key.toLowerCase().includes('private') ? '***[HIDDEN]***' : String(val));
+ } else {
+ console.warn(`Configuration key '${key}' not found`);
+ process.exit(1);
+ }
+ }
+
+ private runUnset(key: string | undefined, profileName?: string): void {
+ if (!key) {
+ console.error('Key is required');
+ process.exit(1);
+ }
+ this.unsetConfigValue(key, profileName);
+ console.log(`Configuration key '${key}' removed`);
+ }
+
+ private async runReset(options: ConfigOptions): Promise {
+ if (!options.force) {
+ const target = options.profile ? `profile '${options.profile}'` : 'entire configuration';
+ const ok = await this.prompt.confirm(`Reset ${target}?`);
+ if (!ok) {
+ console.log('Reset cancelled');
+ return;
+ }
+ }
+ this.resetConfig(options.profile);
+ console.log(options.profile ? `Profile '${options.profile}' reset` : 'Configuration reset');
+ }
+
+ private async runProfile(
+ op: string | undefined,
+ name: string | undefined,
+ options: ConfigOptions
+ ): Promise {
+ switch (op) {
+ case 'create':
+ if (!name) {
+ console.error('Profile name is required');
+ process.exit(1);
+ }
+ this.createProfile(name);
+ console.log(`Profile '${name}' created`);
+ break;
+ case 'switch':
+ if (!name) {
+ console.error('Profile name is required');
+ process.exit(1);
+ }
+ this.switchProfile(name);
+ console.log(`Switched to profile '${name}'`);
+ break;
+ case 'delete':
+ if (!name) {
+ console.error('Profile name is required');
+ process.exit(1);
+ }
+ if (!options.force) {
+ const ok = await this.prompt.confirm(`Delete profile '${name}'?`);
+ if (!ok) {
+ console.log('Cancelled');
+ return;
+ }
+ }
+ this.deleteProfile(name);
+ console.log(`Profile '${name}' deleted`);
+ break;
+ case 'list': {
+ const config = this.loadConfig();
+ if (!config.profiles || Object.keys(config.profiles).length === 0) {
+ console.log('No profiles found');
+ return;
+ }
+ console.log('📋 Available Profiles:');
+ for (const n of Object.keys(config.profiles)) {
+ console.log(` • ${n}${n === config.currentProfile ? ' (active)' : ''}`);
+ }
+ break;
+ }
+ default:
+ console.log('Usage: config profile [name]');
+ }
+ }
+
+ private async setConfigInteractive(profileName?: string): Promise {
+ const config = this.loadConfig();
+ const target: ConfigSettings = profileName ? (config.profiles?.[profileName] ?? {}) : config;
+ const envConfig = this.configService;
+
+ const { inquirer } = await import('inquirer').then(m => ({ inquirer: m.default }));
+ const answers = await inquirer.prompt([
+ {
+ type: 'input',
+ name: 'defaultSourceChain',
+ message: 'Default source chain:',
+ default: target.defaultSourceChain,
+ },
+ {
+ type: 'input',
+ name: 'defaultDestinationChain',
+ message: 'Default destination chain:',
+ default: target.defaultDestinationChain,
+ },
+ { type: 'password', name: 'evmKey', message: 'EVM private key (optional):', mask: '*' },
+ { type: 'password', name: 'tvmKey', message: 'TVM private key (optional):', mask: '*' },
+ { type: 'password', name: 'svmKey', message: 'SVM private key (optional):', mask: '*' },
+ ]);
+
+ void envConfig;
+
+ if (answers.defaultSourceChain)
+ target.defaultSourceChain = answers.defaultSourceChain as string;
+ if (answers.defaultDestinationChain)
+ target.defaultDestinationChain = answers.defaultDestinationChain as string;
+ if (!target.defaultPrivateKeys) target.defaultPrivateKeys = {};
+ if (answers.evmKey) target.defaultPrivateKeys[ChainType.EVM] = answers.evmKey as string;
+ if (answers.tvmKey) target.defaultPrivateKeys[ChainType.TVM] = answers.tvmKey as string;
+ if (answers.svmKey) target.defaultPrivateKeys[ChainType.SVM] = answers.svmKey as string;
+
+ if (profileName) {
+ if (!config.profiles) config.profiles = {};
+ config.profiles[profileName] = target;
+ } else {
+ Object.assign(config, target);
+ }
+
+ this.saveConfig(config);
+ console.log('✅ Configuration updated');
+ }
+
+ private loadConfig(): ConfigSettings {
+ if (!fs.existsSync(CONFIG_FILE)) return {};
+ return JSON.parse(fs.readFileSync(CONFIG_FILE, 'utf-8')) as ConfigSettings;
+ }
+
+ private saveConfig(config: ConfigSettings): void {
+ this.ensureConfigDir();
+ fs.writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2));
+ }
+
+ private ensureConfigDir(): void {
+ if (!fs.existsSync(CONFIG_DIR)) fs.mkdirSync(CONFIG_DIR, { recursive: true });
+ if (!fs.existsSync(PROFILES_DIR)) fs.mkdirSync(PROFILES_DIR, { recursive: true });
+ }
+
+ private displayConfig(config: ConfigSettings): void {
+ if (config.defaultSourceChain)
+ console.log(` Default Source Chain: ${config.defaultSourceChain}`);
+ if (config.defaultDestinationChain)
+ console.log(` Default Destination Chain: ${config.defaultDestinationChain}`);
+ if (config.rpcUrls) {
+ for (const [chain, url] of Object.entries(config.rpcUrls)) {
+ console.log(` RPC URL (${chain}): ${url}`);
+ }
+ }
+ if (config.defaultPrivateKeys) {
+ for (const [chainType, key] of Object.entries(config.defaultPrivateKeys)) {
+ if (key) console.log(` Private Key (${chainType}): ***[SET]***`);
+ }
+ }
+ if (
+ !config.defaultSourceChain &&
+ !config.defaultDestinationChain &&
+ !config.rpcUrls &&
+ !config.defaultPrivateKeys
+ ) {
+ console.log(' No configuration set');
+ }
+ }
+
+ private setConfigValue(key: string, value: string, profileName?: string): void {
+ const config = this.loadConfig();
+ const target: ConfigSettings = profileName ? (config.profiles?.[profileName] ?? {}) : config;
+ this.setNestedValue(target, key, value);
+ if (profileName) {
+ if (!config.profiles) config.profiles = {};
+ config.profiles[profileName] = target;
+ } else {
+ Object.assign(config, target);
+ }
+ this.saveConfig(config);
+ console.log(`✅ '${key}' set to '${value}'`);
+ }
+
+ private unsetConfigValue(key: string, profileName?: string): void {
+ const config = this.loadConfig();
+ const target: ConfigSettings = profileName ? (config.profiles?.[profileName] ?? {}) : config;
+ this.deleteNestedValue(target, key);
+ if (profileName) {
+ if (!config.profiles) config.profiles = {};
+ config.profiles[profileName] = target;
+ } else {
+ Object.assign(config, target);
+ }
+ this.saveConfig(config);
+ }
+
+ private createProfile(name: string): void {
+ const config = this.loadConfig();
+ if (!config.profiles) config.profiles = {};
+ if (config.profiles[name]) throw new Error(`Profile '${name}' already exists`);
+ config.profiles[name] = {};
+ this.saveConfig(config);
+ }
+
+ private switchProfile(name: string): void {
+ const config = this.loadConfig();
+ if (!config.profiles?.[name]) throw new Error(`Profile '${name}' does not exist`);
+ config.currentProfile = name;
+ this.saveConfig(config);
+ }
+
+ private deleteProfile(name: string): void {
+ const config = this.loadConfig();
+ if (!config.profiles?.[name]) throw new Error(`Profile '${name}' does not exist`);
+ delete config.profiles[name];
+ if (config.currentProfile === name) delete config.currentProfile;
+ this.saveConfig(config);
+ }
+
+ private resetConfig(profileName?: string): void {
+ if (profileName) {
+ const config = this.loadConfig();
+ if (config.profiles?.[profileName]) {
+ config.profiles[profileName] = {};
+ this.saveConfig(config);
+ }
+ } else if (fs.existsSync(CONFIG_FILE)) {
+ fs.unlinkSync(CONFIG_FILE);
+ }
+ }
+
+ private getNestedValue(obj: ConfigSettings | Record, keyPath: string): unknown {
+ return keyPath.split('.').reduce((cur: unknown, k) => {
+ if (cur && typeof cur === 'object' && k in (cur as Record)) {
+ return (cur as Record)[k];
+ }
+ return undefined;
+ }, obj);
+ }
+
+ private setNestedValue(
+ obj: ConfigSettings | Record,
+ keyPath: string,
+ value: unknown
+ ): void {
+ const keys = keyPath.split('.');
+ const last = keys.pop()!;
+ const target = keys.reduce(
+ (cur: Record, k) => {
+ if (!cur[k] || typeof cur[k] !== 'object') cur[k] = {};
+ return cur[k] as Record;
+ },
+ obj as Record
+ );
+ target[last] = value;
+ }
+
+ private deleteNestedValue(obj: ConfigSettings | Record, keyPath: string): void {
+ const keys = keyPath.split('.');
+ const last = keys.pop()!;
+ const target = keys.reduce((cur: unknown, k) => {
+ if (cur && typeof cur === 'object' && k in (cur as Record)) {
+ return (cur as Record)[k];
+ }
+ return undefined;
+ }, obj as unknown);
+ if (target && typeof target === 'object' && last in (target as Record)) {
+ delete (target as Record)[last];
+ }
+ }
+
+ @Option({ flags: '-i, --interactive', description: 'Interactive mode' })
+ parseInteractive(): boolean {
+ return true;
+ }
+
+ @Option({ flags: '--profile ', description: 'Target profile' })
+ parseProfile(val: string): string {
+ return val;
+ }
+
+ @Option({ flags: '--force', description: 'Skip confirmation' })
+ parseForce(): boolean {
+ return true;
+ }
+}
diff --git a/src/cli/commands/publish.command.ts b/src/cli/commands/publish.command.ts
new file mode 100644
index 0000000..84fa39b
--- /dev/null
+++ b/src/cli/commands/publish.command.ts
@@ -0,0 +1,387 @@
+import { Injectable } from '@nestjs/common';
+
+import { Keypair } from '@solana/web3.js';
+import { Command, CommandRunner, Option } from 'nest-commander';
+import { TronWeb } from 'tronweb';
+import { Hex } from 'viem';
+import { privateKeyToAccount } from 'viem/accounts';
+
+import { AddressNormalizerService } from '@/blockchain/address-normalizer.service';
+import { ChainsService } from '@/blockchain/chains.service';
+import { PublisherFactory } from '@/blockchain/publisher-factory.service';
+import { ConfigService } from '@/config/config.service';
+import { TOKEN_CONFIGS } from '@/config/tokens.config';
+import { IntentBuilder } from '@/intent/intent-builder.service';
+import { IntentStorage } from '@/intent/intent-storage.service';
+import { QuoteResult, QuoteService } from '@/quote/quote.service';
+import { KeyHandle } from '@/shared/security';
+import { BlockchainAddress, ChainType, Intent, UniversalAddress } from '@/shared/types';
+import { IntentStatus, StatusService } from '@/status/status.service';
+
+import { DisplayService } from '../services/display.service';
+import { PromptService } from '../services/prompt.service';
+
+function deriveAddress(key: string, chainType: ChainType): string {
+ switch (chainType) {
+ case ChainType.EVM:
+ return privateKeyToAccount(key as Hex).address;
+ case ChainType.TVM:
+ return TronWeb.address.fromPrivateKey(key) as string;
+ case ChainType.SVM: {
+ let keypair: Keypair;
+ if (key.startsWith('[') && key.endsWith(']')) {
+ keypair = Keypair.fromSecretKey(new Uint8Array(JSON.parse(key) as number[]));
+ } else if (key.includes(',')) {
+ keypair = Keypair.fromSecretKey(
+ new Uint8Array(key.split(',').map(b => parseInt(b.trim())))
+ );
+ } else {
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
+ const bs58 = require('bs58') as { decode: (s: string) => Uint8Array };
+ keypair = Keypair.fromSecretKey(bs58.decode(key));
+ }
+ return keypair.publicKey.toBase58();
+ }
+ }
+}
+
+interface PublishOptions {
+ source?: string;
+ destination?: string;
+ privateKey?: string;
+ privateKeyTvm?: string;
+ privateKeySvm?: string;
+ rpc?: string;
+ recipient?: string;
+ portalAddress?: string;
+ proverAddress?: string;
+ dryRun?: boolean;
+ watch?: boolean;
+}
+
+function resolveKey(options: PublishOptions, chainType: ChainType): string | undefined {
+ switch (chainType) {
+ case ChainType.EVM:
+ return options.privateKey;
+ case ChainType.TVM:
+ return options.privateKeyTvm;
+ case ChainType.SVM:
+ return options.privateKeySvm;
+ }
+}
+
+@Injectable()
+@Command({ name: 'publish', description: 'Publish an intent to the blockchain' })
+export class PublishCommand extends CommandRunner {
+ constructor(
+ private readonly chains: ChainsService,
+ private readonly config: ConfigService,
+ private readonly normalizer: AddressNormalizerService,
+ private readonly publisherFactory: PublisherFactory,
+ private readonly quoteService: QuoteService,
+ private readonly intentBuilder: IntentBuilder,
+ private readonly intentStorage: IntentStorage,
+ private readonly prompt: PromptService,
+ private readonly display: DisplayService,
+ private readonly statusService: StatusService
+ ) {
+ super();
+ }
+
+ async run(_params: string[], options: PublishOptions): Promise {
+ this.display.title('🎨 Interactive Intent Publishing');
+
+ const allChains = this.chains.listChains();
+ const sourceChain = options.source
+ ? this.chains.resolveChain(options.source)
+ : await this.prompt.selectChain(allChains, 'Select source chain:');
+
+ const destChain = options.destination
+ ? this.chains.resolveChain(options.destination)
+ : await this.prompt.selectChain(
+ allChains.filter(c => c.id !== sourceChain.id),
+ 'Select destination chain:'
+ );
+
+ const tokens = Object.values(TOKEN_CONFIGS);
+
+ this.display.section('📏 Route Configuration (Destination Chain)');
+ const routeToken = await this.prompt.selectToken(destChain, tokens, 'route');
+
+ this.display.section('💰 Reward Configuration (Source Chain)');
+ const rewardToken = await this.prompt.selectToken(sourceChain, tokens, 'reward');
+ const { parsed: rewardAmount } = await this.prompt.inputAmount(
+ rewardToken.symbol ?? 'tokens',
+ rewardToken.decimals
+ );
+
+ this.display.section('👤 Recipient Configuration');
+ const destKey =
+ resolveKey(options, destChain.type) ?? this.config.getKeyForChainType(destChain.type);
+ const recipientDefault = destKey ? deriveAddress(destKey, destChain.type) : undefined;
+ const recipientRaw =
+ options.recipient ??
+ (await this.prompt.inputAddress(destChain, 'recipient', recipientDefault));
+ const recipient = this.normalizer.normalize(
+ recipientRaw as Parameters[0],
+ destChain.type
+ );
+
+ const rawKey =
+ resolveKey(options, sourceChain.type) ??
+ this.config.getKeyForChainType(sourceChain.type) ??
+ '';
+ const keyHandle = new KeyHandle(rawKey);
+
+ // Derive sender address synchronously, then keep async key handle for publisher
+ let senderAddress: string;
+ const publishKeyHandle = new KeyHandle(rawKey);
+ keyHandle.use(key => {
+ senderAddress = deriveAddress(key, sourceChain.type);
+ });
+
+ // Quote or fallback
+ let encodedRoute: string;
+ let sourcePortal: UniversalAddress | undefined;
+ let proverAddress: UniversalAddress | undefined;
+ let quote: QuoteResult | undefined;
+
+ try {
+ this.display.spinner('Getting quote...');
+ quote = await this.quoteService.getQuote({
+ source: sourceChain.id,
+ destination: destChain.id,
+ amount: rewardAmount,
+ funder: senderAddress!,
+ recipient: recipientRaw,
+ routeToken: routeToken.address,
+ rewardToken: rewardToken.address,
+ });
+ this.display.succeed('Quote received');
+ this.display.displayQuote(quote, rewardToken, rewardAmount, routeToken);
+ encodedRoute = quote.encodedRoute;
+ sourcePortal = this.normalizer.normalize(
+ quote.sourcePortal as Parameters[0],
+ sourceChain.type
+ );
+ proverAddress = this.normalizer.normalize(
+ quote.prover as Parameters[0],
+ sourceChain.type
+ );
+ } catch (error) {
+ console.error(error);
+ this.display.warn('Quote service unavailable — using manual configuration');
+
+ const { parsed: routeAmount } = await this.prompt.inputAmount(
+ routeToken.symbol ?? 'tokens',
+ routeToken.decimals
+ );
+
+ const destPortal = destChain.portalAddress!;
+ const routeTokenUniversal = this.normalizer.normalize(
+ routeToken.address as Parameters[0],
+ destChain.type
+ );
+
+ const { encodedRoute: manualEncodedRoute } = this.intentBuilder.buildManualRoute({
+ destChain,
+ recipient,
+ routeToken: routeTokenUniversal,
+ routeAmount,
+ portal: destPortal,
+ });
+ encodedRoute = manualEncodedRoute;
+ }
+
+ // Source portal: CLI arg → interactive prompt (quote already set above if available)
+ if (!sourcePortal && options.portalAddress) {
+ sourcePortal = this.normalizer.normalize(
+ options.portalAddress as Parameters[0],
+ sourceChain.type
+ );
+ }
+ if (!sourcePortal) {
+ const raw = await this.prompt.inputManualPortal(sourceChain);
+ sourcePortal = this.normalizer.normalize(
+ raw as Parameters[0],
+ sourceChain.type
+ );
+ }
+
+ // Prover address: CLI arg → chain config → interactive prompt (quote already set above if available)
+ if (!proverAddress && options.proverAddress) {
+ proverAddress = this.normalizer.normalize(
+ options.proverAddress as Parameters[0],
+ sourceChain.type
+ );
+ }
+ if (!proverAddress && sourceChain.proverAddress) {
+ proverAddress = sourceChain.proverAddress;
+ }
+ if (!proverAddress) {
+ const raw = await this.prompt.inputManualProver(sourceChain);
+ proverAddress = this.normalizer.normalize(
+ raw as Parameters[0],
+ sourceChain.type
+ );
+ }
+
+ const rewardTokenUniversal = this.normalizer.normalize(
+ rewardToken.address as Parameters[0],
+ sourceChain.type
+ );
+
+ const reward = this.intentBuilder.buildReward({
+ sourceChain,
+ creator: this.normalizer.normalize(
+ senderAddress! as Parameters[0],
+ sourceChain.type
+ ),
+ prover: proverAddress,
+ rewardToken: rewardTokenUniversal,
+ rewardAmount,
+ });
+
+ // Display summary + confirm
+ const confirmed = await this.prompt.confirmPublish();
+ if (!confirmed) throw new Error('Publication cancelled by user');
+
+ if (options.dryRun) {
+ this.display.warning('Dry run — not publishing');
+ return;
+ }
+
+ const publisher = this.publisherFactory.create(sourceChain);
+ const result = await publisher.publish(
+ sourceChain.id,
+ destChain.id,
+ reward,
+ encodedRoute,
+ publishKeyHandle,
+ sourcePortal
+ );
+
+ if (!result.success) {
+ this.display.fail('Publishing failed');
+ throw new Error(result.error);
+ }
+
+ const intent: Intent = {
+ destination: destChain.id,
+ sourceChainId: sourceChain.id,
+ route: {} as Intent['route'],
+ reward,
+ };
+ await this.intentStorage.save(intent, result);
+ this.display.succeed('Intent published!');
+ this.display.displayTransactionResult(result);
+
+ const watchEnabled = options.watch === true;
+ const canWatch = destChain.type === ChainType.EVM;
+
+ if (watchEnabled && result.intentHash) {
+ if (!canWatch) {
+ this.display.log(`Fulfillment watching not yet supported for ${destChain.type} chains.`);
+ } else {
+ const timeoutMultipler = 3;
+ const estimatedSec = quote?.estimatedFulfillTimeSec ?? 300;
+ const timeoutMs = estimatedSec * timeoutMultipler * 1000;
+
+ this.display.spinner(`Watching for fulfillment on ${destChain.name}...`);
+
+ const watchChain = quote?.destinationPortalAddress
+ ? {
+ ...destChain,
+ portalAddress: this.normalizer.normalize(
+ quote.destinationPortalAddress as BlockchainAddress,
+ destChain.type
+ ),
+ }
+ : destChain;
+
+ let finalStatus: IntentStatus | null = null;
+ const outcome = await this.statusService.watch(
+ result.intentHash,
+ watchChain,
+ status => {
+ finalStatus = status;
+ },
+ { timeoutMs }
+ );
+
+ if (outcome === 'fulfilled' && finalStatus) {
+ this.display.succeed('Intent fulfilled!');
+ this.display.displayFulfillmentResult(finalStatus);
+ } else {
+ this.display.warn(
+ `Not fulfilled within ${estimatedSec * timeoutMultipler}s — check manually: ` +
+ `routes status ${result.intentHash} --chain ${destChain.name}`
+ );
+ }
+ }
+ }
+
+ void recipient; // used in reward/route construction
+ }
+
+ @Option({ flags: '-s, --source ', description: 'Source chain name or ID' })
+ parseSource(val: string): string {
+ return val;
+ }
+
+ @Option({ flags: '-d, --destination ', description: 'Destination chain name or ID' })
+ parseDestination(val: string): string {
+ return val;
+ }
+
+ @Option({ flags: '-k, --private-key ', description: 'EVM private key (overrides env)' })
+ parsePrivateKey(val: string): string {
+ return val;
+ }
+
+ @Option({ flags: '--private-key-tvm ', description: 'TVM private key (overrides env)' })
+ parsePrivateKeyTvm(val: string): string {
+ return val;
+ }
+
+ @Option({ flags: '--private-key-svm ', description: 'SVM private key (overrides env)' })
+ parsePrivateKeySvm(val: string): string {
+ return val;
+ }
+
+ @Option({ flags: '-r, --rpc ', description: 'RPC URL override' })
+ parseRpc(val: string): string {
+ return val;
+ }
+
+ @Option({ flags: '--recipient ', description: 'Recipient address on destination chain' })
+ parseRecipient(val: string): string {
+ return val;
+ }
+
+ @Option({
+ flags: '--portal-address ',
+ description: 'Portal contract address on the source chain',
+ })
+ parsePortalAddress(val: string): string {
+ return val;
+ }
+
+ @Option({
+ flags: '--prover-address ',
+ description: 'Prover contract address on the source chain',
+ })
+ parseProverAddress(val: string): string {
+ return val;
+ }
+
+ @Option({ flags: '--dry-run', description: 'Validate without broadcasting' })
+ parseDryRun(): boolean {
+ return true;
+ }
+
+ @Option({ flags: '-w, --watch', description: 'Watch for fulfillment after publishing' })
+ parseWatch(): boolean {
+ return true;
+ }
+}
diff --git a/src/cli/commands/status.command.ts b/src/cli/commands/status.command.ts
new file mode 100644
index 0000000..56e039c
--- /dev/null
+++ b/src/cli/commands/status.command.ts
@@ -0,0 +1,106 @@
+import { Injectable } from '@nestjs/common';
+
+import chalk from 'chalk';
+import { Command, CommandRunner, Option } from 'nest-commander';
+
+import { ChainsService } from '@/blockchain/chains.service';
+import { IntentStatus, StatusService } from '@/status/status.service';
+
+import { DisplayService } from '../services/display.service';
+
+interface StatusOptions {
+ chain?: string;
+ watch?: boolean;
+ json?: boolean;
+ verbose?: boolean;
+}
+
+@Injectable()
+@Command({
+ name: 'status',
+ description: 'Check the fulfillment status of an intent',
+ arguments: '',
+})
+export class StatusCommand extends CommandRunner {
+ constructor(
+ private readonly chains: ChainsService,
+ private readonly statusService: StatusService,
+ private readonly display: DisplayService
+ ) {
+ super();
+ }
+
+ async run(inputs: string[], options: StatusOptions): Promise {
+ const intentHash = inputs[0];
+
+ if (!intentHash || !intentHash.startsWith('0x') || intentHash.length !== 66) {
+ this.display.error('Intent hash must be a 0x-prefixed 64-character hex string');
+ process.exit(1);
+ }
+
+ if (!options.chain) {
+ this.display.error('Destination chain is required. Use --chain option.');
+ process.exit(1);
+ }
+
+ const chain = this.chains.resolveChain(options.chain);
+
+ if (!options.json && !options.watch) {
+ this.display.title('🔍 Checking Intent Status');
+ this.display.log(`Intent Hash: ${intentHash}`);
+ this.display.log(`Chain: ${chain.name} (${chain.id})`);
+ }
+
+ if (options.watch) {
+ await this.statusService.watch(
+ intentHash,
+ chain,
+ status => this.displayStatus(status, options),
+ {}
+ );
+ } else {
+ const status = await this.statusService.getStatus(intentHash, chain);
+ this.displayStatus(status, options);
+ }
+ }
+
+ private displayStatus(status: IntentStatus, options: StatusOptions): void {
+ if (options.json) {
+ console.log(JSON.stringify(status, (_k, v) => (typeof v === 'bigint' ? v.toString() : v), 2));
+ return;
+ }
+
+ const statusText = status.fulfilled ? chalk.green('✅ Fulfilled') : chalk.yellow('⏳ Pending');
+ this.display.log(`Status: ${statusText}`);
+
+ if (status.fulfilled) {
+ if (status.solver) this.display.log(`Solver: ${status.solver}`);
+ if (status.fulfillmentTxHash) this.display.log(`Tx: ${status.fulfillmentTxHash}`);
+ if (status.blockNumber) this.display.log(`Block: ${status.blockNumber.toString()}`);
+ if (status.timestamp)
+ this.display.log(`Time: ${new Date(status.timestamp * 1000).toLocaleString()}`);
+ } else {
+ this.display.log('The intent has not been fulfilled yet.');
+ }
+ }
+
+ @Option({ flags: '-c, --chain ', description: 'Destination chain (name or ID)' })
+ parseChain(val: string): string {
+ return val;
+ }
+
+ @Option({ flags: '-w, --watch', description: 'Poll every 10 seconds until fulfilled' })
+ parseWatch(): boolean {
+ return true;
+ }
+
+ @Option({ flags: '--json', description: 'Output result as JSON' })
+ parseJson(): boolean {
+ return true;
+ }
+
+ @Option({ flags: '--verbose', description: 'Show portal address and raw transaction details' })
+ parseVerbose(): boolean {
+ return true;
+ }
+}
diff --git a/src/cli/commands/tokens.command.ts b/src/cli/commands/tokens.command.ts
new file mode 100644
index 0000000..cb4b233
--- /dev/null
+++ b/src/cli/commands/tokens.command.ts
@@ -0,0 +1,20 @@
+import { Injectable } from '@nestjs/common';
+
+import { Command, CommandRunner } from 'nest-commander';
+
+import { TOKENS } from '@/config/tokens.config';
+
+import { DisplayService } from '../services/display.service';
+
+@Injectable()
+@Command({ name: 'tokens', description: 'List configured tokens' })
+export class TokensCommand extends CommandRunner {
+ constructor(private readonly display: DisplayService) {
+ super();
+ }
+
+ run(): Promise {
+ this.display.displayTokens(Object.values(TOKENS));
+ return Promise.resolve();
+ }
+}
diff --git a/src/cli/services/display.module.ts b/src/cli/services/display.module.ts
new file mode 100644
index 0000000..751978c
--- /dev/null
+++ b/src/cli/services/display.module.ts
@@ -0,0 +1,9 @@
+import { Module } from '@nestjs/common';
+
+import { DisplayService } from './display.service';
+
+@Module({
+ providers: [DisplayService],
+ exports: [DisplayService],
+})
+export class DisplayModule {}
diff --git a/src/cli/services/display.service.ts b/src/cli/services/display.service.ts
new file mode 100644
index 0000000..facfa00
--- /dev/null
+++ b/src/cli/services/display.service.ts
@@ -0,0 +1,148 @@
+import { Injectable } from '@nestjs/common';
+
+import chalk from 'chalk';
+import Table from 'cli-table3';
+import ora, { Ora } from 'ora';
+import { formatUnits } from 'viem';
+
+import { IntentStatus, PublishResult } from '@/blockchain/base.publisher';
+import { TokenConfig } from '@/config/tokens.config';
+import { QuoteResult } from '@/quote/quote.service';
+import { ChainConfig } from '@/shared/types';
+
+@Injectable()
+export class DisplayService {
+ private activeSpinner: Ora | null = null;
+
+ spinner(text: string): void {
+ this.stopSpinner();
+ this.activeSpinner = ora(text).start();
+ }
+
+ succeed(text?: string): void {
+ if (this.activeSpinner) {
+ this.activeSpinner.succeed(text);
+ this.activeSpinner = null;
+ } else {
+ console.log(chalk.green(`✓ ${text}`));
+ }
+ }
+ fail(text?: string): void {
+ if (this.activeSpinner) {
+ this.activeSpinner.fail(text);
+ this.activeSpinner = null;
+ } else {
+ console.error(chalk.red(`✗ ${text}`));
+ }
+ }
+ warn(text?: string): void {
+ if (this.activeSpinner) {
+ this.activeSpinner.warn(text);
+ this.activeSpinner = null;
+ } else {
+ console.warn(chalk.yellow(`⚠ ${text}`));
+ }
+ }
+ stopSpinner(): void {
+ this.activeSpinner?.stop();
+ this.activeSpinner = null;
+ }
+
+ log(msg: string): void {
+ if (this.activeSpinner) {
+ this.activeSpinner.stop();
+ console.log(chalk.gray(msg));
+ this.activeSpinner.start();
+ } else {
+ console.log(chalk.gray(msg));
+ }
+ }
+ success(msg: string): void {
+ console.log(chalk.green(`✅ ${msg}`));
+ }
+ error(msg: string): void {
+ console.error(chalk.red(`❌ ${msg}`));
+ }
+ warning(msg: string): void {
+ console.warn(chalk.yellow(`⚠️ ${msg}`));
+ }
+ title(msg: string): void {
+ console.log(chalk.bold.blue(msg));
+ }
+ section(msg: string): void {
+ console.log(chalk.blue(msg));
+ }
+
+ displayTable(headers: string[], rows: string[][]): void {
+ const table = new Table({ head: headers.map(h => chalk.cyan(h)), style: { border: ['gray'] } });
+ rows.forEach(row => table.push(row));
+ console.log(table.toString());
+ }
+
+ displayTransactionResult(result: PublishResult): void {
+ this.displayTable(
+ ['Field', 'Value'],
+ [
+ ['Transaction Hash', result.transactionHash ?? '-'],
+ ['Intent Hash', result.intentHash ?? '-'],
+ ['Vault Address', result.vaultAddress ?? '-'],
+ ]
+ );
+ }
+
+ displayFulfillmentResult(status: IntentStatus): void {
+ this.displayTable(
+ ['Field', 'Value'],
+ [
+ ['Fulfillment Tx', status.fulfillmentTxHash ?? '-'],
+ ['Solver', status.solver ?? '-'],
+ ['Block', status.blockNumber?.toString() ?? '-'],
+ ]
+ );
+ }
+
+ displayQuote(
+ quote: QuoteResult,
+ sourceToken: { symbol?: string; decimals: number },
+ sourceAmount: bigint,
+ destToken: { symbol?: string; decimals: number }
+ ): void {
+ const srcSymbol = sourceToken.symbol ?? 'tokens';
+ const dstSymbol = destToken.symbol ?? 'tokens';
+ const rows: string[][] = [
+ ['Source Token', srcSymbol],
+ ['Source Amount', `${formatUnits(sourceAmount, sourceToken.decimals)} ${srcSymbol}`],
+ ['Destination Token', dstSymbol],
+ [
+ 'Destination Amount',
+ `${formatUnits(BigInt(quote.destinationAmount), destToken.decimals)} ${dstSymbol}`,
+ ],
+ ['Portal', quote.sourcePortal],
+ ['Prover', quote.prover],
+ ['Deadline', new Date(quote.deadline * 1000).toLocaleString()],
+ ];
+ if (quote.estimatedFulfillTimeSec !== undefined) {
+ rows.push(['Est. Fulfill Time', `${quote.estimatedFulfillTimeSec}s`]);
+ }
+ this.displayTable(['Quote Summary', ''], rows);
+ }
+
+ displayChains(chains: ChainConfig[]): void {
+ this.displayTable(
+ ['Name', 'ID', 'Type', 'Native Currency'],
+ chains.map(c => [c.name, c.id.toString(), c.type, c.nativeCurrency.symbol])
+ );
+ }
+
+ displayTokens(tokens: TokenConfig[]): void {
+ this.displayTable(
+ ['Symbol', 'Name', 'Decimals', 'Available Chains'],
+ tokens.map(t => [
+ t.symbol,
+ t.name,
+ t.decimals.toString(),
+ Object.keys(t.addresses).join(', '),
+ ])
+ );
+ }
+}
diff --git a/src/cli/services/prompt.service.ts b/src/cli/services/prompt.service.ts
new file mode 100644
index 0000000..12f899c
--- /dev/null
+++ b/src/cli/services/prompt.service.ts
@@ -0,0 +1,203 @@
+import { Injectable } from '@nestjs/common';
+
+import inquirer from 'inquirer';
+import { parseUnits } from 'viem';
+
+import { AddressNormalizerService } from '@/blockchain/address-normalizer.service';
+import { ChainRegistryService } from '@/blockchain/chain-registry.service';
+import { TokenConfig } from '@/config/tokens.config';
+import { ChainConfig } from '@/shared/types';
+
+@Injectable()
+export class PromptService {
+ constructor(
+ private readonly registry: ChainRegistryService,
+ private readonly normalizer: AddressNormalizerService
+ ) {}
+
+ async selectChain(chains: ChainConfig[], message: string): Promise {
+ const { chain } = await inquirer.prompt([
+ {
+ type: 'list',
+ name: 'chain',
+ message,
+ choices: chains.map(c => ({ name: `${c.name} (${c.id})`, value: c })),
+ },
+ ]);
+ return chain;
+ }
+
+ async selectToken(
+ chain: ChainConfig,
+ tokens: TokenConfig[],
+ label: string
+ ): Promise<{ address: string; decimals: number; symbol?: string }> {
+ const availableTokens = tokens.filter(t => !!t.addresses[chain.id.toString()]);
+ const choices = [
+ ...availableTokens.map(t => ({ name: `${t.symbol} - ${t.name}`, value: t.symbol })),
+ { name: 'Custom Token Address', value: 'CUSTOM' },
+ ];
+
+ const { tokenChoice } = await inquirer.prompt([
+ {
+ type: 'list',
+ name: 'tokenChoice',
+ message: `Select ${label} token:`,
+ choices,
+ },
+ ]);
+
+ if (tokenChoice === 'CUSTOM') {
+ const handler = this.registry.get(chain.type);
+ const { address, decimals } = await inquirer.prompt([
+ {
+ type: 'input',
+ name: 'address',
+ message: 'Enter token address:',
+ validate: (input: string) => {
+ if (!handler.validateAddress(input)) {
+ return `Invalid ${chain.type} address — expected ${handler.getAddressFormat()}`;
+ }
+ return true;
+ },
+ },
+ {
+ type: 'input',
+ name: 'decimals',
+ message: 'Enter token decimals (e.g., 18 for most ERC20, 6 for USDC):',
+ default: '18',
+ validate: (input: string) => {
+ const num = parseInt(input);
+ return !isNaN(num) && num >= 0 && num <= 255
+ ? true
+ : 'Please enter a valid number between 0 and 255';
+ },
+ },
+ ]);
+ return { address: address as string, decimals: parseInt(decimals as string) };
+ }
+
+ const token = availableTokens.find(t => t.symbol === tokenChoice);
+ if (!token) throw new Error(`Token ${tokenChoice as string} not found`);
+
+ const tokenAddress = token.addresses[chain.id.toString()];
+ if (!tokenAddress) throw new Error(`Token ${token.symbol} not available on chain ${chain.id}`);
+
+ return {
+ address: this.normalizer.denormalize(tokenAddress, chain.type) as string,
+ decimals: token.decimals,
+ symbol: token.symbol,
+ };
+ }
+
+ async inputAmount(
+ label: string,
+ decimals: number,
+ defaultValue = '0.1'
+ ): Promise<{ raw: string; parsed: bigint }> {
+ const { amount } = await inquirer.prompt([
+ {
+ type: 'input',
+ name: 'amount',
+ message: `Enter ${label} amount in human-readable format (e.g., "10" for 10 tokens):`,
+ default: defaultValue,
+ validate: (input: string) => {
+ const num = parseFloat(input);
+ return !isNaN(num) && num > 0 ? true : 'Please enter a positive number';
+ },
+ },
+ ]);
+ return {
+ raw: amount as string,
+ parsed: parseUnits(amount as string, decimals),
+ };
+ }
+
+ async inputAddress(chain: ChainConfig, label: string, defaultValue?: string): Promise {
+ const handler = this.registry.get(chain.type);
+ const { address } = await inquirer.prompt([
+ {
+ type: 'input',
+ name: 'address',
+ message: `Enter ${label} address on ${chain.name} (${chain.type} chain):`,
+ default: defaultValue,
+ validate: (input: string) => {
+ if (!input || input.trim() === '') return `${label} address is required`;
+ if (!handler.validateAddress(input)) {
+ return `Invalid ${chain.type} address — expected ${handler.getAddressFormat()}`;
+ }
+ return true;
+ },
+ },
+ ]);
+ return address as string;
+ }
+
+ async confirmPublish(): Promise {
+ const { confirmed } = await inquirer.prompt([
+ {
+ type: 'confirm',
+ name: 'confirmed',
+ message: 'Publish this intent?',
+ default: true,
+ },
+ ]);
+ return confirmed;
+ }
+
+ async confirm(message: string, defaultValue = false): Promise {
+ const { confirmed } = await inquirer.prompt([
+ {
+ type: 'confirm',
+ name: 'confirmed',
+ message,
+ default: defaultValue,
+ },
+ ]);
+ return confirmed;
+ }
+
+ async inputManualPortal(chain: ChainConfig): Promise {
+ const handler = this.registry.get(chain.type);
+ const { portal } = await inquirer.prompt([
+ {
+ type: 'input',
+ name: 'portal',
+ message: `Enter portal contract address on ${chain.name}:`,
+ default: chain.portalAddress
+ ? (this.normalizer.denormalize(chain.portalAddress, chain.type) as string)
+ : undefined,
+ validate: (input: string) => {
+ if (!input || input.trim() === '') return 'Portal address is required';
+ if (!handler.validateAddress(input)) {
+ return `Invalid ${chain.type} address — expected ${handler.getAddressFormat()}`;
+ }
+ return true;
+ },
+ },
+ ]);
+ return portal as string;
+ }
+
+ async inputManualProver(chain: ChainConfig): Promise {
+ const handler = this.registry.get(chain.type);
+ const { prover } = await inquirer.prompt([
+ {
+ type: 'input',
+ name: 'prover',
+ message: `Enter prover contract address on ${chain.name}:`,
+ default: chain.proverAddress
+ ? (this.normalizer.denormalize(chain.proverAddress, chain.type) as string)
+ : undefined,
+ validate: (input: string) => {
+ if (!input || input.trim() === '') return 'Prover address is required';
+ if (!handler.validateAddress(input)) {
+ return `Invalid ${chain.type} address — expected ${handler.getAddressFormat()}`;
+ }
+ return true;
+ },
+ },
+ ]);
+ return prover as string;
+ }
+}
diff --git a/src/commands/config.ts b/src/commands/config.ts
deleted file mode 100644
index b60f5b4..0000000
--- a/src/commands/config.ts
+++ /dev/null
@@ -1,535 +0,0 @@
-/**
- * Config Command
- */
-
-import * as fs from 'fs';
-import * as os from 'os';
-import * as path from 'path';
-
-import { Command } from 'commander';
-import inquirer from 'inquirer';
-
-import { ChainType } from '@/core/interfaces/intent';
-import { logger } from '@/utils/logger';
-
-interface ConfigSettings {
- defaultSourceChain?: string;
- defaultDestinationChain?: string;
- defaultPrivateKeys?: {
- [ChainType.EVM]?: string;
- [ChainType.TVM]?: string;
- [ChainType.SVM]?: string;
- };
- rpcUrls?: Record;
- profiles?: Record;
- currentProfile?: string;
-}
-
-const CONFIG_DIR = path.join(os.homedir(), '.eco-routes');
-const CONFIG_FILE = path.join(CONFIG_DIR, 'config.json');
-const PROFILES_DIR = path.join(CONFIG_DIR, 'profiles');
-
-export function createConfigCommand(): Command {
- const command = new Command('config');
-
- command.description('Manage CLI configuration settings');
-
- // List all configuration
- command
- .command('list')
- .description('List current configuration')
- .option('--profile ', 'Show configuration for specific profile')
- .action(async options => {
- try {
- const config = loadConfig();
-
- if (options.profile) {
- if (!config.profiles?.[options.profile]) {
- logger.error(`Profile '${options.profile}' not found`);
- process.exit(1);
- }
-
- logger.title(`📋 Profile: ${options.profile}`);
- displayConfig(config.profiles[options.profile]);
- } else {
- logger.title('📋 Current Configuration');
- if (config.currentProfile) {
- logger.info(`Active Profile: ${config.currentProfile}`);
- logger.info('');
- }
- displayConfig(config);
-
- if (config.profiles && Object.keys(config.profiles).length > 0) {
- logger.section('Available Profiles');
- Object.keys(config.profiles).forEach(name => {
- const isActive = name === config.currentProfile ? ' (active)' : '';
- logger.info(`• ${name}${isActive}`);
- });
- }
- }
- } catch (error) {
- logger.error(
- `Error reading configuration: ${error instanceof Error ? error.message : String(error)}`
- );
- process.exit(1);
- }
- });
-
- // Set configuration values
- command
- .command('set')
- .description('Set configuration values')
- .argument('[key]', 'Configuration key (e.g., defaultSourceChain)')
- .argument('[value]', 'Configuration value')
- .option('--profile ', 'Set value for specific profile')
- .option('-i, --interactive', 'Interactive mode')
- .action(async (key, value, options) => {
- try {
- if (options.interactive || (!key && !value)) {
- await setConfigInteractive(options.profile);
- } else if (key && value !== undefined) {
- await setConfigValue(key, value, options.profile);
- } else {
- logger.error('Please provide both key and value, or use --interactive mode');
- process.exit(1);
- }
- } catch (error) {
- logger.error(
- `Error setting configuration: ${error instanceof Error ? error.message : String(error)}`
- );
- process.exit(1);
- }
- });
-
- // Get configuration value
- command
- .command('get')
- .description('Get configuration value')
- .argument('', 'Configuration key')
- .option('--profile ', 'Get value from specific profile')
- .action((key, options) => {
- try {
- const config = loadConfig();
- const targetConfig = options.profile ? config.profiles?.[options.profile] || {} : config;
-
- const value = getNestedValue(targetConfig, key);
-
- if (value !== undefined) {
- // Mask private keys for security
- if (key.toLowerCase().includes('private')) {
- logger.log('***[HIDDEN]***');
- } else {
- logger.log(String(value));
- }
- } else {
- logger.warn(`Configuration key '${key}' not found`);
- process.exit(1);
- }
- } catch (error) {
- logger.error(
- `Error getting configuration: ${error instanceof Error ? error.message : String(error)}`
- );
- process.exit(1);
- }
- });
-
- // Remove configuration key
- command
- .command('unset')
- .description('Remove configuration key')
- .argument('', 'Configuration key to remove')
- .option('--profile ', 'Remove from specific profile')
- .action(async (key, options) => {
- try {
- await unsetConfigValue(key, options.profile);
- logger.success(`Configuration key '${key}' removed`);
- } catch (error) {
- logger.error(
- `Error removing configuration: ${error instanceof Error ? error.message : String(error)}`
- );
- process.exit(1);
- }
- });
-
- // Profile management - create profile subcommand
- const profileCommand = command.command('profile').description('Manage configuration profiles');
-
- profileCommand
- .command('create ')
- .description('Create a new profile')
- .action(async name => {
- try {
- await createProfile(name);
- logger.success(`Profile '${name}' created`);
- } catch (error) {
- logger.error(
- `Error creating profile: ${error instanceof Error ? error.message : String(error)}`
- );
- process.exit(1);
- }
- });
-
- profileCommand
- .command('switch ')
- .description('Switch to a profile')
- .action(async name => {
- try {
- await switchProfile(name);
- logger.success(`Switched to profile '${name}'`);
- } catch (error) {
- logger.error(
- `Error switching profile: ${error instanceof Error ? error.message : String(error)}`
- );
- process.exit(1);
- }
- });
-
- profileCommand
- .command('delete ')
- .description('Delete a profile')
- .option('--force', 'Skip confirmation')
- .action(async (name, options) => {
- try {
- if (!options.force) {
- const { confirm } = await inquirer.prompt([
- {
- type: 'confirm',
- name: 'confirm',
- message: `Are you sure you want to delete profile '${name}'?`,
- default: false,
- },
- ]);
-
- if (!confirm) {
- logger.info('Profile deletion cancelled');
- return;
- }
- }
-
- await deleteProfile(name);
- logger.success(`Profile '${name}' deleted`);
- } catch (error) {
- logger.error(
- `Error deleting profile: ${error instanceof Error ? error.message : String(error)}`
- );
- process.exit(1);
- }
- });
-
- profileCommand
- .command('list')
- .description('List available profiles')
- .action(() => {
- try {
- const config = loadConfig();
- if (!config.profiles || Object.keys(config.profiles).length === 0) {
- logger.info('No profiles found');
- return;
- }
-
- logger.title('📋 Available Profiles');
- Object.keys(config.profiles).forEach(name => {
- const isActive = name === config.currentProfile ? ' (active)' : '';
- logger.info(`• ${name}${isActive}`);
- });
- } catch (error) {
- logger.error(
- `Error listing profiles: ${error instanceof Error ? error.message : String(error)}`
- );
- process.exit(1);
- }
- });
-
- // Reset configuration
- command
- .command('reset')
- .description('Reset configuration to defaults')
- .option('--profile ', 'Reset specific profile')
- .option('--force', 'Skip confirmation')
- .action(async options => {
- try {
- if (!options.force) {
- const target = options.profile ? `profile '${options.profile}'` : 'entire configuration';
- const { confirm } = await inquirer.prompt([
- {
- type: 'confirm',
- name: 'confirm',
- message: `Are you sure you want to reset ${target}?`,
- default: false,
- },
- ]);
-
- if (!confirm) {
- logger.info('Reset cancelled');
- return;
- }
- }
-
- await resetConfig(options.profile);
- logger.success(
- options.profile ? `Profile '${options.profile}' reset` : 'Configuration reset'
- );
- } catch (error) {
- logger.error(
- `Error resetting configuration: ${error instanceof Error ? error.message : String(error)}`
- );
- process.exit(1);
- }
- });
-
- return command;
-}
-
-function loadConfig(): ConfigSettings {
- if (!fs.existsSync(CONFIG_FILE)) {
- return {};
- }
-
- try {
- const content = fs.readFileSync(CONFIG_FILE, 'utf-8');
- return JSON.parse(content);
- } catch (error) {
- throw new Error(
- `Failed to parse config file: ${error instanceof Error ? error.message : String(error)}`
- );
- }
-}
-
-function saveConfig(config: ConfigSettings): void {
- ensureConfigDir();
- fs.writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2));
-}
-
-function ensureConfigDir(): void {
- if (!fs.existsSync(CONFIG_DIR)) {
- fs.mkdirSync(CONFIG_DIR, { recursive: true });
- }
- if (!fs.existsSync(PROFILES_DIR)) {
- fs.mkdirSync(PROFILES_DIR, { recursive: true });
- }
-}
-
-function displayConfig(config: ConfigSettings): void {
- const data: Record = {};
-
- if (config.defaultSourceChain) data['Default Source Chain'] = config.defaultSourceChain;
- if (config.defaultDestinationChain)
- data['Default Destination Chain'] = config.defaultDestinationChain;
-
- // Show RPC URLs
- if (config.rpcUrls && Object.keys(config.rpcUrls).length > 0) {
- Object.entries(config.rpcUrls).forEach(([chain, url]) => {
- data[`RPC URL (${chain})`] = url;
- });
- }
-
- // Show private key status (masked)
- if (config.defaultPrivateKeys) {
- Object.entries(config.defaultPrivateKeys).forEach(([chainType, key]) => {
- if (key) {
- data[`Private Key (${chainType})`] = '***[SET]***';
- }
- });
- }
-
- if (Object.keys(data).length === 0) {
- logger.info('No configuration set');
- } else {
- logger.displayKeyValue(data);
- }
-}
-
-async function setConfigInteractive(profileName?: string): Promise {
- const config = loadConfig();
- const targetConfig = profileName ? config.profiles?.[profileName] || {} : config;
-
- logger.title('🔧 Interactive Configuration Setup');
-
- if (profileName) {
- logger.info(`Configuring profile: ${profileName}`);
- }
-
- const questions = [
- {
- type: 'input',
- name: 'defaultSourceChain',
- message: 'Default source chain (name or ID):',
- default: targetConfig.defaultSourceChain,
- },
- {
- type: 'input',
- name: 'defaultDestinationChain',
- message: 'Default destination chain (name or ID):',
- default: targetConfig.defaultDestinationChain,
- },
- {
- type: 'password',
- name: 'evmPrivateKey',
- message: 'EVM private key (optional):',
- mask: '*',
- },
- {
- type: 'password',
- name: 'tvmPrivateKey',
- message: 'TVM private key (optional):',
- mask: '*',
- },
- {
- type: 'password',
- name: 'svmPrivateKey',
- message: 'SVM private key (optional):',
- mask: '*',
- },
- ];
-
- const answers = await inquirer.prompt(questions);
-
- // Update configuration
- if (answers.defaultSourceChain) targetConfig.defaultSourceChain = answers.defaultSourceChain;
- if (answers.defaultDestinationChain)
- targetConfig.defaultDestinationChain = answers.defaultDestinationChain;
-
- if (!targetConfig.defaultPrivateKeys) targetConfig.defaultPrivateKeys = {};
- if (answers.evmPrivateKey) targetConfig.defaultPrivateKeys[ChainType.EVM] = answers.evmPrivateKey;
- if (answers.tvmPrivateKey) targetConfig.defaultPrivateKeys[ChainType.TVM] = answers.tvmPrivateKey;
- if (answers.svmPrivateKey) targetConfig.defaultPrivateKeys[ChainType.SVM] = answers.svmPrivateKey;
-
- if (profileName) {
- if (!config.profiles) config.profiles = {};
- config.profiles[profileName] = targetConfig;
- } else {
- Object.assign(config, targetConfig);
- }
-
- saveConfig(config);
- logger.success('Configuration updated successfully');
-}
-
-async function setConfigValue(key: string, value: string, profileName?: string): Promise {
- const config = loadConfig();
- const targetConfig = profileName ? config.profiles?.[profileName] || {} : config;
-
- setNestedValue(targetConfig, key, value);
-
- if (profileName) {
- if (!config.profiles) config.profiles = {};
- config.profiles[profileName] = targetConfig;
- } else {
- Object.assign(config, targetConfig);
- }
-
- saveConfig(config);
- logger.success(`Configuration key '${key}' set to '${value}'`);
-}
-
-async function unsetConfigValue(key: string, profileName?: string): Promise {
- const config = loadConfig();
- const targetConfig = profileName ? config.profiles?.[profileName] || {} : config;
-
- deleteNestedValue(targetConfig, key);
-
- if (profileName) {
- if (!config.profiles) config.profiles = {};
- config.profiles[profileName] = targetConfig;
- } else {
- Object.assign(config, targetConfig);
- }
-
- saveConfig(config);
-}
-
-async function createProfile(name: string): Promise {
- const config = loadConfig();
-
- if (!config.profiles) config.profiles = {};
- if (config.profiles[name]) {
- throw new Error(`Profile '${name}' already exists`);
- }
-
- config.profiles[name] = {};
- saveConfig(config);
-}
-
-async function switchProfile(name: string): Promise {
- const config = loadConfig();
-
- if (!config.profiles?.[name]) {
- throw new Error(`Profile '${name}' does not exist`);
- }
-
- config.currentProfile = name;
- saveConfig(config);
-}
-
-async function deleteProfile(name: string): Promise {
- const config = loadConfig();
-
- if (!config.profiles?.[name]) {
- throw new Error(`Profile '${name}' does not exist`);
- }
-
- delete config.profiles[name];
- if (config.currentProfile === name) {
- delete config.currentProfile;
- }
-
- saveConfig(config);
-}
-
-async function resetConfig(profileName?: string): Promise {
- if (profileName) {
- const config = loadConfig();
- if (config.profiles?.[profileName]) {
- config.profiles[profileName] = {};
- saveConfig(config);
- }
- } else {
- if (fs.existsSync(CONFIG_FILE)) {
- fs.unlinkSync(CONFIG_FILE);
- }
- }
-}
-
-// Utility functions for nested object operations
-function getNestedValue(obj: ConfigSettings | Record, path: string): unknown {
- return path.split('.').reduce((current: unknown, key: string) => {
- if (current && typeof current === 'object' && key in current) {
- return (current as Record)[key];
- }
- return undefined;
- }, obj);
-}
-
-function setNestedValue(
- obj: ConfigSettings | Record,
- path: string,
- value: unknown
-): void {
- const keys = path.split('.');
- const lastKey = keys.pop()!;
- const target = keys.reduce(
- (current: Record, key: string) => {
- if (!current[key] || typeof current[key] !== 'object') {
- current[key] = {};
- }
- return current[key] as Record;
- },
- obj as Record
- );
- target[lastKey] = value;
-}
-
-function deleteNestedValue(obj: ConfigSettings | Record, path: string): void {
- const keys = path.split('.');
- const lastKey = keys.pop()!;
- const target = keys.reduce((current: unknown, key: string) => {
- if (current && typeof current === 'object' && key in current) {
- return (current as Record)[key];
- }
- return undefined;
- }, obj as unknown);
-
- if (target && typeof target === 'object' && lastKey in target) {
- delete (target as Record)[lastKey];
- }
-}
diff --git a/src/commands/publish.ts b/src/commands/publish.ts
deleted file mode 100644
index 91b49db..0000000
--- a/src/commands/publish.ts
+++ /dev/null
@@ -1,671 +0,0 @@
-/**
- * Publish Command
- */
-
-import * as crypto from 'crypto';
-
-import { Keypair, PublicKey } from '@solana/web3.js';
-import { Command } from 'commander';
-import inquirer from 'inquirer';
-import { TronWeb } from 'tronweb';
-import {
- encodeFunctionData,
- erc20Abi,
- formatUnits,
- Hex,
- isAddress as isViemAddress,
- parseUnits,
-} from 'viem';
-import { privateKeyToAccount } from 'viem/accounts';
-
-import { BasePublisher } from '@/blockchain/base-publisher';
-import { EvmPublisher } from '@/blockchain/evm-publisher';
-import { SvmPublisher } from '@/blockchain/svm-publisher';
-import { TvmPublisher } from '@/blockchain/tvm-publisher';
-import { serialize } from '@/commons/utils/serialize';
-import { ChainConfig, getChainById, getChainByName, listChains } from '@/config/chains';
-import { loadEnvConfig } from '@/config/env';
-import { getTokenAddress, getTokenBySymbol, listTokens } from '@/config/tokens';
-import { ChainType, Intent } from '@/core/interfaces/intent';
-import { BlockchainAddress, SvmAddress, TronAddress } from '@/core/types/blockchain-addresses';
-import { UniversalAddress } from '@/core/types/universal-address';
-import { AddressNormalizer } from '@/core/utils/address-normalizer';
-import { PortalEncoder } from '@/core/utils/portal-encoder';
-import { getQuote, QuoteResponse } from '@/core/utils/quote';
-import { logger } from '@/utils/logger';
-
-interface PublishCommandOptions {
- source?: string;
- destination?: string;
- privateKey?: string;
- rpc?: string;
- dryRun?: boolean;
-}
-
-export function createPublishCommand(): Command {
- const command = new Command('publish');
-
- command
- .description('Publish an intent to the blockchain')
- .option('-s, --source ', 'Source chain (name or ID)')
- .option('-d, --destination ', 'Destination chain (name or ID)')
- .option('-k, --private-key ', 'Private key (overrides env)')
- .option('-r, --rpc ', 'RPC URL (overrides env)')
- .option('--recipient ', 'Recipient address on destination chain')
- .option('--dry-run', 'Validate without publishing')
- .action(async options => {
- try {
- // Interactive mode
- logger.title('🎨 Interactive Intent Publishing');
-
- const { reward, encodedRoute, sourceChain, destChain, sourcePortal } =
- await buildIntentInteractively(options);
-
- if (process.env.DEBUG) {
- logger.log(`Reward: ${serialize(reward)}`);
- }
-
- const privateKey = getPrivateKey(sourceChain);
-
- // Determine RPC URL
- const rpcUrl = options.rpc || sourceChain.rpcUrl;
-
- // Create publisher based on source chain type
- let publisher: BasePublisher;
- switch (sourceChain.type) {
- case ChainType.EVM:
- publisher = new EvmPublisher(rpcUrl);
- break;
- case ChainType.TVM:
- publisher = new TvmPublisher(rpcUrl);
- break;
- case ChainType.SVM:
- publisher = new SvmPublisher(rpcUrl);
- break;
- default:
- throw new Error(`Unsupported chain type: ${sourceChain.type}`);
- }
-
- // Get sender address
- const senderAddress = getWalletAddr(sourceChain, options);
-
- logger.log(`Sender: ${senderAddress}`);
- logger.log(`Source: ${sourceChain.name} (${sourceChain.id})`);
- logger.log(`Destination: ${destChain.name} (${destChain.id})`);
-
- if (options.dryRun) {
- logger.warning('Dry run - not publishing');
- return;
- }
-
- // Publish
- logger.spinner('Publishing intent to blockchain...');
- const result = await publisher.publish(
- sourceChain.id,
- destChain.id,
- reward,
- encodedRoute,
- privateKey,
- sourcePortal
- );
-
- if (result.success) {
- logger.displayTransactionResult(result);
- } else {
- logger.fail('Publishing failed');
- throw new Error(result.error || 'Publishing failed');
- }
- } catch (error: unknown) {
- const errorMessage = error instanceof Error ? error.message : 'Unknown error';
- logger.error(`Error: ${errorMessage}`);
- if (process.env.DEBUG && error instanceof Error) {
- logger.error(`Stack: ${error.stack}`);
- }
- process.exit(1);
- }
- });
-
- return command;
-}
-
-/**
- * Build intent interactively
- */
-async function buildIntentInteractively(options: PublishCommandOptions) {
- const chains = listChains();
-
- // 1. Get source chain
- let sourceChain: ChainConfig | undefined;
- if (options.source) {
- sourceChain = getChainByName(options.source) || getChainById(BigInt(options.source));
- if (!sourceChain) {
- throw new Error(`Unknown source chain: ${options.source}`);
- }
- } else {
- const { source: sourceId } = await inquirer.prompt([
- {
- type: 'list',
- name: 'source',
- message: 'Select source chain:',
- choices: chains.map(c => ({
- name: `${c.name} (${c.id})`,
- value: c.id,
- })),
- },
- ]);
-
- sourceChain = getChainById(BigInt(sourceId))!;
- }
-
- // 2. Get destination chain
- let destChain: ChainConfig | undefined;
- if (options.destination) {
- destChain = getChainByName(options.destination) || getChainById(BigInt(options.destination));
- if (!destChain) {
- throw new Error(`Unknown destination chain: ${options.destination}`);
- }
- } else {
- const { destination: destinationId } = await inquirer.prompt([
- {
- type: 'list',
- name: 'destination',
- message: 'Select destination chain:',
- choices: chains
- .filter(c => c.id !== sourceChain!.id)
- .map(c => ({ name: `${c.name} (${c.id})`, value: c.id })),
- },
- ]);
- destChain = getChainById(destinationId)!;
- }
-
- // 4. Prompt for reward configuration
- logger.section('💰 Reward Configuration (Source Chain)');
-
- const rewardToken = await selectToken(sourceChain, 'reward');
-
- const { rewardAmountStr } = await inquirer.prompt([
- {
- type: 'input',
- name: 'rewardAmountStr',
- default: '0.1',
- message: `Enter reward amount${rewardToken.symbol ? ` (${rewardToken.symbol})` : ''} in human-readable format (e.g., "10" for 10 tokens):`,
- validate: input => {
- try {
- const num = parseFloat(input);
- if (isNaN(num) || num <= 0) {
- return 'Please enter a positive number';
- }
- return true;
- } catch {
- return 'Invalid amount';
- }
- },
- },
- ]);
-
- // Convert human-readable amount to token units using parseUnits
- const rewardAmount = parseUnits(rewardAmountStr, rewardToken.decimals);
-
- // 7. Prompt for route configuration
- logger.section('📏 Route Configuration (Destination Chain)');
-
- const routeToken = await selectToken(destChain, 'route');
-
- // 7. Prompt for recipient address
- logger.section('👤 Recipient Configuration');
-
- let defaultRecipient: string | undefined;
- try {
- defaultRecipient = getWalletAddr(destChain, options);
- } catch {
- // Ignore default recipient
- }
-
- const { recipientAddress } = await inquirer.prompt([
- {
- type: 'input',
- name: 'recipientAddress',
- message: `Enter recipient address on ${destChain.name} (${destChain.type} chain):`,
- default: defaultRecipient,
- validate: input => {
- if (!input || input.trim() === '') {
- return 'Recipient address is required';
- }
-
- try {
- // Validate the address format based on destination chain type
- switch (destChain.type) {
- case ChainType.EVM:
- if (!isViemAddress(input)) {
- return `Invalid EVM address format. Expected format: 0x... (40 hex characters after 0x)`;
- }
- break;
- case ChainType.TVM:
- if (!TronWeb.isAddress(input)) {
- return `Invalid Tron address format. Expected format: T... (base58) or 41... (hex)`;
- }
- break;
- case ChainType.SVM:
- try {
- new PublicKey(input);
- } catch {
- return `Invalid Solana address format. Expected format: base58 encoded public key`;
- }
- break;
- default:
- return `Unsupported destination chain type: ${destChain.type}`;
- }
-
- // Try to normalize the address to ensure it's fully valid
- AddressNormalizer.normalize(input, destChain.type);
- return true;
- } catch (error: unknown) {
- const errorMessage = error instanceof Error ? error.message : 'Invalid address format';
- return `Invalid address: ${errorMessage}`;
- }
- },
- },
- ]);
-
- // 3. Get wallet address (creator) from private key
- const creatorAddress = AddressNormalizer.normalize(
- getWalletAddr(sourceChain, options),
- sourceChain.type
- );
-
- // Normalize the recipient address
- const normalizedRecipient = AddressNormalizer.normalize(recipientAddress, destChain.type);
-
- // 5. Get quote (with fallback to manual configuration)
- let quote: QuoteResponse | null = null;
-
- logger.spinner('Getting quote...');
- try {
- quote = await getQuote({
- source: sourceChain.id,
- destination: destChain.id,
- funder: AddressNormalizer.denormalize(creatorAddress, sourceChain.type),
- recipient: AddressNormalizer.denormalize(normalizedRecipient, destChain.type),
- amount: rewardAmount,
- routeToken: routeToken.address,
- rewardToken: rewardToken.address,
- });
-
- logger.succeed('Quote fetched');
-
- // Validate contract addresses from quote
- if (quote && (!quote.contracts?.sourcePortal || !quote.contracts?.prover)) {
- logger.warning('Quote response missing required contract addresses');
- quote = null;
- }
- } catch (error: any) {
- logger.stopSpinner();
- if (process.env.DEBUG) {
- console.log(error.stack);
- }
- logger.warning('Quote service unavailable');
- quote = null;
- }
-
- // Variables to hold route/reward data
- let encodedRoute!: Hex;
- let sourcePortal!: UniversalAddress;
- let proverAddress!: UniversalAddress;
- let routeAmountDisplay!: string;
-
- // 6. Set fixed deadlines
- const now = Math.floor(Date.now() / 1000);
- let rewardDeadline = BigInt(now + 2.5 * 60 * 60);
-
- if (quote) {
- // Extract quote data (now unified format from both APIs)
- const quoteData = quote.quoteResponse;
-
- if (!quoteData) {
- logger.warning('Quote response missing quote data');
- quote = null;
- } else {
- encodedRoute = quoteData.encodedRoute as Hex;
- sourcePortal = AddressNormalizer.normalize(quote.contracts.sourcePortal, sourceChain.type);
- proverAddress = AddressNormalizer.normalize(quote.contracts.prover, sourceChain.type);
- routeAmountDisplay = formatUnits(BigInt(quoteData.destinationAmount), routeToken.decimals);
- rewardDeadline = BigInt(quoteData.deadline);
-
- // Display solver-v2 specific fields if available
- if (quoteData.estimatedFulfillTimeSec) {
- logger.info(`Estimated fulfillment time: ${quoteData.estimatedFulfillTimeSec} seconds`);
- }
-
- if (quoteData.intentExecutionType) {
- logger.info(`Execution type: ${quoteData.intentExecutionType}`);
- }
- }
- }
-
- if (!quote) {
- // FALLBACK: Manual configuration
- logger.section('⚠️ Manual Configuration Required');
-
- // Display detailed warning
- logger.warning('Quote service is unavailable. Manual configuration is required.');
- logger.log('');
- logger.log('⚠️ Important:');
- logger.log(' • You must provide the route amount manually');
- logger.log(' • Portal and prover addresses will be needed');
- logger.log(' • Routing may not be optimal without quote service');
- logger.log('');
-
- const { proceedManual } = await inquirer.prompt([
- {
- type: 'confirm',
- name: 'proceedManual',
- message: 'Do you want to proceed with manual configuration?',
- default: true,
- },
- ]);
-
- if (!proceedManual) {
- throw new Error('Publication cancelled by user');
- }
-
- // Prompt for route amount
- const { routeAmountStr } = await inquirer.prompt([
- {
- type: 'input',
- name: 'routeAmountStr',
- message: `Enter expected route amount (tokens to receive on ${destChain.name}):`,
- validate: input => {
- try {
- const num = parseFloat(input);
- if (isNaN(num) || num <= 0) {
- return 'Please enter a positive number';
- }
- return true;
- } catch {
- return 'Invalid amount';
- }
- },
- },
- ]);
-
- const routeAmount = parseUnits(routeAmountStr, routeToken.decimals);
- routeAmountDisplay = routeAmountStr;
-
- // Get or prompt for portal address
- if (sourceChain.portalAddress) {
- sourcePortal = sourceChain.portalAddress;
- logger.log(`Using portal address from config: ${sourcePortal}`);
- } else {
- const { portalAddressInput } = await inquirer.prompt([
- {
- type: 'input',
- name: 'portalAddressInput',
- message: `Enter source portal address for ${sourceChain.name}:`,
- validate: input => {
- try {
- AddressNormalizer.normalize(input, sourceChain.type);
- return true;
- } catch {
- return 'Invalid address format';
- }
- },
- },
- ]);
- sourcePortal = AddressNormalizer.normalize(portalAddressInput, sourceChain.type);
- }
-
- // Get or prompt for prover address
- if (sourceChain.proverAddress) {
- proverAddress = sourceChain.proverAddress;
- logger.log(`Using prover address from config: ${proverAddress}`);
- } else {
- const { proverAddressInput } = await inquirer.prompt([
- {
- type: 'input',
- name: 'proverAddressInput',
- message: `Enter prover address for ${sourceChain.name}:`,
- validate: input => {
- try {
- AddressNormalizer.normalize(input, sourceChain.type);
- return true;
- } catch {
- return 'Invalid address format';
- }
- },
- },
- ]);
- proverAddress = AddressNormalizer.normalize(proverAddressInput, sourceChain.type);
- }
-
- // Build Route object manually
- logger.spinner('Building route manually...');
-
- const now = Math.floor(Date.now() / 1000);
- const routeDeadline = BigInt(now + 2 * 60 * 60); // 2 hours
-
- // Encode transfer function call for route token
- const transferCallData = encodeFunctionData({
- abi: erc20Abi,
- functionName: 'transfer',
- args: [
- AddressNormalizer.denormalize(normalizedRecipient, destChain.type) as `0x${string}`,
- routeAmount,
- ],
- });
-
- const route: Intent['route'] = {
- salt: `0x${crypto.randomBytes(32).toString('hex')}` as Hex,
- deadline: routeDeadline,
- portal: sourcePortal,
- nativeAmount: 0n,
- tokens: [
- {
- token: AddressNormalizer.normalize(routeToken.address, destChain.type),
- amount: routeAmount,
- },
- ],
- calls: [
- {
- target: AddressNormalizer.normalize(routeToken.address, destChain.type),
- data: transferCallData,
- value: 0n,
- },
- ],
- };
-
- // Encode the route
- encodedRoute = PortalEncoder.encode(route, destChain.type);
- logger.succeed('Route built and encoded');
- }
-
- // 7. Build reward using addresses from quote or manual input
- const reward: Intent['reward'] = {
- deadline: rewardDeadline,
- prover: proverAddress,
- creator: creatorAddress,
- nativeAmount: 0n,
- tokens: [
- {
- token: AddressNormalizer.normalize(rewardToken.address, sourceChain.type),
- amount: rewardAmount,
- },
- ],
- };
-
- logger.displayIntentSummary({
- source: `${sourceChain.name} (${sourceChain.id})`,
- destination: `${destChain.name} (${destChain.id})`,
- creator: AddressNormalizer.denormalize(creatorAddress, sourceChain.type),
- recipient: normalizedRecipient,
- rewardDeadline: new Date(Number(rewardDeadline) * 1000).toLocaleString(),
- routeToken: `${routeToken.address}${routeToken.symbol ? ` (${routeToken.symbol})` : ''}`,
- routeAmount: routeAmountDisplay,
- rewardToken: `${rewardToken.address}${rewardToken.symbol ? ` (${rewardToken.symbol})` : ''}`,
- rewardAmount: `${rewardAmountStr} (${rewardAmount.toString()} units)`,
- });
-
- const { confirm } = await inquirer.prompt([
- {
- type: 'confirm',
- name: 'confirm',
- message: 'Publish this intent?',
- default: true,
- },
- ]);
-
- if (!confirm) {
- throw new Error('Publication cancelled by user');
- }
-
- return {
- reward,
- encodedRoute,
- sourceChain,
- destChain,
- sourcePortal,
- };
-}
-
-/**
- * Select a token for a specific chain
- */
-async function selectToken(
- chain: ChainConfig,
- type: string
-): Promise<{ address: BlockchainAddress; decimals: number; symbol?: string }> {
- // Get available tokens for this chain
- const allTokens = listTokens();
- const chainTokens = allTokens.filter(token => {
- const address = getTokenAddress(token.symbol, chain.id);
- return address !== undefined;
- });
-
- const choices = [
- ...chainTokens.map(t => ({
- name: `${t.symbol} - ${t.name}`,
- value: t.symbol,
- })),
- { name: 'Custom Token Address', value: 'CUSTOM' },
- ];
-
- const { tokenChoice } = await inquirer.prompt([
- {
- type: 'list',
- name: 'tokenChoice',
- message: `Select ${type} token:`,
- choices,
- },
- ]);
-
- if (tokenChoice === 'CUSTOM') {
- const { address, decimals } = await inquirer.prompt([
- {
- type: 'input',
- name: 'address',
- message: 'Enter token address:',
- validate: input => {
- try {
- AddressNormalizer.normalize(input, chain.type);
- return true;
- } catch {
- return 'Invalid address format';
- }
- },
- },
- {
- type: 'input',
- name: 'decimals',
- message: 'Enter token decimals (e.g., 18 for most ERC20, 6 for USDC):',
- default: '18',
- validate: input => {
- const num = parseInt(input);
- return !isNaN(num) && num >= 0 && num <= 255
- ? true
- : 'Please enter a valid number between 0 and 255';
- },
- },
- ]);
- return { address, decimals: parseInt(decimals) };
- }
-
- // Get token config for selected symbol
- const tokenConfig = getTokenBySymbol(tokenChoice);
- if (!tokenConfig) {
- throw new Error(`Token ${tokenChoice} not found`);
- }
-
- const tokenAddress = getTokenAddress(tokenChoice, chain.id);
- if (!tokenAddress) {
- throw new Error(`Token ${tokenChoice} not available on chain ${chain.id}`);
- }
-
- // Denormalize the token address to chain-native format for display
- return {
- address: AddressNormalizer.denormalize(tokenAddress, chain.type),
- decimals: tokenConfig.decimals,
- symbol: tokenConfig.symbol,
- };
-}
-
-export function getWalletAddr(
- chain: ChainConfig,
- options?: PublishCommandOptions
-): BlockchainAddress {
- const privateKey = getPrivateKey(chain, options?.privateKey);
-
- if (!privateKey) {
- throw new Error(`No private key configured for ${chain.type} chain`);
- }
-
- switch (chain.type) {
- case ChainType.EVM:
- const account = privateKeyToAccount(privateKey as Hex);
- return account.address;
- case ChainType.TVM:
- const tronAddress = TronWeb.address.fromPrivateKey(privateKey);
- if (!tronAddress) {
- throw new Error('Invalid Tron private key');
- }
- return tronAddress as TronAddress;
- case ChainType.SVM:
- let keypair: Keypair;
- if (privateKey.startsWith('[')) {
- const bytes = JSON.parse(privateKey);
- keypair = Keypair.fromSecretKey(new Uint8Array(bytes));
- } else {
- // eslint-disable-next-line @typescript-eslint/no-require-imports
- const bs58 = require('bs58');
- const bytes = bs58.decode(privateKey);
- keypair = Keypair.fromSecretKey(bytes);
- }
- return keypair.publicKey.toBase58() as SvmAddress;
- default:
- throw new Error('Unknown chain type');
- }
-}
-
-function getPrivateKey(chain: ChainConfig, privateKey?: string) {
- // Load configuration
- const env = loadEnvConfig();
-
- // Determine private key
- if (!privateKey) {
- switch (chain.type) {
- case ChainType.EVM:
- privateKey = env.evmPrivateKey;
- break;
- case ChainType.TVM:
- privateKey = env.tvmPrivateKey;
- break;
- case ChainType.SVM:
- privateKey = env.svmPrivateKey;
- break;
- }
- }
-
- if (!privateKey) {
- throw new Error(`No private key provided for ${chain.type} chain`);
- }
-
- return privateKey;
-}
diff --git a/src/commands/status.ts b/src/commands/status.ts
deleted file mode 100644
index fac5197..0000000
--- a/src/commands/status.ts
+++ /dev/null
@@ -1,299 +0,0 @@
-/**
- * Status Command
- */
-
-import { Command } from 'commander';
-import { Address, createPublicClient, getContract, Hex, http } from 'viem';
-
-import { portalAbi } from '@/commons/abis/portal.abi';
-import { ChainConfig, getChainById, getChainByName } from '@/config/chains';
-import { ChainType } from '@/core/interfaces/intent';
-import { AddressNormalizer } from '@/core/utils/address-normalizer';
-import { chalk, logger } from '@/utils/logger';
-
-interface IntentStatus {
- intentHash: string;
- isFulfilled: boolean;
- claimant?: string;
- transactionHash?: string;
- blockNumber?: bigint;
- timestamp?: Date;
-}
-
-interface StatusCommandOptions {
- chain?: string;
- watch?: boolean;
- json?: boolean;
- verbose?: boolean;
-}
-
-export function createStatusCommand(): Command {
- const command = new Command('status');
-
- command
- .description('Check the fulfillment status of an intent')
- .argument('', 'Intent hash to check (0x-prefixed hex string)')
- .option('-c, --chain ', 'Destination chain (name or ID)')
- .option('-w, --watch', 'Watch for status updates (poll every 30 seconds)')
- .option('--json', 'Output in JSON format')
- .option('--verbose', 'Show detailed information')
- .action(async (intentHashArg: string, options) => {
- try {
- // Validate intent hash format
- if (!intentHashArg.startsWith('0x') || intentHashArg.length !== 66) {
- throw new Error('Intent hash must be a 0x-prefixed 64-character hex string');
- }
-
- const intentHash = intentHashArg as Hex;
-
- // Get destination chain
- let destChain: ChainConfig | undefined;
- if (options.chain) {
- // Try to get by name first, then by ID
- destChain = getChainByName(options.chain) || getChainById(options.chain);
- if (!destChain) {
- throw new Error(`Unknown chain: ${options.chain}`);
- }
- } else {
- throw new Error('Destination chain is required. Use --chain option.');
- }
-
- // Only EVM chains are supported for now
- if (destChain.type !== ChainType.EVM) {
- throw new Error('Status checking is currently only supported for EVM chains');
- }
-
- if (!options.json && !options.watch) {
- logger.title(`🔍 Checking Intent Status`);
- logger.info(`Intent Hash: ${intentHash}`);
- logger.info(`Chain: ${destChain.name} (${destChain.id})`);
- logger.info('');
- }
-
- if (options.watch) {
- await watchIntentStatus(intentHash, destChain, options);
- } else {
- const status = await getIntentStatus(intentHash, destChain, options.verbose);
- displayStatus(status, options);
- }
- } catch (error) {
- if (options.json) {
- logger.log(
- JSON.stringify(
- { error: error instanceof Error ? error.message : String(error) },
- null,
- 2
- )
- );
- } else {
- logger.error(
- `Error checking intent status: ${error instanceof Error ? error.message : String(error)}`
- );
- }
- process.exit(1);
- }
- });
-
- return command;
-}
-
-async function getIntentStatus(
- intentHash: Hex,
- chain: ChainConfig,
- verbose: boolean = false
-): Promise {
- // Create public client for the destination chain
- const client = createPublicClient({
- chain: {
- id: Number(chain.id),
- name: chain.name,
- network: chain.name.toLowerCase(),
- nativeCurrency: chain.nativeCurrency,
- rpcUrls: {
- default: { http: [chain.rpcUrl] },
- public: { http: [chain.rpcUrl] },
- },
- },
- transport: http(chain.rpcUrl),
- });
-
- // Get the portal address (denormalized for EVM)
- if (!chain.portalAddress) {
- throw new Error(`No portal address configured for chain ${chain.name}`);
- }
-
- const portalAddress = AddressNormalizer.denormalize(
- chain.portalAddress,
- ChainType.EVM
- ) as Address;
-
- if (verbose) {
- logger.info(`Querying Portal contract: ${portalAddress}`);
- }
-
- // Create contract instance
- const portalContract = getContract({
- address: portalAddress,
- abi: portalAbi,
- client,
- });
-
- try {
- // TODO: Must query the last 10k blocks in 1k intervals
- // Query for IntentFulfilled events
- const [event] = await portalContract.getEvents.IntentFulfilled({
- intentHash,
- });
-
- if (process.env.DEBUG) {
- logger.log(`Event: ${JSON.stringify({ event, portalAddress, client: client.chain.name })}`);
- }
-
- const status: IntentStatus = {
- intentHash,
- isFulfilled: Boolean(event),
- };
-
- if (status.isFulfilled) {
- // Get the most recent fulfillment event
-
- status.claimant = event.args.claimant;
- status.transactionHash = event.transactionHash;
- status.blockNumber = event.blockNumber;
-
- // Get block timestamp
- if (event.blockNumber) {
- const block = await client.getBlock({ blockNumber: event.blockNumber });
- status.timestamp = new Date(Number(block.timestamp) * 1000);
- }
-
- if (verbose && event.transactionHash) {
- logger.info(`Fulfillment transaction: ${event.transactionHash}`);
- logger.info(`Block number: ${event.blockNumber}`);
- }
- }
-
- return status;
- } catch (error) {
- throw new Error(
- `Failed to query Portal contract: ${error instanceof Error ? error.message : String(error)}`
- );
- }
-}
-
-async function watchIntentStatus(
- intentHash: Hex,
- chain: ChainConfig,
- options: StatusCommandOptions
-): Promise {
- const POLL_INTERVAL = 10_000; // 30 seconds
-
- if (!options.json) {
- logger.title(`👀 Watching Intent Status`);
- logger.info(`Polling every 10 seconds... (Press Ctrl+C to stop)`);
- logger.info('');
- }
-
- let lastStatus: IntentStatus | null = null;
-
- while (true) {
- try {
- const status = await getIntentStatus(intentHash, chain, options.verbose);
-
- // Only display if status changed or it's the first check
- if (!lastStatus || status.isFulfilled !== lastStatus.isFulfilled) {
- if (options.json) {
- logger.log(
- JSON.stringify(
- {
- timestamp: new Date().toISOString(),
- ...status,
- },
- null,
- 2
- )
- );
- } else {
- if (lastStatus) {
- logger.info(`Status changed at ${new Date().toLocaleTimeString()}`);
- }
- displayStatus(status, options);
-
- if (status.isFulfilled) {
- logger.succeed('Intent fulfilled! Stopping watch...');
- break;
- }
- }
- lastStatus = status;
- }
-
- if (!options.json && !status.isFulfilled) {
- process.stdout.write(
- `\rLast checked: ${new Date().toLocaleTimeString()} - Status: Pending...`
- );
- }
-
- await new Promise(resolve => setTimeout(resolve, POLL_INTERVAL));
- } catch (error) {
- if (options.json) {
- logger.log(
- JSON.stringify(
- {
- timestamp: new Date().toISOString(),
- error: error instanceof Error ? error.message : String(error),
- },
- null,
- 2
- )
- );
- } else {
- logger.error(
- `Error during watch: ${error instanceof Error ? error.message : String(error)}`
- );
- }
-
- // Wait before retrying
- await new Promise(resolve => setTimeout(resolve, POLL_INTERVAL));
- }
- }
-}
-
-function displayStatus(status: IntentStatus, options: StatusCommandOptions): void {
- if (options.json) {
- logger.log(
- JSON.stringify(
- status,
- (key, value) => {
- // Convert BigInt to string for JSON serialization
- return typeof value === 'bigint' ? value.toString() : value;
- },
- 2
- )
- );
- return;
- }
-
- // Human-readable display
- const statusText = status.isFulfilled ? chalk.green('✅ Fulfilled') : chalk.yellow('⏳ Pending');
-
- logger.info(`Status: ${statusText}`);
-
- if (status.isFulfilled && status.claimant) {
- logger.info(`Solver (Claimant): ${status.claimant}`);
-
- if (status.transactionHash) {
- logger.info(`Fulfillment Transaction: ${status.transactionHash}`);
- }
-
- if (status.blockNumber) {
- logger.info(`Block Number: ${status.blockNumber.toString()}`);
- }
-
- if (status.timestamp) {
- logger.info(`Fulfilled At: ${status.timestamp.toLocaleString()}`);
- }
- } else if (!status.isFulfilled) {
- logger.info('The intent has not been fulfilled yet.');
- logger.info('Solvers are still working to execute this intent.');
- }
-}
diff --git a/src/commons/utils/buffer.ts b/src/commons/utils/buffer.ts
index 63e3b0d..0733adc 100644
--- a/src/commons/utils/buffer.ts
+++ b/src/commons/utils/buffer.ts
@@ -1,3 +1,3 @@
import { Hex } from 'viem';
-export const toBuffer = (hex: Hex) => Buffer.from(hex.slice(2), 'hex');
+export const toBuffer = (hex: Hex): Buffer => Buffer.from(hex.slice(2), 'hex');
diff --git a/src/commons/utils/converter.ts b/src/commons/utils/converter.ts
index a18d62a..98f5d4f 100644
--- a/src/commons/utils/converter.ts
+++ b/src/commons/utils/converter.ts
@@ -2,7 +2,7 @@ import { PublicKey } from '@solana/web3.js';
import { Hex } from 'viem';
import { toBuffer } from '@/commons/utils/buffer';
-import { SvmAddress } from '@/core/types/blockchain-addresses';
+import { SvmAddress } from '@/shared/types';
export function addressToBytes32(address: string): number[] {
// Convert Solana address or hex address to 32-byte array
diff --git a/src/commons/utils/instruments.ts b/src/commons/utils/instruments.ts
index f7ecee0..e8a6e62 100644
--- a/src/commons/utils/instruments.ts
+++ b/src/commons/utils/instruments.ts
@@ -1,7 +1,7 @@
import { BN, web3 } from '@coral-xyz/anchor';
-import { Intent } from '@/core/interfaces/intent';
-import { AddressNormalizer } from '@/core/utils/address-normalizer';
+import { AddressNormalizer } from '@/blockchain/utils/address-normalizer';
+import { Intent } from '@/shared/types';
import { PortalIdlTypes } from '../types/portal-idl.type';
import * as PortalIdlCoder from '../types/portal-idl-coder.type';
diff --git a/src/commons/utils/portal-borsh-coder.ts b/src/commons/utils/portal-borsh-coder.ts
index 02551b7..53a1b05 100644
--- a/src/commons/utils/portal-borsh-coder.ts
+++ b/src/commons/utils/portal-borsh-coder.ts
@@ -2,7 +2,7 @@ import { BorshCoder, Idl } from '@coral-xyz/anchor';
import { getPortalIdlByNetwork, Network } from '@/commons/idls/portal.idl';
-export function getPortalBorshCoder(network: Network) {
+export function getPortalBorshCoder(network: Network): BorshCoder {
const idl = getPortalIdlByNetwork(network);
return new BorshCoder(idl as unknown as Idl);
}
diff --git a/src/commons/utils/portal-hash.utils.ts b/src/commons/utils/portal-hash.utils.ts
index 2c15ce8..645e5f8 100644
--- a/src/commons/utils/portal-hash.utils.ts
+++ b/src/commons/utils/portal-hash.utils.ts
@@ -7,26 +7,29 @@
import { encodePacked, Hex, keccak256 } from 'viem';
-import { Intent } from '@/core/interfaces/intent';
-import { ChainTypeDetector } from '@/core/utils/chain-detector';
-import { PortalEncoder } from '@/core/utils/portal-encoder';
+import { PortalEncoder } from '@/blockchain/utils/portal-encoder';
+import { ChainType, Intent } from '@/shared/types';
+
+const TVM_CHAIN_IDS = new Set([728126428, 2494104990]);
+const SVM_CHAIN_IDS = new Set([1399811149, 1399811150, 1399811151]);
+
+function detectChainType(chainId: bigint): ChainType {
+ const id = Number(chainId);
+ if (TVM_CHAIN_IDS.has(id)) return ChainType.TVM;
+ if (SVM_CHAIN_IDS.has(id)) return ChainType.SVM;
+ return ChainType.EVM;
+}
export class PortalHashUtils {
static getIntentHash(intent: Intent): { intentHash: Hex; routeHash: Hex; rewardHash: Hex } {
const routeHash = PortalHashUtils.computeRouteHash(intent.route, intent.destination);
const rewardHash = PortalHashUtils.computeRewardHash(intent.reward, intent.sourceChainId);
- // Compute the intent hash using encodePacked
- // intentHash = keccak256(abi.encodePacked(destination, routeHash, rewardHash))
const intentHash = keccak256(
encodePacked(['uint64', 'bytes32', 'bytes32'], [intent.destination, routeHash, rewardHash])
);
- return {
- intentHash,
- routeHash,
- rewardHash,
- };
+ return { intentHash, routeHash, rewardHash };
}
static getIntentHashFromReward(
@@ -38,43 +41,21 @@ export class PortalHashUtils {
const routeHash = keccak256(encodedRoute);
const rewardHash = PortalHashUtils.computeRewardHash(reward, source);
- // Compute the intent hash using encodePacked
- // intentHash = keccak256(abi.encodePacked(destination, routeHash, rewardHash))
const intentHash = keccak256(
encodePacked(['uint64', 'bytes32', 'bytes32'], [destination, routeHash, rewardHash])
);
- return {
- intentHash,
- routeHash,
- rewardHash,
- };
+ return { intentHash, routeHash, rewardHash };
}
- /**
- * Computes route hash using source chain encoding
- * Accepts both Intent route (with UniversalAddress) and EVMIntent route
- *
- * @param route - Route data structure
- * @param destination - Destination chain id
- * @returns Route hash as Hex
- */
static computeRouteHash(route: Intent['route'], destination: bigint): Hex {
- const chainType = ChainTypeDetector.detect(destination);
+ const chainType = detectChainType(destination);
const routeEncoded = PortalEncoder.encode(route, chainType);
return keccak256(routeEncoded);
}
- /**
- * Computes reward hash using source chain encoding
- * Accepts both Intent reward (with UniversalAddress)
- *
- * @param reward - Reward data structure
- * @param sourceChainId - Source chain ID to determine encoding type
- * @returns Reward hash as Hex
- */
static computeRewardHash(reward: Intent['reward'], sourceChainId: bigint): Hex {
- const chainType = ChainTypeDetector.detect(sourceChainId);
+ const chainType = detectChainType(sourceChainId);
const rewardEncoded = PortalEncoder.encode(reward, chainType);
return keccak256(rewardEncoded);
}
diff --git a/src/commons/utils/tvm-utils.ts b/src/commons/utils/tvm-utils.ts
index 4256e5e..af52bd8 100644
--- a/src/commons/utils/tvm-utils.ts
+++ b/src/commons/utils/tvm-utils.ts
@@ -1,6 +1,6 @@
import { TronWeb } from 'tronweb';
-import { EvmAddress, TronAddress } from '@/core/types/blockchain-addresses';
+import { EvmAddress, TronAddress } from '@/shared/types';
/**
* Utility service for TVM-specific operations like address conversions
diff --git a/src/config/chains.ts b/src/config/chains.ts
deleted file mode 100644
index 451109d..0000000
--- a/src/config/chains.ts
+++ /dev/null
@@ -1,299 +0,0 @@
-/**
- * Chain Configuration
- */
-
-import { arbitrum, bsc, hyperEvm, mainnet, polygon, ronin, sonic } from 'viem/chains';
-
-import { ChainType } from '@/core/interfaces/intent';
-import { BlockchainAddress } from '@/core/types/blockchain-addresses';
-import { UniversalAddress } from '@/core/types/universal-address';
-import { AddressNormalizer } from '@/core/utils/address-normalizer';
-import { logger } from '@/utils/logger';
-
-export interface ChainConfig {
- id: bigint;
- name: string;
- env: 'production' | 'development';
- type: ChainType;
- rpcUrl: string;
- portalAddress?: UniversalAddress;
- proverAddress?: UniversalAddress;
- nativeCurrency: {
- name: string;
- symbol: string;
- decimals: number;
- };
-}
-
-// Default chain configurations
-const chains: Record = {
- // EVM Chains
- ethereum: {
- id: 1n,
- name: 'Ethereum',
- type: ChainType.EVM,
- env: 'production',
- rpcUrl: mainnet.rpcUrls.default.http[0],
- nativeCurrency: mainnet.nativeCurrency,
- },
- optimism: {
- id: 10n,
- name: 'Optimism',
- type: ChainType.EVM,
- env: 'production',
- rpcUrl: 'https://mainnet.optimism.io',
- nativeCurrency: {
- name: 'Ether',
- symbol: 'ETH',
- decimals: 18,
- },
- },
- bsc: {
- id: BigInt(bsc.id),
- name: bsc.name,
- type: ChainType.EVM,
- env: 'production',
- rpcUrl: bsc.rpcUrls.default.http[0],
- nativeCurrency: bsc.nativeCurrency,
- },
- base: {
- id: 8453n,
- name: 'Base',
- type: ChainType.EVM,
- env: 'production',
- rpcUrl: 'https://mainnet.base.org',
- nativeCurrency: {
- name: 'Ether',
- symbol: 'ETH',
- decimals: 18,
- },
- },
- arbitrum: {
- id: BigInt(arbitrum.id),
- name: arbitrum.name,
- type: ChainType.EVM,
- env: 'production',
- rpcUrl: arbitrum.rpcUrls.default.http[0],
- nativeCurrency: {
- name: 'Ether',
- symbol: 'ETH',
- decimals: 18,
- },
- },
- polygon: {
- id: BigInt(polygon.id),
- name: polygon.name,
- type: ChainType.EVM,
- env: 'production',
- rpcUrl: polygon.rpcUrls.default.http[0],
- nativeCurrency: polygon.nativeCurrency,
- },
- ronin: {
- id: BigInt(ronin.id),
- name: ronin.name,
- type: ChainType.EVM,
- env: 'production',
- rpcUrl: ronin.rpcUrls.default.http[0],
- nativeCurrency: ronin.nativeCurrency,
- },
-
- sonic: {
- id: BigInt(sonic.id),
- name: sonic.name,
- type: ChainType.EVM,
- env: 'production',
- rpcUrl: sonic.rpcUrls.default.http[0],
- nativeCurrency: sonic.nativeCurrency,
- },
-
- hyperevm: {
- id: BigInt(hyperEvm.id),
- name: hyperEvm.name,
- type: ChainType.EVM,
- env: 'production',
- rpcUrl: hyperEvm.rpcUrls.default.http[0],
- nativeCurrency: hyperEvm.nativeCurrency,
- },
-
- // Testnet Chains
- 'base-sepolia': {
- id: 84532n,
- name: 'Base Sepolia',
- type: ChainType.EVM,
- env: 'development',
- rpcUrl: 'https://sepolia.base.org',
- portalAddress: AddressNormalizer.normalize(
- '0x06EFdb68dbF245ECb49E3aE10Cd0f893B674443c',
- ChainType.EVM
- ),
- proverAddress: AddressNormalizer.normalize(
- '0x9523b6c0caac8122dbd5dd1c1d336ceba637038d',
- ChainType.EVM
- ),
- nativeCurrency: {
- name: 'Ether',
- symbol: 'ETH',
- decimals: 18,
- },
- },
- 'optimism-sepolia': {
- id: 11155420n,
- name: 'Optimism Sepolia',
- type: ChainType.EVM,
- env: 'development',
- rpcUrl: 'https://sepolia.optimism.io',
- portalAddress: AddressNormalizer.normalize(
- '0x06EFdb68dbF245ECb49E3aE10Cd0f893B674443c',
- ChainType.EVM
- ),
- proverAddress: AddressNormalizer.normalize(
- '0x9523b6c0caac8122dbd5dd1c1d336ceba637038d',
- ChainType.EVM
- ),
- nativeCurrency: {
- name: 'Ether',
- symbol: 'ETH',
- decimals: 18,
- },
- },
- 'plasma-testnet': {
- id: 9746n,
- name: 'Plasma Testnet',
- type: ChainType.EVM,
- env: 'development',
- rpcUrl: 'https://rpc.testnet.plasm.technology',
- portalAddress: AddressNormalizer.normalize(
- '0x06EFdb68dbF245ECb49E3aE10Cd0f893B674443c',
- ChainType.EVM
- ),
- proverAddress: AddressNormalizer.normalize(
- '0x9523b6c0caac8122dbd5dd1c1d336ceba637038d',
- ChainType.EVM
- ),
- nativeCurrency: {
- name: 'Ether',
- symbol: 'ETH',
- decimals: 18,
- },
- },
- sepolia: {
- id: 11155111n,
- name: 'Sepolia',
- type: ChainType.EVM,
- env: 'development',
- rpcUrl: 'https://rpc.sepolia.org',
- portalAddress: AddressNormalizer.normalize(
- '0x06EFdb68dbF245ECb49E3aE10Cd0f893B674443c',
- ChainType.EVM
- ),
- proverAddress: AddressNormalizer.normalize(
- '0x9523b6c0caac8122dbd5dd1c1d336ceba637038d',
- ChainType.EVM
- ),
- nativeCurrency: {
- name: 'Ether',
- symbol: 'ETH',
- decimals: 18,
- },
- },
-
- // TVM Chains
- tron: {
- id: 728126428n,
- name: 'Tron',
- type: ChainType.TVM,
- env: 'production',
- rpcUrl: 'https://api.trongrid.io',
- nativeCurrency: {
- name: 'Tron',
- symbol: 'TRX',
- decimals: 6,
- },
- },
- 'tron-shasta': {
- id: 2494104990n,
- name: 'Tron Shasta',
- type: ChainType.TVM,
- env: 'development',
- rpcUrl: 'https://api.shasta.trongrid.io',
- nativeCurrency: {
- name: 'Tron',
- symbol: 'TRX',
- decimals: 6,
- },
- },
-
- // SVM Chains
- solana: {
- id: 1399811149n,
- name: 'Solana',
- type: ChainType.SVM,
- env: 'production',
- rpcUrl: 'https://api.mainnet-beta.solana.com',
- nativeCurrency: {
- name: 'Solana',
- symbol: 'SOL',
- decimals: 9,
- },
- },
-
- 'solana-devnet': {
- id: 1399811150n, // Solana devnet chain ID (from onchain)
- name: 'Solana Devnet',
- type: ChainType.SVM,
- env: 'development',
- rpcUrl: 'https://api.devnet.solana.com',
- nativeCurrency: {
- name: 'Solana',
- symbol: 'SOL',
- decimals: 9,
- },
- },
-};
-
-const ENV = process.env.NODE_CHAINS_ENV || 'production';
-export const CHAIN_CONFIGS: typeof chains = Object.fromEntries(
- Object.entries(chains).filter(([, chain]) => chain.env === ENV)
-);
-
-// Helper function to get chain by ID
-export function getChainById(chainId: bigint): ChainConfig | undefined {
- return Object.values(CHAIN_CONFIGS).find(chain => chain.id.toString() === chainId.toString());
-}
-
-// Helper function to get chain by name
-export function getChainByName(name: string): ChainConfig | undefined {
- return CHAIN_CONFIGS[name.toLowerCase()];
-}
-
-// Helper function to list all supported chains
-export function listChains(): ChainConfig[] {
- return Object.values(CHAIN_CONFIGS);
-}
-
-// Update Portal address from environment if available
-export function updatePortalAddresses(env: Record) {
- const addressMappings: Record = {
- PORTAL_ADDRESS_ETH: 'ethereum',
- PORTAL_ADDRESS_OPTIMISM: 'optimism',
- PORTAL_ADDRESS_BASE: 'base',
- PORTAL_ADDRESS_TRON: 'tron',
- PORTAL_ADDRESS_SOLANA: 'solana',
- };
-
- for (const [envKey, chainKey] of Object.entries(addressMappings)) {
- const address = env[envKey];
- if (address && CHAIN_CONFIGS[chainKey]) {
- try {
- CHAIN_CONFIGS[chainKey].portalAddress = AddressNormalizer.normalize(
- address as BlockchainAddress,
- CHAIN_CONFIGS[chainKey].type
- );
- } catch (error) {
- logger.warning(
- `Failed to set portal address for ${chainKey}: ${error instanceof Error ? error.message : String(error)}`
- );
- }
- }
- }
-}
diff --git a/src/config/config.module.ts b/src/config/config.module.ts
new file mode 100644
index 0000000..8a3622e
--- /dev/null
+++ b/src/config/config.module.ts
@@ -0,0 +1,32 @@
+import { Global, Module } from '@nestjs/common';
+import { ConfigModule as NestConfigModule } from '@nestjs/config';
+
+import { EnvSchema } from './validation/env.schema';
+import { ConfigService } from './config.service';
+
+@Global()
+@Module({
+ imports: [
+ NestConfigModule.forRoot({
+ isGlobal: true,
+ validate: config => {
+ const result = EnvSchema.safeParse(config);
+ if (!result.success) {
+ const lines = result.error.issues.map(
+ issue => ` ${issue.path.join('.')}: ${issue.message}`
+ );
+ console.error(
+ '\nConfiguration error: invalid or missing environment variables\n\n' +
+ lines.join('\n') +
+ '\n\nCopy .env.example to .env and fill in the required values.\n'
+ );
+ process.exit(1);
+ }
+ return result.data;
+ },
+ }),
+ ],
+ providers: [ConfigService],
+ exports: [ConfigService],
+})
+export class ConfigModule {}
diff --git a/src/config/config.service.ts b/src/config/config.service.ts
new file mode 100644
index 0000000..d723cc3
--- /dev/null
+++ b/src/config/config.service.ts
@@ -0,0 +1,79 @@
+import { Injectable } from '@nestjs/common';
+import { ConfigService as NestConfigService } from '@nestjs/config';
+
+import { Hex } from 'viem';
+
+import { ChainType } from '@/shared/types';
+
+@Injectable()
+export class ConfigService {
+ constructor(private readonly config: NestConfigService) {}
+
+ getEvmPrivateKey(): Hex | undefined {
+ return this.config.get('EVM_PRIVATE_KEY');
+ }
+
+ getTvmPrivateKey(): string | undefined {
+ return this.config.get('TVM_PRIVATE_KEY');
+ }
+
+ getSvmPrivateKey(): string | undefined {
+ return this.config.get('SVM_PRIVATE_KEY');
+ }
+
+ getKeyForChainType(chainType: ChainType): string | undefined {
+ switch (chainType) {
+ case ChainType.EVM:
+ return this.getEvmPrivateKey();
+ case ChainType.TVM:
+ return this.getTvmPrivateKey();
+ case ChainType.SVM:
+ return this.getSvmPrivateKey();
+ }
+ }
+
+ getRpcUrl(chainType: ChainType, variant: 'primary' | 'fallback' = 'primary'): string | undefined {
+ const map: Record> = {
+ [ChainType.EVM]: {
+ primary: '',
+ fallback: '',
+ },
+ [ChainType.TVM]: {
+ primary: this.config.get('TVM_RPC_URL') ?? 'https://api.trongrid.io',
+ fallback: this.config.get('TVM_RPC_URL_2') ?? 'https://tron.publicnode.com',
+ },
+ [ChainType.SVM]: {
+ primary: this.config.get('SVM_RPC_URL') ?? 'https://api.mainnet-beta.solana.com',
+ fallback: this.config.get('SVM_RPC_URL_2') ?? 'https://solana.publicnode.com',
+ },
+ };
+ return map[chainType][variant] || undefined;
+ }
+
+ getQuoteEndpoint(): { url: string; type: 'solver-v2' | 'preprod' | 'production' } {
+ const solverUrl = this.config.get('SOLVER_URL');
+ if (solverUrl) {
+ return { url: `${solverUrl}/api/v2/quote/reverse`, type: 'solver-v2' };
+ }
+ if (this.config.get('QUOTES_API_URL') || this.config.get('QUOTES_PREPROD')) {
+ return { url: 'https://quotes-preprod.eco.com/api/v3/quotes/single', type: 'preprod' };
+ }
+ return { url: 'https://quotes.eco.com/api/v3/quotes/single', type: 'production' };
+ }
+
+ getDeadlineOffsetSeconds(): number {
+ return this.config.get('DEADLINE_OFFSET_SECONDS') ?? 9000;
+ }
+
+ getDappId(): string {
+ return this.config.get('DAPP_ID') ?? 'eco-routes-cli';
+ }
+
+ getChainsEnv(): 'production' | 'development' {
+ return this.config.get<'production' | 'development'>('NODE_CHAINS_ENV') ?? 'production';
+ }
+
+ isDebug(): boolean {
+ return !!this.config.get('DEBUG');
+ }
+}
diff --git a/src/config/env.ts b/src/config/env.ts
deleted file mode 100644
index e3c4667..0000000
--- a/src/config/env.ts
+++ /dev/null
@@ -1,31 +0,0 @@
-/**
- * Environment Configuration
- */
-
-import * as dotenv from 'dotenv';
-import { Hex } from 'viem';
-
-// Load environment variables
-dotenv.config();
-
-export interface EnvConfig {
- evmPrivateKey?: Hex;
- tvmPrivateKey?: string;
- svmPrivateKey?: string;
- evmRpcUrl?: string;
- tvmRpcUrl?: string;
- svmRpcUrl?: string;
- solverUrl?: string;
-}
-
-export function loadEnvConfig(): EnvConfig {
- return {
- evmPrivateKey: process.env.EVM_PRIVATE_KEY as Hex | undefined,
- tvmPrivateKey: process.env.TVM_PRIVATE_KEY,
- svmPrivateKey: process.env.SVM_PRIVATE_KEY,
- evmRpcUrl: process.env.EVM_RPC_URL,
- tvmRpcUrl: process.env.TVM_RPC_URL || 'https://api.trongrid.io',
- svmRpcUrl: process.env.SVM_RPC_URL || 'https://api.mainnet-beta.solana.com',
- solverUrl: process.env.SOLVER_URL,
- };
-}
diff --git a/src/config/tokens.config.ts b/src/config/tokens.config.ts
new file mode 100644
index 0000000..de7f3e8
--- /dev/null
+++ b/src/config/tokens.config.ts
@@ -0,0 +1,250 @@
+/**
+ * Token Configuration
+ */
+
+import { AddressNormalizer } from '@/blockchain/utils/address-normalizer';
+import { EvmAddress, SvmAddress, TronAddress, UniversalAddress } from '@/shared/types';
+
+/** Describes a cross-chain token and its deployed contract addresses. */
+export interface TokenConfig {
+ /** Ticker symbol, e.g. `"USDC"`, `"USDT"`. */
+ symbol: string;
+ /** Human-readable name, e.g. `"USD Coin"`. */
+ name: string;
+ /**
+ * Number of decimal places for the smallest unit.
+ * Used to convert between human-readable amounts and on-chain integers
+ * (e.g. `6` for USDC: `1 USDC = 1_000_000` base units).
+ */
+ decimals: number;
+ /**
+ * Map of chain ID (as decimal string) to Universal-format token address.
+ *
+ * String keys are required because `bigint` cannot be a JavaScript object key.
+ * Lookup pattern: `token.addresses[chainId.toString()]`
+ *
+ * @example `{ "8453": "0x000...abc", "1": "0x000...def" }`
+ */
+ addresses: Record;
+}
+
+// Common token configurations
+export const TOKEN_CONFIGS: Record = {
+ USDC: {
+ symbol: 'USDC',
+ name: 'USD Coin',
+ decimals: 6,
+ addresses: {
+ '1': AddressNormalizer.normalizeEvm(
+ '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48' as EvmAddress
+ ), // Ethereum
+ '10': AddressNormalizer.normalizeEvm(
+ '0x0b2C639c533813f4Aa9D7837CAf62653d097Ff85' as EvmAddress
+ ), // Optimism
+ '8453': AddressNormalizer.normalizeEvm(
+ '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913' as EvmAddress
+ ), // Base
+ '137': AddressNormalizer.normalizeEvm(
+ '0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359' as EvmAddress
+ ), // Polygon
+ '999': AddressNormalizer.normalizeEvm(
+ '0xb88339CB7199b77E23DB6E890353E22632Ba630f' as EvmAddress
+ ), // Hyperevm
+ '2020': AddressNormalizer.normalizeEvm(
+ '0x0b7007c13325c48911f73a2dad5fa5dcbf808adc' as EvmAddress
+ ), // Runin
+ '42161': AddressNormalizer.normalizeEvm(
+ '0xaf88d065e77c8cC2239327C5EDb3A432268e5831' as EvmAddress
+ ), // Arbitrum
+ '146': AddressNormalizer.normalizeEvm(
+ '0x29219dd400f2bf60e5a23d13be72b486d4038894' as EvmAddress
+ ), // Sonic
+ '84532': AddressNormalizer.normalizeEvm(
+ '0x036cbd53842c5426634e7929541ec2318f3dcf7e' as EvmAddress
+ ), // Base Sepolia
+ '11155420': AddressNormalizer.normalizeEvm(
+ '0x5fd84259d66Cd46123540766Be93DFE6D43130D7' as EvmAddress
+ ), // Optimism Sepolia
+ '9746': AddressNormalizer.normalizeEvm(
+ '0x107d0b0428741b37331138040F793aF171682603' as EvmAddress
+ ), // Plasma Testnet
+ '11155111': AddressNormalizer.normalizeEvm(
+ '0x1c7D4B196Cb0C7B01d743Fbc6116a902379C7238' as EvmAddress
+ ), // Sepolia
+ '1399811149': AddressNormalizer.normalizeSvm(
+ 'EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v' as SvmAddress
+ ),
+ '1399811150': AddressNormalizer.normalizeSvm(
+ '4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU' as SvmAddress
+ ),
+ // Add more as needed
+ },
+ },
+ USDT: {
+ symbol: 'USDT',
+ name: 'Tether USD',
+ decimals: 6,
+ addresses: {
+ '1': AddressNormalizer.normalizeEvm(
+ '0xdAC17F958D2ee523a2206206994597C13D831ec7' as EvmAddress
+ ), // Ethereum
+ '10': AddressNormalizer.normalizeEvm(
+ '0x94b008aA00579c1307B0EF2c499aD98a8ce58e58' as EvmAddress
+ ), // Optimism
+ '999': AddressNormalizer.normalizeEvm(
+ '0xB8CE59FC3717ada4C02eaDF9682A9e934F625ebb' as EvmAddress
+ ), // Hyperevm
+ '8453': AddressNormalizer.normalizeEvm(
+ '0xfde4C96c8593536E31F229EA8f37b2ADa2699bb2' as EvmAddress
+ ), // Base
+ '728126428': AddressNormalizer.normalizeTvm(
+ 'TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t' as TronAddress
+ ), // Tron
+ '2494104990': AddressNormalizer.normalizeTvm(
+ 'TG3XXyExBkPp9nzdajDZsozEu4BkaSJozs' as TronAddress
+ ), // Tron Shasta
+ '1399811149': AddressNormalizer.normalizeSvm(
+ 'Es9vMFrzaCERmJfrF4H2FYD4KCoNkY11McCe8BenwNYB' as SvmAddress
+ ),
+ // Add more as needed
+ },
+ },
+ bUSDC: {
+ symbol: 'bUSDC',
+ name: 'Binance USDC',
+ decimals: 18,
+ addresses: {
+ '56': AddressNormalizer.normalizeEvm(
+ '0x8ac76a51cc950d9822d68b83fe1ad97b32cd580d' as EvmAddress
+ ), // BNB Smart Chain
+ },
+ },
+ bUSDT: {
+ symbol: 'bUSDT',
+ name: 'Binance USDT',
+ decimals: 18,
+ addresses: {
+ '56': AddressNormalizer.normalizeEvm(
+ '0x55d398326f99059fF775485246999027B3197955' as EvmAddress
+ ), // BNB Smart Chain
+ },
+ },
+ ETH: {
+ symbol: 'ETH',
+ name: 'Ether',
+ decimals: 18,
+ addresses: {
+ // ETH-native chains → zero address (no contract; native currency sentinel)
+ '1': AddressNormalizer.normalizeEvm(
+ '0x0000000000000000000000000000000000000000' as EvmAddress
+ ), // Ethereum
+ '10': AddressNormalizer.normalizeEvm(
+ '0x0000000000000000000000000000000000000000' as EvmAddress
+ ), // Optimism
+ '8453': AddressNormalizer.normalizeEvm(
+ '0x0000000000000000000000000000000000000000' as EvmAddress
+ ), // Base
+ '42161': AddressNormalizer.normalizeEvm(
+ '0x0000000000000000000000000000000000000000' as EvmAddress
+ ), // Arbitrum
+ // ETH testnets → zero address
+ '11155111': AddressNormalizer.normalizeEvm(
+ '0x0000000000000000000000000000000000000000' as EvmAddress
+ ), // Sepolia
+ '84532': AddressNormalizer.normalizeEvm(
+ '0x0000000000000000000000000000000000000000' as EvmAddress
+ ), // Base Sepolia
+ '11155420': AddressNormalizer.normalizeEvm(
+ '0x0000000000000000000000000000000000000000' as EvmAddress
+ ), // Optimism Sepolia
+ },
+ },
+ WETH: {
+ symbol: 'WETH',
+ name: 'Wrap Ether',
+ decimals: 18,
+ addresses: {
+ // ETH-native chains → zero address (no contract; native currency sentinel)
+ '1': AddressNormalizer.normalizeEvm(
+ '0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2' as EvmAddress
+ ), // Ethereum
+ '8453': AddressNormalizer.normalizeEvm(
+ '0x4200000000000000000000000000000000000006' as EvmAddress
+ ), // Base
+ '2020': AddressNormalizer.normalizeEvm(
+ '0xc99a6a985ed2cac1ef41640596c5a5f9f4e19ef5' as EvmAddress
+ ), // Ronin WETH
+ },
+ },
+};
+
+/**
+ * Looks up a token configuration by ticker symbol (case-sensitive).
+ *
+ * @param symbol - Ticker symbol, e.g. `"USDC"`.
+ * @returns The matching {@link TokenConfig}, or `undefined` if not found.
+ *
+ * @example
+ * ```ts
+ * const usdc = getTokenBySymbol('USDC');
+ * // usdc?.decimals === 6
+ * ```
+ */
+export function getTokenBySymbol(symbol: string): TokenConfig | undefined {
+ return TOKEN_CONFIGS[symbol];
+}
+
+/**
+ * Returns the Universal-format address of a token on a specific chain.
+ *
+ * @param symbol - Ticker symbol, e.g. `"USDC"`.
+ * @param chainId - The target chain ID.
+ * @returns The Universal-format token address, or `undefined` if the token
+ * does not have a deployment on the given chain.
+ *
+ * @example
+ * ```ts
+ * const addr = getTokenAddress('USDC', 8453n); // Base mainnet USDC
+ * ```
+ */
+export function getTokenAddress(symbol: string, chainId: bigint): UniversalAddress | undefined {
+ const token = getTokenBySymbol(symbol);
+ if (!token) return undefined;
+
+ // Use chainId as string for lookup
+ return token.addresses[chainId.toString()];
+}
+
+/**
+ * Returns all token configurations registered in {@link TOKEN_CONFIGS}.
+ *
+ * @returns An array of every {@link TokenConfig}.
+ *
+ * @example
+ * ```ts
+ * listTokens().forEach(t => console.log(t.symbol));
+ * ```
+ */
+export function listTokens(): TokenConfig[] {
+ return Object.values(TOKEN_CONFIGS);
+}
+
+/**
+ * Registers a custom token in the global {@link TOKEN_CONFIGS} map.
+ *
+ * The symbol is normalised to uppercase before insertion, so `"usdc"` and
+ * `"USDC"` resolve to the same key.
+ *
+ * @param config - The token configuration to register.
+ *
+ * @example
+ * ```ts
+ * addCustomToken({ symbol: 'MYTOKEN', name: 'My Token', decimals: 18, addresses: {} });
+ * ```
+ */
+export function addCustomToken(config: TokenConfig): void {
+ TOKEN_CONFIGS[config.symbol.toUpperCase()] = config;
+}
+
+// Re-export TOKENS as alias for backward compatibility with plan references
+export const TOKENS = TOKEN_CONFIGS;
diff --git a/src/config/tokens.ts b/src/config/tokens.ts
deleted file mode 100644
index 286aba8..0000000
--- a/src/config/tokens.ts
+++ /dev/null
@@ -1,153 +0,0 @@
-/**
- * Token Configuration
- */
-
-import { ChainType } from '@/core/interfaces/intent';
-import { SvmAddress } from '@/core/types/blockchain-addresses';
-import { UniversalAddress } from '@/core/types/universal-address';
-import { AddressNormalizer } from '@/core/utils/address-normalizer';
-
-export interface TokenConfig {
- symbol: string;
- name: string;
- decimals: number;
- addresses: Record; // chainId (as string) -> address
-}
-
-// Common token configurations
-export const TOKEN_CONFIGS: Record