diff --git a/.env.example b/.env.example index 506b05e..164d0e1 100644 --- a/.env.example +++ b/.env.example @@ -27,33 +27,7 @@ # TRONGRID_API_KEY=your_trongrid_api_key_here # ------------------------------------------------------------------------------ -# Wallet Configuration (choose ONE mode) +# Wallet Configuration # ------------------------------------------------------------------------------ -# --- Option A: Local Mode (Recommended) --- -# Encrypted key storage via agent-wallet SDK. -# Private keys are never exposed in environment variables. -# See README.md for setup instructions. - -# Master password (same as the one used during `agent-wallet init`) -# AGENT_WALLET_PASSWORD=your_master_password - -# Path to agent-wallet secrets directory (default: ~/.agent-wallet) -# AGENT_WALLET_DIR=~/.agent-wallet - -# --- Option B: Static Mode --- -# Direct private key via environment variable. Less secure — key is in plaintext. -# Only used when Agent-Wallet env vars are NOT set. - -# Raw private key (hex, with or without 0x prefix) -# TRON_PRIVATE_KEY=your_private_key_here - -# Or use a BIP39 mnemonic instead of a raw key -# TRON_MNEMONIC=your twelve word mnemonic phrase here - -# HD wallet derivation index (default: 0, used with TRON_MNEMONIC) -# TRON_ACCOUNT_INDEX=0 - -# --- Option C: Read-Only Mode --- -# If neither Local nor Static env vars are set, the server runs in -# read-only mode. Write tools (transfer, staking, etc.) will not be registered. +# Wallets are managed through agent-wallet file-backed configuration. Please see the agent-wallet docs. diff --git a/AGENTS.md b/AGENTS.md index 6221dcd..99e9705 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -77,19 +77,17 @@ This document provides essential information for AI agents working on this repos - **Sensitive Data**: NEVER hardcode private keys or mnemonics. - **Env Vars**: - - `AGENT_WALLET_PASSWORD`: Master password for agent-wallet encrypted keystore (recommended). - - `AGENT_WALLET_DIR`: Wallet directory (optional, default: `~/.agent-wallet`). - - `TRON_PRIVATE_KEY`: Hex key for write operations. - - `TRON_MNEMONIC`: 12/24 word phrase (alternative to key). - `TRONGRID_API_KEY`: Optional but recommended for Mainnet. + - This repository no longer reads or maps legacy `TRON_*` wallet variables. + - Wallet setup should follow `agent-wallet` file-backed configuration and the SDK-supported `AGENT_WALLET_*` settings. ## 🤖 MCP Specifics - **Tools**: Every tool must have a clear `description` and `inputSchema`. -- **Conditional Registration**: Tools are conditionally registered based on whether a wallet is configured. +- **Registration**: Tools are registered up front. `readOnly` only hides write tools at registration time; wallet availability is checked when the handler runs. - "Write" tools (state-changing) are automatically identified by `readOnlyHint: false`. - - Special "Read" tools that depend on wallet configuration (e.g., `get_wallet_address`) must specify `requiresWallet: true` in their annotations. -- **Annotations**: Use `annotations` (`title`, `readOnlyHint`, `requiresWallet`, etc.) to help LLMs understand tool impact and to control registration logic. + - Tools that need a configured wallet should say so in `description` (and return a clear error at runtime if the wallet is missing). +- **Annotations**: Use `annotations` (`title`, `readOnlyHint`, etc.) to help LLMs understand tool impact and runtime expectations. - **Serialization**: Use the `utils.formatJson` helper to handle `BigInt` when returning tool results. --- diff --git a/CHANGELOG.md b/CHANGELOG.md index 6b6c4d2..cadedd0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,14 @@ All notable changes to this project will be documented in this file. +## [1.1.7] - 2026-03-21 + +### Changed + +- Tools and prompts are now registered up front; wallet availability is checked at execution time. +- Removed legacy `TRON_*` wallet mapping from this repository while keeping `agent-wallet`-managed wallet flows. +- Updated docs and tests to match the new wallet semantics and no-wallet runtime behavior. + ## [1.1.6] - 2026-03-18 ### Changed diff --git a/README.md b/README.md index 15e08d5..6fb0e9e 100644 --- a/README.md +++ b/README.md @@ -36,9 +36,9 @@ Key capabilities: - **Smart Contracts**: Interact with any TRON smart contract (Read/Write). - **Tokens**: Transfer TRX and TRC20 tokens; check balances. - **Address Management**: Convert between Hex (0x...) and Base58 (T...) formats. -- **Wallet Integration**: Agent-wallet (encrypted keystore), Private Key, and Mnemonic (BIP-39) wallets. +- **Wallet Integration**: Agent-wallet-managed file-backed wallets. - **Multi-Network**: Seamless support for Mainnet, Nile, and Shasta. -- **Dynamic Access Control**: Automatically hides write tools if no wallet is configured or if `--readonly` mode is active. +- **Dynamic Access Control**: Write-capable tools stay registered; `--readonly` hides them, and wallet-dependent handlers fail at execution time if no wallet is available. ## Features @@ -91,8 +91,7 @@ Key capabilities: ### Wallet & Security -- **Agent Wallet (Recommended)**: Encrypted key storage via agent-wallet SDK — private keys never leave the keystore. -- **Static Wallet**: Configure via `TRON_PRIVATE_KEY` or `TRON_MNEMONIC` environment variables. +- **Agent Wallet**: File-backed wallet storage via `agent-wallet` SDK. - **HD Wallet**: Supports BIP-44 derivation path `m/44'/195'/0'/0/{index}`. - **Signing**: Sign arbitrary messages and transactions. @@ -122,9 +121,7 @@ npm install ### Environment Variables -**CRITICAL SECURITY NOTE**: For your security, **NEVER** save your private keys or mnemonics directly in the MCP configuration JSON files (like `claude_desktop_config.json` or `mcp.json`). Instead, set them as environment variables in your operating system or shell configuration. - -To enable write operations (transfers, contract calls) and ensure reliable API access, you should configure the following variables. +**CRITICAL SECURITY NOTE**: For your security, **NEVER** save your private keys or mnemonics directly in the MCP configuration JSON files (like `claude_desktop_config.json` or `mcp.json`). For wallet setup, follow `agent-wallet`'s file-backed configuration and the SDK-supported `AGENT_WALLET_*` settings; use environment variables only for non-secret operational settings like `TRONGRID_API_KEY`. #### Network Configuration @@ -137,37 +134,11 @@ To enable write operations (transfers, contract calls) and ensure reliable API a #### Wallet Configuration -Choose **one** of the following modes. If none is configured, the server runs in **read-only mode**. - -**Option 1: Agent-Wallet Mode (Recommended)** - -Private keys are encrypted at rest and never exposed in environment variables. +Wallets are managed through `agent-wallet` file-backed configuration. This repository no longer reads or maps legacy `TRON_PRIVATE_KEY` / `TRON_MNEMONIC` / `TRON_ACCOUNT_INDEX` wallet variables. > **Prerequisites**: Install and configure [agent-wallet](https://github.com/BofAI/agent-wallet/blob/main/doc/getting-started.md) -```bash -export AGENT_WALLET_PASSWORD="" -export AGENT_WALLET_DIR="" # Optional, default: ~/.agent-wallet -``` - -> `AGENT_WALLET_PASSWORD` must match the master password used during `agent-wallet`. If not set, agent-wallet mode is disabled and the server falls back to static mode or read-only mode. - -**Option 2: Private Key** - -```bash -export TRON_PRIVATE_KEY="" -``` - -**Option 3: Mnemonic Phrase** - -```bash -export TRON_MNEMONIC=" ... " -export TRON_ACCOUNT_INDEX="0" # Optional, default: 0 -``` - -> **Security Note**: Static modes expose keys in plaintext. **Only keep small amounts of funds** in these wallets — large balances carry a real **risk of theft**. Use Agent-Wallet Mode (Option 1) for any significant funds. - -> See [`.env.example`](.env.example) for a complete list of all supported environment variables. +> See [`agent-wallet`](https://github.com/BofAI/agent-wallet) for wallet file formats, local setup, and the SDK-supported `AGENT_WALLET_*` settings. ### Server Configuration @@ -228,14 +199,14 @@ npx vitest tests/core/services/contracts.test.ts # Contract services npx vitest tests/core/services/account-resource.test.ts # Account resource services npx vitest tests/core/services/staking.test.ts # Staking services -# Integration tests (real Nile RPC; write tests require AGENT_WALLET_PASSWORD or TRON_PRIVATE_KEY) +# Integration tests (real Nile RPC; write-operation coverage is skipped unless wallet support is explicitly enabled) npx vitest tests/core/tools_integration.test.ts # Full tool flow on Nile npx vitest tests/core/services/multicall.test.ts # Multicall integration npx vitest tests/core/services/services.test.ts # Services integration ``` - **Unit tests** use mocks and do not need network or wallet. -- **Integration tests** (`tools_integration.test.ts`) call Nile RPC; most cases are read-only. Tests that broadcast transactions (e.g. `vote_witness`, `withdraw_balance`) run only when a wallet is configured (`AGENT_WALLET_PASSWORD` or `TRON_PRIVATE_KEY`) and are skipped otherwise. +- **Integration tests** (`tools_integration.test.ts`) call Nile RPC; most cases are read-only. Wallet-dependent handlers are exercised as runtime failures by default, while write-success paths require an explicit wallet fixture or equivalent setup. ### Client Configuration @@ -249,12 +220,6 @@ Runs the latest version directly from npm via stdio transport. claude mcp add mcp-server-tron -- npx -y @bankofai/mcp-server-tron ``` -With environment variables: - -```bash -claude mcp add -e AGENT_WALLET_PASSWORD=xxx -e TRONGRID_API_KEY=xxx mcp-server-tron -- npx -y @bankofai/mcp-server-tron -``` - **Cursor** (`.cursor/mcp.json`): ```json @@ -264,8 +229,7 @@ claude mcp add -e AGENT_WALLET_PASSWORD=xxx -e TRONGRID_API_KEY=xxx mcp-server-t "command": "npx", "args": ["-y", "@bankofai/mcp-server-tron"], "env": { - "AGENT_WALLET_PASSWORD": "YOUR_PASSWORD (Or set in system env)", - "TRONGRID_API_KEY": "YOUR_KEY_HERE (Or set in system env)" + "TRONGRID_API_KEY": "YOUR_KEY_HERE" } } } @@ -496,9 +460,8 @@ claude mcp add -transport http mcp-server-tron https://tron-mcp-server.bankofai. ## Security Considerations -- **Private Keys & Mnemonics**: **NEVER** save your sensitive wallet information in plain text configuration files (like `mcp.json`). These files are often unencrypted and can be accidentally shared or committed to git. Use system environment variables which are more secure. -- **Fund Safety (Static Mode)**: If you use `TRON_PRIVATE_KEY` or `TRON_MNEMONIC`, keys are stored in plaintext environment variables. This carries a **real risk of fund theft** — environment variables can be leaked via shell history, process listings, or log files. **Only keep a small amount of funds** in these wallets. For wallets holding any significant value, always use [Agent-Wallet Mode](#option-1-agent-wallet-mode-recommended). -- **Shared Machines**: Be aware that on shared systems, environment variables might be visible to other users via `/proc` or system monitoring tools. +- **Private Keys & Mnemonics**: Keep wallet material inside `agent-wallet` file-backed configuration instead of plain text MCP config files. This repository no longer maps legacy `TRON_*` wallet variables; use `AGENT_WALLET_*` only when following the `agent-wallet` SDK documentation. +- **Shared Machines**: Be aware that plain environment variables can be visible to other users via `/proc` or system monitoring tools. - **Testnets**: Always test on Nile or Shasta before performing operations on Mainnet. - **Approvals**: Be cautious when approving token allowances via `write_contract`. Only approve what is necessary. diff --git a/mcp_example.json b/mcp_example.json index 524172f..236b202 100644 --- a/mcp_example.json +++ b/mcp_example.json @@ -4,8 +4,7 @@ "command": "npx", "args": ["tsx", "src/index.ts"], "env": { - "AGENT_WALLET_PASSWORD": "YOUR_PASSWORD_HERE", - "TRON_PRIVATE_KEY": "YOUR_PRIVATE_KEY_HERE (legacy, optional)" + "TRONGRID_API_KEY": "YOUR_KEY_HERE" } } } diff --git a/package-lock.json b/package-lock.json index 3d8faad..040d6a9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,15 +1,15 @@ { "name": "@bankofai/mcp-server-tron", - "version": "1.1.5", + "version": "1.1.7", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@bankofai/mcp-server-tron", - "version": "1.1.5", + "version": "1.1.7", "license": "MIT", "dependencies": { - "@bankofai/agent-wallet": "^2.2.0", + "@bankofai/agent-wallet": "^2.3.0", "@modelcontextprotocol/sdk": "^1.22.0", "@scure/bip32": "^2.0.1", "@scure/bip39": "^2.0.1", @@ -87,9 +87,9 @@ } }, "node_modules/@bankofai/agent-wallet": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/@bankofai/agent-wallet/-/agent-wallet-2.2.0.tgz", - "integrity": "sha512-dupa5ZgK47KegPcxWrseG/eVfBKyTbqJFcWqU2S5B239H07VnfQ2Dv5sfwnT/k8t06AQKGciTbxwJDLCDEI21g==", + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@bankofai/agent-wallet/-/agent-wallet-2.3.0.tgz", + "integrity": "sha512-Jezo0ffhZrsF0HQMV74wTJlLUtHoY4d7TeYbjqjkWketCqBdhiX9/90hS4qwk99QZwnbt+1HRUschOaSA1wkRw==", "license": "MIT", "dependencies": { "@inquirer/prompts": "^8.3.0", diff --git a/package.json b/package.json index 79a7ccd..5b5a07c 100644 --- a/package.json +++ b/package.json @@ -1,9 +1,9 @@ { "name": "@bankofai/mcp-server-tron", "mcpName": "io.github.bankofai/mcp-server-tron", - "module": "src/index.ts", + "module": "build/index.js", "type": "module", - "version": "1.1.6", + "version": "1.1.7", "description": "MCP server for TRON blockchain. Supports TRX/TRC20 transfers, smart contracts, and AI prompts.", "bin": { "mcp-server-tron": "./bin/cli.js" @@ -53,7 +53,7 @@ "typescript": "^5.8.2" }, "dependencies": { - "@bankofai/agent-wallet": "^2.2.0", + "@bankofai/agent-wallet": "^2.3.0", "@modelcontextprotocol/sdk": "^1.22.0", "@scure/bip32": "^2.0.1", "@scure/bip39": "^2.0.1", diff --git a/server.json b/server.json index c90b48b..5330f26 100644 --- a/server.json +++ b/server.json @@ -6,30 +6,16 @@ "url": "https://github.com/BofAI/mcp-server-tron", "source": "github" }, - "version": "1.1.5", + "version": "1.1.7", "packages": [ { "registryType": "npm", "identifier": "@bankofai/mcp-server-tron", - "version": "1.1.5", + "version": "1.1.7", "transport": { "type": "stdio" }, "environmentVariables": [ - { - "name": "AGENT_WALLET_PASSWORD", - "description": "Master password for agent-wallet encrypted keystore (recommended for write operations)", - "isRequired": false, - "format": "string", - "isSecret": true - }, - { - "name": "TRON_PRIVATE_KEY", - "description": "Private key for the TRON wallet (legacy, optional)", - "isRequired": false, - "format": "string", - "isSecret": true - }, { "name": "TRONGRID_API_KEY", "description": "TronGrid API Key for higher rate limits (optional)", @@ -40,4 +26,4 @@ ] } ] -} \ No newline at end of file +} diff --git a/src/core/prompts.ts b/src/core/prompts.ts index 9029a5c..25f4338 100644 --- a/src/core/prompts.ts +++ b/src/core/prompts.ts @@ -1,6 +1,5 @@ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import { z } from "zod"; -import * as services from "./services/index.js"; /** * Register task-oriented prompts with the MCP server @@ -19,9 +18,9 @@ import * as services from "./services/index.js"; */ export function registerTRONPrompts(server: McpServer, options: { readOnly?: boolean } = {}) { /** - * Helper to register a prompt with automatic wallet requirement detection. - * Prompts that guide write operations should only be registered if a wallet - * is configured and we are not in read-only mode. + * Helper to register a prompt with read-only gating. + * Prompts are registered up front; write guidance is hidden only in readonly + * mode and wallet availability is checked when the underlying tools run. */ const registerPrompt = ( name: string, @@ -30,24 +29,18 @@ export function registerTRONPrompts(server: McpServer, options: { readOnly?: boo argsSchema?: T; }, handler: (args: z.infer>) => any, - extra: { requiresWallet?: boolean; isReadOnly?: boolean } = {}, + extra: { isReadOnly?: boolean } = {}, ) => { // Default to true: most prompts are informational and safe in readonly mode. // This differs from tools.ts where the default is false (write-capable) because // unregistered tools could mutate state, while prompts only guide the LLM. const isReadOnly = extra.isReadOnly !== false; - const walletNeeded = extra.requiresWallet === true; // 1. Skip if in read-only mode and the prompt is for write operations if (options.readOnly && !isReadOnly) { return; } - // 2. Skip if the prompt needs a wallet but none is configured - if (walletNeeded && services.getActiveWalletId() === null) { - return; - } - server.registerPrompt(name, definition as any, handler as any); }; @@ -122,7 +115,7 @@ ${ }, ], }), - { requiresWallet: true, isReadOnly: false }, + { isReadOnly: false }, ); registerPrompt( @@ -252,7 +245,7 @@ After execution: ], }; }, - { requiresWallet: true, isReadOnly: false }, + { isReadOnly: false }, ); registerPrompt( diff --git a/src/core/services/agent-wallet.ts b/src/core/services/agent-wallet.ts index a7876b2..f248bd6 100644 --- a/src/core/services/agent-wallet.ts +++ b/src/core/services/agent-wallet.ts @@ -2,15 +2,12 @@ * Agent-wallet integration layer for mcp-server-tron. * * Provides a unified signing interface via agent-wallet SDK. - * Supports: - * - **Encrypted Storage mode**: Keys encrypted at rest (password-protected). - * - **Static/Env mode**: Keys provided via environment variables. */ import { type WalletProvider, resolveWalletProvider, - type BaseWallet, + type Wallet, type Eip712Capable, } from "@bankofai/agent-wallet"; import { TronWeb } from "tronweb"; @@ -21,36 +18,14 @@ import { getTronWeb } from "./clients.js"; // --------------------------------------------------------------------------- let provider: WalletProvider | null = null; -let activeWallet: BaseWallet | null = null; +let activeWallet: Wallet | null = null; let activeAddress: string | null = null; // --------------------------------------------------------------------------- -// Provider initialization (lazy) -// --------------------------------------------------------------------------- - -/** - * Configure environment variables for backward compatibility. - * Maps TRON_PRIVATE_KEY -> AGENT_WALLET_PRIVATE_KEY etc. - */ -function ensureEnvMapping() { - if (process.env.TRON_PRIVATE_KEY && !process.env.AGENT_WALLET_PRIVATE_KEY) { - process.env.AGENT_WALLET_PRIVATE_KEY = process.env.TRON_PRIVATE_KEY; - } - if (process.env.TRON_MNEMONIC && !process.env.AGENT_WALLET_MNEMONIC) { - process.env.AGENT_WALLET_MNEMONIC = process.env.TRON_MNEMONIC; - } - if (process.env.TRON_MNEMONIC_ACCOUNT_INDEX && !process.env.AGENT_WALLET_MNEMONIC_ACCOUNT_INDEX) { - process.env.AGENT_WALLET_MNEMONIC_ACCOUNT_INDEX = process.env.TRON_MNEMONIC_ACCOUNT_INDEX; - } -} - function getProvider(): WalletProvider | null { if (provider) return provider; - ensureEnvMapping(); - try { - // resolveWalletProvider detects mode from AGENT_WALLET_* env vars provider = resolveWalletProvider({ network: "tron" }); return provider; } catch (_e) { @@ -66,14 +41,12 @@ function getProvider(): WalletProvider | null { /** * Get the currently active agent-wallet. */ -export async function getActiveWallet(): Promise { +export async function getActiveWallet(): Promise { if (activeWallet) return activeWallet; const p = getProvider(); if (!p) { - throw new Error( - "Wallet not configured. Please set AGENT_WALLET_PASSWORD, TRON_PRIVATE_KEY, TRON_MNEMONIC, or TRON_MNEMONIC_ACCOUNT_INDEX.", - ); + throw new Error("Wallet not configured."); } activeWallet = await p.getActiveWallet(); @@ -86,21 +59,20 @@ export async function getActiveWallet(): Promise { */ export async function getOwnerAddress(): Promise { if (activeAddress) return activeAddress; - const wallet = await getActiveWallet(); - activeAddress = await wallet.getAddress(); + await getActiveWallet(); + if (activeAddress == null) { + throw new Error("Failed to resolve active wallet address"); + } return activeAddress; } /** - * Switch the active wallet at runtime (Encrypted Storage mode only). + * Switch the active wallet at runtime when the provider supports multi-wallet selection. */ export async function selectWallet(walletId: string): Promise<{ id: string; address: string }> { const p = getProvider(); if (!p || typeof (p as any).setActive !== "function") { - throw new Error( - "select_wallet is not available. " + - "Ensure AGENT_WALLET_PASSWORD is configured for encrypted storage mode.", - ); + throw new Error("select_wallet is not available."); } const lp = p as any; @@ -126,21 +98,42 @@ export async function listAgentWallets(): Promise< if (typeof (p as any).listWallets === "function") { const lp = p as any; + // agent-wallet@2.3+: listWallets(): Array<[walletId, walletConfig, isActive]> + // agent-wallet@2.2 (and tests) may return Promise>. const wallets = await lp.listWallets(); const result: Array<{ id: string; type: string; address: string }> = []; for (const w of wallets) { - const wallet = await lp.getWallet(w.id); + let walletId: string; + let walletType: string; + + if (Array.isArray(w)) { + walletId = w[0] as string; + walletType = ((w[1] as { type?: string })?.type ?? "unknown") as string; + } else { + walletId = (w as { id?: string }).id ?? ""; + walletType = ((w as { type?: string }).type ?? "unknown") as string; + } + + if (!walletId) { + // Skip malformed entries so one bad import does not hide all wallets. + continue; + } + + const wallet = await lp.getWallet(walletId); const address = await wallet.getAddress(); - result.push({ id: w.id, type: w.type, address }); + result.push({ id: walletId, type: walletType, address }); } return result; } - // Static/Env mode + // Single-wallet mode const wallet = await p.getActiveWallet(); const address = await wallet.getAddress(); - return [{ id: "default", type: "static", address }]; + if (address == null) { + return []; + } + return [{ id: "single", type: "single", address }]; } /** @@ -153,7 +146,7 @@ export function getActiveWalletId(): string | null { if (typeof (p as any).getActiveId === "function") { return (p as any).getActiveId(); } - return "default"; + return null; } // --------------------------------------------------------------------------- diff --git a/src/core/tools/index.ts b/src/core/tools/index.ts index e651d4d..b4c0a71 100644 --- a/src/core/tools/index.ts +++ b/src/core/tools/index.ts @@ -1,6 +1,5 @@ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import { z } from "zod"; -import * as services from "../services/index.js"; import type { RegisterToolFn } from "./types.js"; import { registerWalletTools } from "./wallet.js"; import { registerNetworkTools } from "./network.js"; @@ -26,23 +25,18 @@ import { registerAccountResourceTools } from "./account-resource.js"; /** * Register all TRON-related tools with the MCP server * - * SECURITY: Either TRON_PRIVATE_KEY or TRON_MNEMONIC environment variable must be set for write operations. - * Private keys and mnemonics are never passed as tool arguments for security reasons. - * Tools will use the configured wallet for all transactions. - * - * Configuration options: - * - TRON_PRIVATE_KEY: Hex private key (with or without 0x prefix) - * - TRON_MNEMONIC: BIP-39 mnemonic phrase (12 or 24 words) - * - TRON_ACCOUNT_INDEX: Optional account index for HD wallet derivation (default: 0) + * Write operations are registered up front and validate wallet availability + * when the tool handler runs. Private keys and mnemonics are never passed as + * tool arguments for security reasons. * * @param server The MCP server instance * @param options Registration options (e.g., readOnly mode) */ export function registerTRONTools(server: McpServer, options: { readOnly?: boolean } = {}) { /** - * Helper to register a tool with automatic wallet requirement detection. - * If a tool is not read-only or explicitly requires a wallet, it will only be - * registered if a wallet is configured via environment variables. + * Helper to register a tool with read-only gating. + * Write tools (`readOnlyHint` not true) are omitted when `options.readOnly` is set; + * wallet availability is validated inside handlers when invoked. */ const registerTool: RegisterToolFn = ( name: string, @@ -52,7 +46,6 @@ export function registerTRONTools(server: McpServer, options: { readOnly?: boole annotations?: { title?: string; readOnlyHint?: boolean; - requiresWallet?: boolean; destructiveHint?: boolean; idempotentHint?: boolean; openWorldHint?: boolean; @@ -65,24 +58,12 @@ export function registerTRONTools(server: McpServer, options: { readOnly?: boole // for safety. This is stricter than prompts.ts (which defaults to read-only) because // tools can directly mutate blockchain state. const isReadOnly = annotations.readOnlyHint === true; - const walletNeeded = annotations.requiresWallet === true || !isReadOnly; // 1. Skip if in read-only mode and the tool is a write operation if (options.readOnly && !isReadOnly) { return; } - // 2. Skip if the tool needs a wallet but none is configured - if (walletNeeded && services.getActiveWalletId() === null) { - return; - } - - // Strip custom `requiresWallet` before passing to SDK (not a standard MCP annotation) - if (definition.annotations?.requiresWallet !== undefined) { - const { requiresWallet: _, ...standardAnnotations } = definition.annotations; - definition = { ...definition, annotations: standardAnnotations }; - } - server.registerTool(name, definition as any, handler as any); }; diff --git a/src/core/tools/types.ts b/src/core/tools/types.ts index 808af43..7773d58 100644 --- a/src/core/tools/types.ts +++ b/src/core/tools/types.ts @@ -8,7 +8,6 @@ export type RegisterToolFn = ( annotations?: { title?: string; readOnlyHint?: boolean; - requiresWallet?: boolean; destructiveHint?: boolean; idempotentHint?: boolean; openWorldHint?: boolean; diff --git a/src/core/tools/wallet.ts b/src/core/tools/wallet.ts index 94bb601..bf382bb 100644 --- a/src/core/tools/wallet.ts +++ b/src/core/tools/wallet.ts @@ -12,7 +12,6 @@ export function registerWalletTools(registerTool: RegisterToolFn) { annotations: { title: "Get Wallet Address", readOnlyHint: true, - requiresWallet: true, destructiveHint: false, idempotentHint: true, openWorldHint: false, @@ -63,7 +62,6 @@ export function registerWalletTools(registerTool: RegisterToolFn) { annotations: { title: "List Wallets", readOnlyHint: true, - requiresWallet: true, destructiveHint: false, idempotentHint: true, openWorldHint: false, @@ -82,9 +80,11 @@ export function registerWalletTools(registerTool: RegisterToolFn) { activeWalletId: activeId, wallets, message: - wallets.length === 1 && wallets[0].id === "default" - ? "Using static wallet configured via environment variables." - : `Found ${wallets.length} wallet(s). Use select_wallet to switch the active wallet.`, + wallets.length === 0 + ? "No wallet is currently configured." + : wallets.length === 1 + ? "Using the configured single wallet." + : `Found ${wallets.length} wallet(s). Use select_wallet to switch the active wallet.`, }, null, 2, @@ -117,7 +117,6 @@ export function registerWalletTools(registerTool: RegisterToolFn) { annotations: { title: "Select Wallet", readOnlyHint: false, - requiresWallet: true, destructiveHint: false, idempotentHint: true, openWorldHint: false, diff --git a/tests/core/services/account-resource.test.ts b/tests/core/services/account-resource.test.ts index e5fd33b..7f66c5c 100644 --- a/tests/core/services/account-resource.test.ts +++ b/tests/core/services/account-resource.test.ts @@ -8,23 +8,12 @@ import { } from "../../../src/core/services/account-resource.js"; describe("Account Resource Services Integration (Nile)", () => { - const hasWallet = - !!process.env.TRON_PRIVATE_KEY || - !!process.env.TRON_MNEMONIC || - !!(process.env.AGENT_WALLET_DIR && process.env.AGENT_WALLET_PASSWORD); + const hasWallet = false; it.runIf(hasWallet)( "delegateResource should attempt to delegate and return error or tx hash", async () => { - const receiverAddress = - process.env.TRON_DELEGATEE_ADDRESS || process.env.TRON_ADDRESS || null; - - if (!receiverAddress) { - console.log( - "Skipping delegateResource test: neither TRON_DELEGATEE_ADDRESS nor TRON_ADDRESS configured", - ); - return; - } + const receiverAddress = "T9yD14Nj9j7xAB4dbGeiX9h8unkKHxuWwb"; try { const txId = await delegateResource( @@ -49,15 +38,7 @@ describe("Account Resource Services Integration (Nile)", () => { it.runIf(hasWallet)( "undelegateResource should attempt to undelegate and return error or tx hash", async () => { - const receiverAddress = - process.env.TRON_DELEGATEE_ADDRESS || process.env.TRON_ADDRESS || null; - - if (!receiverAddress) { - console.log( - "Skipping undelegateResource test: neither TRON_DELEGATEE_ADDRESS nor TRON_ADDRESS configured", - ); - return; - } + const receiverAddress = "T9yD14Nj9j7xAB4dbGeiX9h8unkKHxuWwb"; try { const txId = await undelegateResource( @@ -81,12 +62,7 @@ describe("Account Resource Services Integration (Nile)", () => { it.runIf(hasWallet)( "getCanDelegatedMaxSize should return max delegatable amount", async () => { - const address = process.env.TRON_ADDRESS; - if (!address) { - console.log("Skipping getCanDelegatedMaxSize test: TRON_ADDRESS not configured"); - return; - } - + const address = "T9yD14Nj9j7xAB4dbGeiX9h8unkKHxuWwb"; const result = await getCanDelegatedMaxSize(address, "ENERGY", "nile"); expect(result.address).toBe(address); expect(result.resource).toBe("ENERGY"); @@ -101,15 +77,8 @@ describe("Account Resource Services Integration (Nile)", () => { it.runIf(hasWallet)( "getDelegatedResourceV2 should return delegated resource details or empty list", async () => { - const from = process.env.TRON_ADDRESS_FROM || process.env.TRON_ADDRESS; - const to = process.env.TRON_ADDRESS_TO || process.env.TRON_ADDRESS; - - if (!from || !to) { - console.log( - "Skipping getDelegatedResourceV2 test: TRON_ADDRESS or pairing envs (TRON_ADDRESS_FROM / TRON_ADDRESS_TO) not configured", - ); - return; - } + const from = "T9yD14Nj9j7xAB4dbGeiX9h8unkKHxuWwb"; + const to = "T9yD14Nj9j7xAB4dbGeiX9h8unkKHxuWwb"; try { const result = await getDelegatedResourceV2(from, to, "nile"); @@ -133,14 +102,7 @@ describe("Account Resource Services Integration (Nile)", () => { it.runIf(hasWallet)( "getDelegatedResourceAccountIndexV2 should return delegation index", async () => { - const address = process.env.TRON_ADDRESS; - if (!address) { - console.log( - "Skipping getDelegatedResourceAccountIndexV2 test: TRON_ADDRESS not configured", - ); - return; - } - + const address = "T9yD14Nj9j7xAB4dbGeiX9h8unkKHxuWwb"; try { const result = await getDelegatedResourceAccountIndexV2(address, "nile"); expect(result.account).toBeDefined(); diff --git a/tests/core/services/account.test.ts b/tests/core/services/account.test.ts index 55a56b5..4cbb4ff 100644 --- a/tests/core/services/account.test.ts +++ b/tests/core/services/account.test.ts @@ -13,10 +13,7 @@ import { const TEST_ADDRESS = "T9yD14Nj9j7xAB4dbGeiX9h8unkKHxuWwb"; describe("Account Services Integration (Nile)", () => { - const hasWallet = - !!process.env.TRON_PRIVATE_KEY || - !!process.env.TRON_MNEMONIC || - !!(process.env.AGENT_WALLET_DIR && process.env.AGENT_WALLET_PASSWORD); + const hasWallet = false; // ============================================================================ // READ-ONLY TESTS diff --git a/tests/core/services/agent-wallet.test.ts b/tests/core/services/agent-wallet.test.ts index f996eb0..816b421 100644 --- a/tests/core/services/agent-wallet.test.ts +++ b/tests/core/services/agent-wallet.test.ts @@ -1,9 +1,29 @@ -import { describe, it, expect, vi, afterEach } from "vitest"; +import { afterEach, describe, expect, it, vi } from "vitest"; // --------------------------------------------------------------------------- -// Mocks +// Shared mock fns // --------------------------------------------------------------------------- +const mockResolveWalletProvider = vi.fn(); +const mockSignTransaction = vi.fn(); +const mockSignMessage = vi.fn(); +const mockSignTypedData = vi.fn(); +const mockGetActive = vi.fn(); +const mockGetActiveId = vi.fn(); +const mockSetActive = vi.fn(); +const mockListWallets = vi.fn(); +const mockGetWallet = vi.fn(); +const mockTrxSign = vi.fn(); +const mockSendRawTransaction = vi.fn(); + +// --------------------------------------------------------------------------- +// Module mocks +// --------------------------------------------------------------------------- + +vi.mock("@bankofai/agent-wallet", () => ({ + resolveWalletProvider: mockResolveWalletProvider, +})); + vi.mock("tronweb", () => { const MockTronWeb = { createAccount: vi.fn().mockReturnValue({ @@ -15,461 +35,271 @@ vi.mock("tronweb", () => { utils: { crypto: { getAddressFromPrivateKey: vi.fn().mockReturnValue("TNewGeneratedAddress"), - getBufferFromHex: vi.fn((hex) => Buffer.from(hex, "hex")), }, }, }; + return { default: MockTronWeb, TronWeb: MockTronWeb, }; }); -// --------------------------------------------------------------------------- -// Shared mock fns (referenced by the mock factories below) -// --------------------------------------------------------------------------- -const mockSignTransaction = vi.fn(); -const mockSignMessage = vi.fn(); -const mockSignTypedData = vi.fn(); -const mockListWallets = vi.fn(); -const mockGetWallet = vi.fn(); -const mockGenerateKey = vi.fn(); -const mockSavePrivateKey = vi.fn(); -const mockTrxSign = vi.fn(); -const mockSendRawTransaction = vi.fn(); -const mockCryptoSignTx = vi.fn((_pk: string, tx: any) => ({ - ...tx, - signature: ["static-raw-sig"], -})); - -// --------------------------------------------------------------------------- -// Mocks -// --------------------------------------------------------------------------- -const mockGetActiveId = vi.fn(); -const mockGetActive = vi.fn(); -const mockSetActive = vi.fn(); - -vi.mock("@bankofai/agent-wallet", () => ({ - resolveWalletProvider: vi.fn((_options) => { - if (process.env.AGENT_WALLET_PASSWORD) { - return { - listWallets: mockListWallets, - getWallet: mockGetWallet, - getActiveId: mockGetActiveId, - getActiveWallet: mockGetActive, - setActive: mockSetActive, - }; - } - if (process.env.AGENT_WALLET_PRIVATE_KEY || process.env.AGENT_WALLET_MNEMONIC) { - return { - getActiveWallet: mockGetActive, - }; - } - throw new Error( - "resolveWalletProvider requires one of: AGENT_WALLET_PASSWORD, AGENT_WALLET_PRIVATE_KEY, or AGENT_WALLET_MNEMONIC", - ); - }), - SecureKVStore: vi.fn().mockImplementation(function () { - return { - generateKey: mockGenerateKey, - savePrivateKey: mockSavePrivateKey, - }; - }), - TronWallet: vi.fn().mockImplementation(function () { - return { getAddress: vi.fn().mockResolvedValue("TNewGeneratedAddress") }; - }), - loadConfig: vi.fn(() => ({ wallets: {} })), - saveConfig: vi.fn(), -})); - vi.mock("../../../src/core/services/clients.js", () => ({ getTronWeb: vi.fn(() => ({ trx: { sign: mockTrxSign, sendRawTransaction: mockSendRawTransaction, }, - utils: { crypto: { signTransaction: mockCryptoSignTx } }, - })), - getWallet: vi.fn(() => ({ - trx: { sign: mockTrxSign }, })), })); // --------------------------------------------------------------------------- -// ENV helpers +// Helpers // --------------------------------------------------------------------------- -const ORIGINAL_ENV = { ...process.env }; - -function setAgentWalletEnv() { - process.env.AGENT_WALLET_DIR = "/tmp/test-wallet"; - process.env.AGENT_WALLET_PASSWORD = "test-pass"; - delete process.env.TRON_PRIVATE_KEY; - delete process.env.TRON_MNEMONIC; -} -function setStaticEnv() { - delete process.env.AGENT_WALLET_DIR; - delete process.env.AGENT_WALLET_PASSWORD; - process.env.TRON_PRIVATE_KEY = "0000000000000000000000000000000000000000000000000000000000000001"; -} +type AW = typeof import("../../../src/core/services/agent-wallet.js"); -function clearAllWalletEnv() { - delete process.env.AGENT_WALLET_DIR; - delete process.env.AGENT_WALLET_PASSWORD; - delete process.env.TRON_PRIVATE_KEY; - delete process.env.TRON_MNEMONIC; +async function freshImport(): Promise { + vi.resetModules(); + return (await import("../../../src/core/services/agent-wallet.js")) as AW; } -/** Create a full mock wallet with all methods. */ -function createMockWallet(address: string) { +function createMockWallet(address: string, overrides: Record = {}) { return { getAddress: vi.fn().mockResolvedValue(address), signTransaction: mockSignTransaction, signMessage: mockSignMessage, signTypedData: mockSignTypedData, + ...overrides, }; } -// --------------------------------------------------------------------------- -// Dynamic import helper — each call gets a fresh module with clean singletons -// --------------------------------------------------------------------------- -type AW = typeof import("../../../src/core/services/agent-wallet.js"); - -async function freshImport(): Promise { - // Reset module registry so the import gives a fresh singleton set - vi.resetModules(); - return (await import("../../../src/core/services/agent-wallet.js")) as AW; +function createProvider(overrides: Record = {}) { + return { + getActiveWallet: mockGetActive, + getActiveId: mockGetActiveId, + setActive: mockSetActive, + listWallets: mockListWallets, + getWallet: mockGetWallet, + ...overrides, + }; } -// =========================================================================== +// --------------------------------------------------------------------------- // Tests -// =========================================================================== +// --------------------------------------------------------------------------- describe("agent-wallet service", () => { afterEach(() => { - process.env = { ...ORIGINAL_ENV }; vi.clearAllMocks(); + vi.unstubAllEnvs(); + vi.resetModules(); }); - // ========================================================================= - // Mode detection — pure functions, no singleton state - // ========================================================================= - describe("getActiveWalletId", () => { - it("returns 'default' in static mode (no getActiveId function)", async () => { - setStaticEnv(); - const { getActiveWalletId } = await freshImport(); - expect(getActiveWalletId()).toBe("default"); - }); - - it("returns null when no wallet env is configured (SDK throws)", async () => { - clearAllWalletEnv(); + it("returns null when resolveWalletProvider throws", async () => { + mockResolveWalletProvider.mockImplementation(() => { + throw new Error("no wallet"); + }); const { getActiveWalletId } = await freshImport(); expect(getActiveWalletId()).toBe(null); + expect(mockResolveWalletProvider).toHaveBeenCalledWith({ network: "tron" }); }); - it("returns active wallet ID from provider (e.g. Encrypted Storage)", async () => { - setAgentWalletEnv(); + it("returns active wallet ID from provider", async () => { + mockResolveWalletProvider.mockReturnValue(createProvider({ getActiveId: mockGetActiveId })); mockGetActiveId.mockReturnValue("wallet-1"); const { getActiveWalletId } = await freshImport(); expect(getActiveWalletId()).toBe("wallet-1"); }); - it("returns null when provider has no active wallet (returns null from getActiveId)", async () => { - setAgentWalletEnv(); - mockGetActiveId.mockReturnValue(null); + it("returns null when provider has no getActiveId", async () => { + mockResolveWalletProvider.mockReturnValue(createProvider({ getActiveId: undefined })); const { getActiveWalletId } = await freshImport(); expect(getActiveWalletId()).toBe(null); }); }); - // ========================================================================= - // getOwnerAddress - // ========================================================================= - - describe("getOwnerAddress", () => { - it("derives address from TRON_PRIVATE_KEY in static mode", async () => { - setStaticEnv(); - mockGetActive.mockResolvedValue(createMockWallet("TStaticAddr123")); - const { getOwnerAddress } = await freshImport(); - const address = await getOwnerAddress(); - expect(address).toBe("TStaticAddr123"); + describe("getActiveWallet", () => { + it("throws when no provider is available", async () => { + mockResolveWalletProvider.mockImplementation(() => { + throw new Error("no wallet"); + }); + const { getActiveWallet } = await freshImport(); + await expect(getActiveWallet()).rejects.toThrow("Wallet not configured."); }); - it("gets address from agent-wallet in agent-wallet mode", async () => { - setAgentWalletEnv(); - mockGetActive.mockResolvedValue(createMockWallet("TAgentWalletAddr123")); - const { getOwnerAddress } = await freshImport(); + it("returns the active wallet from provider", async () => { + const wallet = createMockWallet("TActiveAddr"); + mockResolveWalletProvider.mockReturnValue(createProvider({ getActiveWallet: mockGetActive })); + mockGetActive.mockResolvedValue(wallet); + const { getActiveWallet } = await freshImport(); - const address = await getOwnerAddress(); - expect(address).toBe("TAgentWalletAddr123"); + const result = await getActiveWallet(); + expect(result).toBe(wallet); + expect(mockGetActive).toHaveBeenCalledTimes(1); }); }); - // ========================================================================= - // selectWallet - // ========================================================================= + describe("getOwnerAddress", () => { + it("returns the active wallet address", async () => { + const wallet = createMockWallet("TOwnerAddr"); + mockResolveWalletProvider.mockReturnValue(createProvider({ getActiveWallet: mockGetActive })); + mockGetActive.mockResolvedValue(wallet); + + const { getOwnerAddress } = await freshImport(); + await expect(getOwnerAddress()).resolves.toBe("TOwnerAddr"); + }); + }); describe("selectWallet", () => { - it("throws in static mode", async () => { - setStaticEnv(); + it("throws when provider does not support setActive", async () => { + mockResolveWalletProvider.mockReturnValue(createProvider({ setActive: undefined })); const { selectWallet } = await freshImport(); - await expect(selectWallet("some-id")).rejects.toThrow( - "select_wallet is not available. Ensure AGENT_WALLET_PASSWORD is configured", - ); + await expect(selectWallet("wallet-1")).rejects.toThrow("select_wallet is not available."); }); - it("switches wallet and persists via provider.setActive", async () => { - setAgentWalletEnv(); - mockGetWallet.mockResolvedValue(createMockWallet("TSwitchedAddr")); - const { selectWallet } = await freshImport(); + it("switches wallet and returns its address", async () => { + const wallet = createMockWallet("TSwitchedAddr"); + mockResolveWalletProvider.mockReturnValue(createProvider({ getWallet: mockGetWallet })); + mockGetWallet.mockResolvedValue(wallet); - const result = await selectWallet("wallet-2"); - expect(result.id).toBe("wallet-2"); - expect(result.address).toBe("TSwitchedAddr"); + const { selectWallet } = await freshImport(); + await expect(selectWallet("wallet-2")).resolves.toEqual({ + id: "wallet-2", + address: "TSwitchedAddr", + }); expect(mockSetActive).toHaveBeenCalledWith("wallet-2"); }); }); - // ========================================================================= - // listAgentWallets - // ========================================================================= - describe("listAgentWallets", () => { - it("returns single default wallet in static mode", async () => { - setStaticEnv(); - mockGetActive.mockResolvedValue(createMockWallet("TStaticAddr")); + it("returns a single wallet when provider has no listWallets", async () => { + const wallet = createMockWallet("TSingleAddr"); + mockResolveWalletProvider.mockReturnValue( + createProvider({ getActiveWallet: mockGetActive, listWallets: undefined }), + ); + mockGetActive.mockResolvedValue(wallet); + const { listAgentWallets } = await freshImport(); - const wallets = await listAgentWallets(); - expect(wallets).toHaveLength(1); - expect(wallets[0].id).toBe("default"); - expect(wallets[0].type).toBe("static"); - expect(wallets[0].address).toBe("TStaticAddr"); + await expect(listAgentWallets()).resolves.toEqual([ + { id: "single", type: "single", address: "TSingleAddr" }, + ]); }); - it("returns all wallets from provider in agent-wallet mode", async () => { - setAgentWalletEnv(); + it("returns all wallets from provider when listWallets exists", async () => { + const wallet1 = createMockWallet("TAddr1"); + const wallet2 = createMockWallet("TAddr2"); + mockResolveWalletProvider.mockReturnValue(createProvider({ listWallets: mockListWallets })); mockListWallets.mockResolvedValue([ { id: "w1", type: "tron_local", chain_id: "tron:mainnet" }, { id: "w2", type: "tron_local", chain_id: "tron:nile" }, ]); - mockGetWallet - .mockResolvedValueOnce(createMockWallet("TAddr1")) - .mockResolvedValueOnce(createMockWallet("TAddr2")); + mockGetWallet.mockResolvedValueOnce(wallet1).mockResolvedValueOnce(wallet2); const { listAgentWallets } = await freshImport(); - const wallets = await listAgentWallets(); - expect(wallets).toHaveLength(2); - expect(wallets[0]).toEqual({ - id: "w1", - type: "tron_local", - address: "TAddr1", - }); - expect(wallets[1]).toEqual({ - id: "w2", - type: "tron_local", - address: "TAddr2", - }); + await expect(listAgentWallets()).resolves.toEqual([ + { id: "w1", type: "tron_local", address: "TAddr1" }, + { id: "w2", type: "tron_local", address: "TAddr2" }, + ]); }); }); - // ========================================================================= - // signTransaction - // ========================================================================= - - describe("signTransaction", () => { + describe("signing", () => { const unsignedTx = { txID: "abc123", raw_data: {}, raw_data_hex: "0a0208" }; - it("signs via agent-wallet SDK regardless of mode (unified)", async () => { - setStaticEnv(); - const signedTx = { ...unsignedTx, signature: ["aw-sig"] }; - mockSignTransaction.mockResolvedValue(JSON.stringify(signedTx)); - mockGetActive.mockResolvedValue(createMockWallet("TAddr")); - - const { signTransaction } = await freshImport(); - const result = await signTransaction(unsignedTx); - expect(result).toEqual(signedTx); - expect(mockSignTransaction).toHaveBeenCalledWith(unsignedTx); - }); - - it("signs via agent-wallet SDK in agent-wallet mode", async () => { - setAgentWalletEnv(); - const signedTx = { ...unsignedTx, signature: ["aw-sig"] }; - mockSignTransaction.mockResolvedValue(JSON.stringify(signedTx)); - mockGetActive.mockResolvedValue(createMockWallet("TAddr")); + it("signTransaction uses the active wallet", async () => { + const wallet = createMockWallet("TAddr"); + mockResolveWalletProvider.mockReturnValue(createProvider({ getActiveWallet: mockGetActive })); + mockGetActive.mockResolvedValue(wallet); + mockSignTransaction.mockResolvedValue(JSON.stringify({ ...unsignedTx, signature: ["sig"] })); const { signTransaction } = await freshImport(); - const result = await signTransaction(unsignedTx); - expect(result).toEqual(signedTx); - expect(mockSignTransaction).toHaveBeenCalledWith(unsignedTx); - }); - }); - - // ========================================================================= - // signTransactionRaw - // ========================================================================= - - describe("signTransactionRaw", () => { - const unsignedTx = { txID: "raw123", raw_data: {}, raw_data_hex: "0b0309" }; - - it("signs via agent-wallet SDK regardless of mode (unified)", async () => { - setStaticEnv(); - const signedTx = { ...unsignedTx, signature: ["aw-raw-sig"] }; - mockSignTransaction.mockResolvedValue(JSON.stringify(signedTx)); - mockGetActive.mockResolvedValue(createMockWallet("TAddr")); - - const { signTransactionRaw } = await freshImport(); - const result = await signTransactionRaw(unsignedTx, "nile"); - expect(result).toEqual(signedTx); + await expect(signTransaction(unsignedTx)).resolves.toEqual({ + ...unsignedTx, + signature: ["sig"], + }); }); - it("signs via agent-wallet in agent-wallet mode", async () => { - setAgentWalletEnv(); - const signedTx = { ...unsignedTx, signature: ["aw-raw-sig"] }; - mockSignTransaction.mockResolvedValue(JSON.stringify(signedTx)); - mockGetActive.mockResolvedValue(createMockWallet("TAddr")); + it("signTransactionRaw uses the active wallet", async () => { + const wallet = createMockWallet("TAddr"); + mockResolveWalletProvider.mockReturnValue(createProvider({ getActiveWallet: mockGetActive })); + mockGetActive.mockResolvedValue(wallet); + mockSignTransaction.mockResolvedValue(JSON.stringify({ ...unsignedTx, signature: ["sig"] })); const { signTransactionRaw } = await freshImport(); - const result = await signTransactionRaw(unsignedTx, "nile"); - expect(result).toEqual(signedTx); + await expect(signTransactionRaw(unsignedTx, "nile")).resolves.toEqual({ + ...unsignedTx, + signature: ["sig"], + }); }); - }); - - // ========================================================================= - // buildSignBroadcast - // ========================================================================= - describe("buildSignBroadcast", () => { - const unsignedTx = { txID: "bsb123", raw_data: {}, raw_data_hex: "0c0409" }; - - it("signs and broadcasts, returning txid on success", async () => { - setStaticEnv(); - mockTrxSign.mockResolvedValue({ ...unsignedTx, signature: ["sig"] }); + it("buildSignBroadcast signs and broadcasts", async () => { + const wallet = createMockWallet("TAddr"); + mockResolveWalletProvider.mockReturnValue(createProvider({ getActiveWallet: mockGetActive })); + mockGetActive.mockResolvedValue(wallet); + mockSignTransaction.mockResolvedValue(JSON.stringify({ ...unsignedTx, signature: ["sig"] })); mockSendRawTransaction.mockResolvedValue({ result: true, txid: "bcast-tx-id" }); const { buildSignBroadcast } = await freshImport(); - const txid = await buildSignBroadcast(unsignedTx, "nile"); - expect(txid).toBe("bcast-tx-id"); + await expect(buildSignBroadcast(unsignedTx, "nile")).resolves.toBe("bcast-tx-id"); }); - it("throws when broadcast fails", async () => { - setStaticEnv(); - mockTrxSign.mockResolvedValue({ ...unsignedTx, signature: ["sig"] }); - mockSendRawTransaction.mockResolvedValue({ - result: false, - code: "BANDWITH_ERROR", - message: "not enough bandwidth", - }); + it("buildSignBroadcast throws when broadcast fails", async () => { + const wallet = createMockWallet("TAddr"); + mockResolveWalletProvider.mockReturnValue(createProvider({ getActiveWallet: mockGetActive })); + mockGetActive.mockResolvedValue(wallet); + mockSignTransaction.mockResolvedValue(JSON.stringify({ ...unsignedTx, signature: ["sig"] })); + mockSendRawTransaction.mockResolvedValue({ result: false, message: "broadcast failed" }); const { buildSignBroadcast } = await freshImport(); await expect(buildSignBroadcast(unsignedTx, "nile")).rejects.toThrow("Broadcast failed"); }); - }); - - // ========================================================================= - // signMessageWithWallet - // ========================================================================= - - describe("signMessageWithWallet", () => { - it("signs message in static mode via TronWeb (may throw due to no network)", async () => { - setStaticEnv(); - const { signMessageWithWallet } = await freshImport(); - try { - await signMessageWithWallet("hello"); - } catch (error: any) { - // Expected — no real TronWeb, but should NOT throw "agent-wallet mode" error - expect(error.message).not.toContain("agent-wallet mode"); - } - }); - it("signs message via agent-wallet SDK in agent-wallet mode", async () => { - setAgentWalletEnv(); + it("signMessageWithWallet uses the active wallet", async () => { + const wallet = createMockWallet("TAddr"); + mockResolveWalletProvider.mockReturnValue(createProvider({ getActiveWallet: mockGetActive })); + mockGetActive.mockResolvedValue(wallet); mockSignMessage.mockResolvedValue("0xsig"); - mockGetActive.mockResolvedValue(createMockWallet("TAddr")); const { signMessageWithWallet } = await freshImport(); - const sig = await signMessageWithWallet("hello"); - expect(sig).toBe("0xsig"); + await expect(signMessageWithWallet("hello")).resolves.toBe("0xsig"); expect(mockSignMessage).toHaveBeenCalledWith(Buffer.from("hello", "utf-8")); }); - }); - // ========================================================================= - // signTypedDataWithWallet - // ========================================================================= - - describe("signTypedDataWithWallet", () => { - const domain = { name: "Test" }; - const types = { Test: [{ name: "value", type: "uint256" }] }; - const value = { value: 1 }; - - it("throws in static mode when TronWeb lacks _signTypedData", async () => { - setStaticEnv(); - const { signTypedDataWithWallet } = await freshImport(); - try { - await signTypedDataWithWallet(domain, types, value); - } catch (error: any) { - expect(error.message).toBeDefined(); - } - }); - - it("signs via agent-wallet in agent-wallet mode", async () => { - setAgentWalletEnv(); + it("signTypedDataWithWallet uses the active wallet", async () => { + const wallet = createMockWallet("TAddr"); + mockResolveWalletProvider.mockReturnValue(createProvider({ getActiveWallet: mockGetActive })); + mockGetActive.mockResolvedValue(wallet); mockSignTypedData.mockResolvedValue("0xtyped-sig"); - mockGetActive.mockResolvedValue(createMockWallet("TAddr")); const { signTypedDataWithWallet } = await freshImport(); - const sig = await signTypedDataWithWallet(domain, types, value); - expect(sig).toBe("0xtyped-sig"); + await expect( + signTypedDataWithWallet({ name: "Test" }, { Test: [] }, { value: 1 }), + ).resolves.toBe("0xtyped-sig"); }); - it("throws when wallet doesn't support signTypedData", async () => { - setAgentWalletEnv(); - const walletWithoutEip712 = { - getAddress: vi.fn().mockResolvedValue("TAddr"), - signTransaction: mockSignTransaction, - signMessage: mockSignMessage, - // No signTypedData - }; + it("throws when wallet does not support signTypedData", async () => { + const walletWithoutEip712 = createMockWallet("TAddr", { + signTypedData: undefined, + }); + mockResolveWalletProvider.mockReturnValue(createProvider({ getActiveWallet: mockGetActive })); mockGetActive.mockResolvedValue(walletWithoutEip712); const { signTypedDataWithWallet } = await freshImport(); - await expect(signTypedDataWithWallet(domain, types, value)).rejects.toThrow( - "does not support signTypedData", - ); + await expect( + signTypedDataWithWallet({ name: "Test" }, { Test: [] }, { value: 1 }), + ).rejects.toThrow("does not support signTypedData"); }); }); - // ========================================================================= - // generateAccount - // ========================================================================= - - describe("generateAccount", () => { - it("returns ephemeral account regardless of AGENT_WALLET_PASSWORD", async () => { - setStaticEnv(); + describe("generateAccountKeypair", () => { + it("returns an ephemeral account", async () => { const { generateAccountKeypair } = await freshImport(); const result = await generateAccountKeypair(); expect(result.address).toBe("TNewGeneratedAddress"); expect(result.privateKey).toBeDefined(); }); - - it("generates a keypair without storage even if password is set", async () => { - setAgentWalletEnv(); - - const { generateAccountKeypair } = await freshImport(); - const result = await generateAccountKeypair(); - expect(result.address).toBe("TNewGeneratedAddress"); - expect(result.privateKey).toBeDefined(); - expect(mockSavePrivateKey).not.toHaveBeenCalled(); - expect(mockSetActive).not.toHaveBeenCalled(); - }); - - it("does not trigger config refresh", async () => { - setAgentWalletEnv(); - - const { generateAccountKeypair } = await freshImport(); - await generateAccountKeypair(); - expect(mockSetActive).not.toHaveBeenCalled(); - }); }); }); diff --git a/tests/core/services/deploycontract.test.ts b/tests/core/services/deploycontract.test.ts index 97e057f..3f0df8b 100644 --- a/tests/core/services/deploycontract.test.ts +++ b/tests/core/services/deploycontract.test.ts @@ -18,11 +18,7 @@ const SIMPLE_STORAGE_BYTECODE = "6080604052348015600f57600080fd5b5060ac80601d6000396000f3fe6080604052348015600f57600080fd5b506004361060325760003560e01c806360fe47b11460375780636d4ce63c146049575b600080fd5b60476042366004605e565b600055565b005b60005460405190815260200160405180910390f35b600060208284031215606f57600080fd5b503591905056fea2646970667358221220ad46ce342d88ac7c6680183acf9cb99fab4db939a9c45b30036ea6f4da69bf2264736f6c63430008190033"; describe("Contract Services Integration (Nile)", () => { - // Only run if wallet is configured (agent-wallet or static env vars) - const hasWallet = - !!process.env.TRON_PRIVATE_KEY || - !!process.env.TRON_MNEMONIC || - !!(process.env.AGENT_WALLET_DIR && process.env.AGENT_WALLET_PASSWORD); + const hasWallet = false; it.runIf(hasWallet)( "should deploy a simple storage contract", diff --git a/tests/core/services/governance.test.ts b/tests/core/services/governance.test.ts index 8f2c76c..c5094d1 100644 --- a/tests/core/services/governance.test.ts +++ b/tests/core/services/governance.test.ts @@ -15,10 +15,7 @@ import { const TEST_ADDRESS = "T9yD14Nj9j7xAB4dbGeiX9h8unkKHxuWwb"; describe("Governance Services Integration (Nile)", () => { - const hasWallet = - !!process.env.TRON_PRIVATE_KEY || - !!process.env.TRON_MNEMONIC || - !!(process.env.AGENT_WALLET_DIR && process.env.AGENT_WALLET_PASSWORD); + const hasWallet = false; // ============================================================================ // READ-ONLY TESTS diff --git a/tests/core/services/proposals.test.ts b/tests/core/services/proposals.test.ts index bb00317..00bfd4e 100644 --- a/tests/core/services/proposals.test.ts +++ b/tests/core/services/proposals.test.ts @@ -8,10 +8,7 @@ import { } from "../../../src/core/services/proposals.js"; describe("Proposals Services Integration (Nile)", () => { - const hasWallet = - !!process.env.TRON_PRIVATE_KEY || - !!process.env.TRON_MNEMONIC || - !!(process.env.AGENT_WALLET_DIR && process.env.AGENT_WALLET_PASSWORD); + const hasWallet = false; // ============================================================================ // READ-ONLY TESTS diff --git a/tests/core/services/staking.test.ts b/tests/core/services/staking.test.ts index 8bd6772..1056a2c 100644 --- a/tests/core/services/staking.test.ts +++ b/tests/core/services/staking.test.ts @@ -9,11 +9,7 @@ import { } from "../../../src/core/services/staking.js"; describe("Staking Services Integration (Nile)", () => { - // Only run if wallet is configured (agent-wallet or static env vars) - const hasWallet = - !!process.env.TRON_PRIVATE_KEY || - !!process.env.TRON_MNEMONIC || - !!(process.env.AGENT_WALLET_DIR && process.env.AGENT_WALLET_PASSWORD); + const hasWallet = false; it.runIf(hasWallet)( "freezeBalanceV2 should attempt to freeze and return error or tx hash", @@ -66,12 +62,7 @@ describe("Staking Services Integration (Nile)", () => { it.runIf(hasWallet)( "getAvailableUnfreezeCount should return a number", async () => { - const address = process.env.TRON_ADDRESS; - if (!address) { - console.log("Skipping getAvailableUnfreezeCount test: TRON_ADDRESS not configured"); - return; - } - + const address = "T9yD14Nj9j7xAB4dbGeiX9h8unkKHxuWwb"; const result = await getAvailableUnfreezeCount(address, "nile"); expect(typeof result).toBe("number"); console.log(`Available unfreeze count: ${result}`); @@ -82,12 +73,7 @@ describe("Staking Services Integration (Nile)", () => { it.runIf(hasWallet)( "getCanWithdrawUnfreezeAmount should return amount information", async () => { - const address = process.env.TRON_ADDRESS; - if (!address) { - console.log("Skipping getCanWithdrawUnfreezeAmount test: TRON_ADDRESS not configured"); - return; - } - + const address = "T9yD14Nj9j7xAB4dbGeiX9h8unkKHxuWwb"; const result = await getCanWithdrawUnfreezeAmount(address, "nile"); expect(typeof result.amountSun).toBe("bigint"); expect(typeof result.timestampMs).toBe("number"); diff --git a/tests/core/services/transfer.test.ts b/tests/core/services/transfer.test.ts index 7e27354..83c051f 100644 --- a/tests/core/services/transfer.test.ts +++ b/tests/core/services/transfer.test.ts @@ -2,22 +2,12 @@ import { describe, it, expect } from "vitest"; import { transferTRX, transferTRC20, approveTRC20 } from "../../../src/core/services/transfer.js"; describe("Transfer Services Integration (Nile)", () => { - const hasWallet = - !!process.env.TRON_PRIVATE_KEY || - !!process.env.TRON_MNEMONIC || - !!(process.env.AGENT_WALLET_DIR && process.env.AGENT_WALLET_PASSWORD); + const hasWallet = false; it.runIf(hasWallet)( "transferTRX should attempt to send TRX and return tx hash or meaningful error", async () => { - const receiverAddress = process.env.TRON_RECEIVER_ADDRESS || process.env.TRON_ADDRESS || null; - - if (!receiverAddress) { - console.log( - "Skipping transferTRX test: neither TRON_RECEIVER_ADDRESS nor TRON_ADDRESS configured", - ); - return; - } + const receiverAddress = "T9yD14Nj9j7xAB4dbGeiX9h8unkKHxuWwb"; try { const txHash = await transferTRX(receiverAddress, "0.000001", "nile"); @@ -35,14 +25,9 @@ describe("Transfer Services Integration (Nile)", () => { it.runIf(hasWallet)( "transferTRC20 should attempt to send TRC20 and return result or meaningful error", async () => { - const receiverAddress = process.env.TRON_RECEIVER_ADDRESS || process.env.TRON_ADDRESS || null; + const receiverAddress = "T9yD14Nj9j7xAB4dbGeiX9h8unkKHxuWwb"; // USDT on Nile testnet - const tokenAddress = process.env.TRC20_TOKEN_ADDRESS || "TXLAQ63Xg1NAzckPwKHvzw7CSEmLMEqcdj"; - - if (!receiverAddress) { - console.log("Skipping transferTRC20 test: no receiver address configured"); - return; - } + const tokenAddress = "TXLAQ63Xg1NAzckPwKHvzw7CSEmLMEqcdj"; try { const result = await transferTRC20(tokenAddress, receiverAddress, "1", "nile"); @@ -59,13 +44,8 @@ describe("Transfer Services Integration (Nile)", () => { it.runIf(hasWallet)( "approveTRC20 should attempt to approve spending and return tx hash or error", async () => { - const spenderAddress = process.env.TRON_SPENDER_ADDRESS || process.env.TRON_ADDRESS || null; - const tokenAddress = process.env.TRC20_TOKEN_ADDRESS || "TXLAQ63Xg1NAzckPwKHvzw7CSEmLMEqcdj"; - - if (!spenderAddress) { - console.log("Skipping approveTRC20 test: no spender address configured"); - return; - } + const spenderAddress = "T9yD14Nj9j7xAB4dbGeiX9h8unkKHxuWwb"; + const tokenAddress = "TXLAQ63Xg1NAzckPwKHvzw7CSEmLMEqcdj"; try { const txHash = await approveTRC20(tokenAddress, spenderAddress, "1000000", "nile"); diff --git a/tests/core/tools.test.ts b/tests/core/tools.test.ts index e3df47a..3f7af82 100644 --- a/tests/core/tools.test.ts +++ b/tests/core/tools.test.ts @@ -111,6 +111,7 @@ describe("TRON Tools Unit Tests", () => { let registeredTools: Map; beforeEach(() => { + vi.resetAllMocks(); server = new McpServer({ name: "test-server", version: "1.0.0", @@ -124,14 +125,14 @@ describe("TRON Tools Unit Tests", () => { return originalRegisterTool(name, schema, handler); }; - (services.getActiveWalletId as any).mockReturnValue("default"); + (services.getActiveWalletId as any).mockReturnValue(null); + (services.getOwnerAddress as any).mockResolvedValue("TDefaultSender"); registerTRONTools(server); - vi.clearAllMocks(); }); describe("Registration", () => { it("should register at least all expected TRON tools", () => { - // already registered in beforeEach with isWalletConfigured=true + // already registered in beforeEach regardless of wallet availability const expectedTools = [ "get_wallet_address", "list_wallets", @@ -254,7 +255,7 @@ describe("TRON Tools Unit Tests", () => { return originalRegisterTool(name, schema, handler); }; - (services.getActiveWalletId as any).mockReturnValue("default"); + (services.getActiveWalletId as any).mockReturnValue(null); registerTRONTools(localServer, { readOnly: true }); // Write tools should NOT be registered @@ -283,10 +284,10 @@ describe("TRON Tools Unit Tests", () => { expect(registeredTools.has("approve_proposal")).toBe(false); expect(registeredTools.has("delete_proposal")).toBe(false); - // get_wallet_address IS a read tool (readOnlyHint: true) - // Since getActiveWalletId() is mocked to "default", it SHOULD be registered - // even in readonly mode because it doesn't perform write operations. + // get_wallet_address IS a read tool (readOnlyHint: true), so it should + // still be registered even in readonly mode. expect(registeredTools.has("get_wallet_address")).toBe(true); + expect(registeredTools.has("list_wallets")).toBe(true); // Read tools should STILL be registered expect(registeredTools.has("get_balance")).toBe(true); @@ -302,7 +303,7 @@ describe("TRON Tools Unit Tests", () => { expect(registeredTools.has("get_proposal")).toBe(true); }); - it("should NOT register wallet-dependent or write tools when no wallet is configured", () => { + it("should still register wallet-dependent and write tools when no wallet is configured", () => { registeredTools = new Map(); const localServer = new McpServer({ name: "test", version: "1" }); const originalRegisterTool = localServer.registerTool.bind(localServer); @@ -314,39 +315,39 @@ describe("TRON Tools Unit Tests", () => { (services.getActiveWalletId as any).mockReturnValue(null); registerTRONTools(localServer); - // Write tools should NOT be registered (no wallet) - expect(registeredTools.has("transfer_trx")).toBe(false); - expect(registeredTools.has("transfer_trc20")).toBe(false); - expect(registeredTools.has("write_contract")).toBe(false); - expect(registeredTools.has("deploy_contract")).toBe(false); - expect(registeredTools.has("sign_message")).toBe(false); - expect(registeredTools.has("freeze_balance_v2")).toBe(false); - expect(registeredTools.has("unfreeze_balance_v2")).toBe(false); - expect(registeredTools.has("withdraw_expire_unfreeze")).toBe(false); - expect(registeredTools.has("cancel_all_unfreeze_v2")).toBe(false); - expect(registeredTools.has("delegate_resource")).toBe(false); - expect(registeredTools.has("undelegate_resource")).toBe(false); - expect(registeredTools.has("create_account")).toBe(false); - expect(registeredTools.has("update_account")).toBe(false); - expect(registeredTools.has("account_permission_update")).toBe(false); - expect(registeredTools.has("broadcast_transaction")).toBe(false); - expect(registeredTools.has("broadcast_hex")).toBe(false); - expect(registeredTools.has("create_transaction")).toBe(false); - - // Governance/proposal write tools should NOT be registered (no wallet) - expect(registeredTools.has("create_witness")).toBe(false); - expect(registeredTools.has("update_witness")).toBe(false); - expect(registeredTools.has("vote_witness")).toBe(false); - expect(registeredTools.has("withdraw_balance")).toBe(false); - expect(registeredTools.has("update_brokerage")).toBe(false); - expect(registeredTools.has("create_proposal")).toBe(false); - expect(registeredTools.has("approve_proposal")).toBe(false); - expect(registeredTools.has("delete_proposal")).toBe(false); - - // Wallet management tools have requiresWallet: true, should be hidden - expect(registeredTools.has("get_wallet_address")).toBe(false); - expect(registeredTools.has("list_wallets")).toBe(false); - expect(registeredTools.has("select_wallet")).toBe(false); + // Write tools should still be registered without a wallet; execution fails later. + expect(registeredTools.has("transfer_trx")).toBe(true); + expect(registeredTools.has("transfer_trc20")).toBe(true); + expect(registeredTools.has("write_contract")).toBe(true); + expect(registeredTools.has("deploy_contract")).toBe(true); + expect(registeredTools.has("sign_message")).toBe(true); + expect(registeredTools.has("freeze_balance_v2")).toBe(true); + expect(registeredTools.has("unfreeze_balance_v2")).toBe(true); + expect(registeredTools.has("withdraw_expire_unfreeze")).toBe(true); + expect(registeredTools.has("cancel_all_unfreeze_v2")).toBe(true); + expect(registeredTools.has("delegate_resource")).toBe(true); + expect(registeredTools.has("undelegate_resource")).toBe(true); + expect(registeredTools.has("create_account")).toBe(true); + expect(registeredTools.has("update_account")).toBe(true); + expect(registeredTools.has("account_permission_update")).toBe(true); + expect(registeredTools.has("broadcast_transaction")).toBe(true); + expect(registeredTools.has("broadcast_hex")).toBe(true); + expect(registeredTools.has("create_transaction")).toBe(true); + + // Governance/proposal write tools should still be registered without a wallet. + expect(registeredTools.has("create_witness")).toBe(true); + expect(registeredTools.has("update_witness")).toBe(true); + expect(registeredTools.has("vote_witness")).toBe(true); + expect(registeredTools.has("withdraw_balance")).toBe(true); + expect(registeredTools.has("update_brokerage")).toBe(true); + expect(registeredTools.has("create_proposal")).toBe(true); + expect(registeredTools.has("approve_proposal")).toBe(true); + expect(registeredTools.has("delete_proposal")).toBe(true); + + // Wallet management tools should also stay visible; they fail at runtime if no wallet exists. + expect(registeredTools.has("get_wallet_address")).toBe(true); + expect(registeredTools.has("list_wallets")).toBe(true); + expect(registeredTools.has("select_wallet")).toBe(true); // Pure read tools should STILL be registered expect(registeredTools.has("get_balance")).toBe(true); @@ -405,17 +406,25 @@ describe("TRON Tools Unit Tests", () => { expect(content.address).toBe("T9yD14Nj9j7xAB4dbGeiX9h8unkKHxuWwb"); }); + it("get_wallet_address should return a structured error when no wallet is configured", async () => { + (services.getOwnerAddress as any).mockRejectedValue(new Error("Wallet not configured.")); + + const result = await registeredTools.get("get_wallet_address").handler({}); + expect(result.isError).toBe(true); + expect(result.content[0].text).toContain("Wallet not configured."); + }); + it("list_wallets should return wallet list with active ID", async () => { (services.listAgentWallets as any).mockResolvedValue([ - { id: "default", type: "env_configured", address: "T9yD14Nj9j7xAB4dbGeiX9h8unkKHxuWwb" }, + { id: "wallet-1", type: "agent_wallet", address: "T9yD14Nj9j7xAB4dbGeiX9h8unkKHxuWwb" }, ]); - (services.getActiveWalletId as any).mockReturnValue("default"); + (services.getActiveWalletId as any).mockReturnValue("wallet-1"); const result = await registeredTools.get("list_wallets").handler({}); const content = JSON.parse(result.content[0].text); - expect(content.activeWalletId).toBe("default"); + expect(content.activeWalletId).toBe("wallet-1"); expect(content.wallets).toHaveLength(1); - expect(content.wallets[0].id).toBe("default"); + expect(content.wallets[0].id).toBe("wallet-1"); expect(content.wallets[0].address).toBe("T9yD14Nj9j7xAB4dbGeiX9h8unkKHxuWwb"); }); @@ -441,14 +450,14 @@ describe("TRON Tools Unit Tests", () => { expect(services.selectWallet).toHaveBeenCalledWith("wallet-2"); }); - it("select_wallet should return error in static mode", async () => { + it("select_wallet should return error when multi-wallet support is unavailable", async () => { (services.selectWallet as any).mockRejectedValue( - new Error("select_wallet is not available in static mode"), + new Error("select_wallet is not available."), ); const result = await registeredTools.get("select_wallet").handler({ walletId: "some-id" }); expect(result.isError).toBe(true); - expect(result.content[0].text).toContain("not available in static mode"); + expect(result.content[0].text).toContain("select_wallet is not available."); }); it("convert_address should handle hex to base58", async () => { @@ -676,6 +685,7 @@ describe("TRON Tools Unit Tests", () => { }); it("deploy_contract should call deployContract service", async () => { + (services.getOwnerAddress as any).mockResolvedValue("TDefaultSender"); (services.deployContract as any).mockResolvedValue({ txID: "tx123", contractAddress: "Taddr", diff --git a/tests/core/tools_integration.test.ts b/tests/core/tools_integration.test.ts index 77a7f26..bddda04 100644 --- a/tests/core/tools_integration.test.ts +++ b/tests/core/tools_integration.test.ts @@ -1,14 +1,22 @@ -import { describe, it, expect, beforeEach } from "vitest"; +import { describe, it, expect, beforeEach, vi } from "vitest"; import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import * as agentWallet from "../../src/core/services/agent-wallet.js"; import { registerTRONTools } from "../../src/core/tools/index"; const USDT_ADDRESS_NILE = "TXYZopYRdj2D9XRtbG411XZZ3kM5VkAeBf"; const TEST_ADDRESS = "T9yD14Nj9j7xAB4dbGeiX9h8unkKHxuWwb"; +const HAS_REAL_KEY = false; -// Use real wallet if available (agent-wallet or static private key) -const REAL_KEY = process.env.TRON_PRIVATE_KEY; -const HAS_AGENT_WALLET = !!process.env.AGENT_WALLET_PASSWORD; -const HAS_REAL_KEY = HAS_AGENT_WALLET || (!!REAL_KEY && REAL_KEY.length === 64); +vi.mock("../../src/core/services/agent-wallet.js", async () => { + const actual = await vi.importActual( + "../../src/core/services/agent-wallet.js", + ); + return { + ...actual, + getOwnerAddress: vi.fn().mockRejectedValue(new Error("Wallet not configured.")), + getActiveWalletId: vi.fn().mockReturnValue(null), + }; +}); describe("TRON Tools Integration (Nile)", () => { let server: McpServer; @@ -28,20 +36,7 @@ describe("TRON Tools Integration (Nile)", () => { return originalRegisterTool(name, schema, handler); }; - // Use real wallet if available so write tool handlers can execute real transactions. - // If no wallet configured, set a dummy key just to register write tools. - const needsDummyKey = !HAS_REAL_KEY; - if (needsDummyKey) { - process.env.TRON_PRIVATE_KEY = - "0000000000000000000000000000000000000000000000000000000000000001"; - } - registerTRONTools(server); - - // Restore env if we used a dummy key - if (needsDummyKey) { - delete process.env.TRON_PRIVATE_KEY; - } }); it("get_balance should return real balance from Nile", async () => { @@ -347,21 +342,13 @@ describe("TRON Tools Integration (Nile)", () => { // Wallet & convert (read-only) // ============================================================================ - it("get_wallet_address should return configured address when wallet is set", async () => { + it("get_wallet_address should be registered without wallet config and fail at runtime", async () => { const tool = registeredTools.get("get_wallet_address"); expect(tool).toBeDefined(); - const result = await tool.handler({}); - // With a dummy key (when TRON_PRIVATE_KEY not set), derivation may fail - if (result.isError && !HAS_REAL_KEY) { - expect(result.content[0].text).toContain("Error"); - return; - } - expect(result.isError).toBeUndefined(); - const content = JSON.parse(result.content[0].text); - expect(content.address).toBeDefined(); - expect(content.base58).toBeDefined(); - expect(content.hex).toBeDefined(); + const result = await tool.handler({}); + expect(result.isError).toBe(true); + expect(result.content[0].text).toContain("Wallet not configured."); }); it("convert_address should convert Base58 to Hex on Nile", async () => { @@ -377,9 +364,7 @@ describe("TRON Tools Integration (Nile)", () => { expect(content.isValid).toBe(true); }); - it("staking tools (v2) should be registered and callable", async () => { - // These tests might fail if TRON_PRIVATE_KEY is not set or account has no balance, - // but the tool registration and handler calling should work. + it("staking tools (v2) should still be registered without wallet config", async () => { const freezeTool = registeredTools.get("freeze_balance_v2"); expect(freezeTool).toBeDefined(); @@ -392,12 +377,12 @@ describe("TRON Tools Integration (Nile)", () => { expect(registeredTools.has("cancel_all_unfreeze_v2")).toBe(true); }); - it("account resource (delegate) tools should be registered", () => { + it("account resource (delegate) tools should still be registered without wallet config", () => { expect(registeredTools.has("delegate_resource")).toBe(true); expect(registeredTools.has("undelegate_resource")).toBe(true); }); - it("deploy_contract tool should be registered", async () => { + it("deploy_contract tool should still be registered without wallet config", async () => { const deployTool = registeredTools.get("deploy_contract"); expect(deployTool).toBeDefined(); }); @@ -480,7 +465,7 @@ describe("TRON Tools Integration (Nile)", () => { expect(content.address).toBe(TEST_ADDRESS); }, 20000); - it("account write tools should be registered", () => { + it("account write tools should still be registered without wallet config", () => { expect(registeredTools.has("create_account")).toBe(true); expect(registeredTools.has("update_account")).toBe(true); expect(registeredTools.has("account_permission_update")).toBe(true); @@ -594,10 +579,9 @@ describe("TRON Tools Integration (Nile)", () => { // ========================================================================== // Governance & Proposal write tool integration tests (Nile) - // Requires TRON_PRIVATE_KEY in .env with a funded Nile testnet account. // ========================================================================== - it("governance write tools should all be registered", () => { + it("governance write tools should still be registered without wallet config", () => { const writeTools = [ "create_witness", "update_witness", diff --git a/tests/integration_stdio.ts b/tests/integration_stdio.ts index 0706fed..caafd60 100644 --- a/tests/integration_stdio.ts +++ b/tests/integration_stdio.ts @@ -1,29 +1,36 @@ import { spawn } from "child_process"; +import { mkdtempSync } from "fs"; import { dirname, join } from "path"; +import { tmpdir } from "os"; import { fileURLToPath } from "url"; const __dirname = dirname(fileURLToPath(import.meta.url)); const serverPath = join(__dirname, "../src/index.ts"); +const noWalletHome = mkdtempSync(join(tmpdir(), "mcp-server-tron-stdio-home-")); async function runIntegrationTest() { const isReadOnlyMode = process.argv.includes("--readonly") || process.argv.includes("-r"); - const noKey = process.argv.includes("--no-key"); - console.log( - `🚀 Starting Integration Test via Stdio (Readonly: ${isReadOnlyMode}, NoKey: ${noKey})...`, - ); + console.log(`🚀 Starting Integration Test via Stdio (Readonly: ${isReadOnlyMode})...`); const spawnArgs = ["tsx", serverPath]; if (isReadOnlyMode) { spawnArgs.push("--readonly"); } - const env = { ...process.env }; - if (noKey) { - delete env.TRON_PRIVATE_KEY; - delete env.TRON_MNEMONIC; - } else { - env.TRON_PRIVATE_KEY = "0000000000000000000000000000000000000000000000000000000000000001"; + const env = { ...process.env } as NodeJS.ProcessEnv; + for (const key of [ + "AGENT_WALLET_PASSWORD", + "AGENT_WALLET_DIR", + "AGENT_WALLET_PRIVATE_KEY", + "AGENT_WALLET_MNEMONIC", + "AGENT_WALLET_MNEMONIC_ACCOUNT_INDEX", + "TRON_PRIVATE_KEY", + "TRON_MNEMONIC", + "TRON_ACCOUNT_INDEX", + ]) { + delete env[key]; } + env.HOME = noWalletHome; const serverProcess = spawn("npx", spawnArgs, { env, @@ -104,24 +111,45 @@ async function runIntegrationTest() { const toolNames = toolsRes.result.tools.map((t: any) => t.name); console.log(`✅ Found ${toolNames.length} tools:`, toolNames.join(", ")); - if (isReadOnlyMode || noKey) { + if (isReadOnlyMode) { if (toolNames.includes("transfer_trx") || toolNames.includes("write_contract")) { - throw new Error(`Write tools found in ${noKey ? "NoKey" : "Readonly"} mode!`); + throw new Error("Write tools found in readonly mode!"); } - console.log(`✅ Verified: Write tools are filtered in ${noKey ? "NoKey" : "Readonly"} mode.`); + console.log("✅ Verified: Write tools are filtered in readonly mode."); } else { - if (!toolNames.includes("get_balance") || !toolNames.includes("transfer_trx")) { - throw new Error("Missing expected tools!"); + if (!toolNames.includes("transfer_trx") || !toolNames.includes("write_contract")) { + throw new Error("Write tools should still be registered without a wallet."); + } + if (!toolNames.includes("get_wallet_address")) { + throw new Error("Wallet tools should still be registered without a wallet."); } } - // 4. Call a Tool (get_supported_networks) - console.log("3️⃣ Calling get_supported_networks..."); - const callPromise = waitForResponse(3); + // 4. Call wallet-aware tool and ensure runtime failure is returned without a wallet. + console.log("3️⃣ Calling get_wallet_address..."); + const walletPromise = waitForResponse(3); send({ jsonrpc: "2.0", id: 3, method: "tools/call", + params: { + name: "get_wallet_address", + arguments: {}, + }, + }); + const walletRes = await walletPromise; + if (!walletRes.result?.isError) { + throw new Error("Expected get_wallet_address to fail without a wallet."); + } + console.log("✅ Wallet tool failed as expected:", walletRes.result.content[0].text); + + // 5. Call a Tool (get_supported_networks) + console.log("4️⃣ Calling get_supported_networks..."); + const callPromise = waitForResponse(4); + send({ + jsonrpc: "2.0", + id: 4, + method: "tools/call", params: { name: "get_supported_networks", arguments: {}, diff --git a/tests/server/http-app.test.ts b/tests/server/http-app.test.ts index 4bddf04..120db86 100644 --- a/tests/server/http-app.test.ts +++ b/tests/server/http-app.test.ts @@ -3,7 +3,7 @@ import { beforeEach, describe, expect, it, vi } from "vitest"; vi.mock("../../src/server/server.js", () => ({ default: vi.fn().mockRejectedValue(new Error("init boom")), MCP_PROTOCOL_VERSION: "2025-11-25", - version: "1.1.6", + version: "1.1.7", })); describe("createHttpApp", () => {