From 532f2816f6a6183fa691531ec740dec3e0bd52b1 Mon Sep 17 00:00:00 2001 From: carson Date: Wed, 11 Jun 2025 12:32:53 -0600 Subject: [PATCH 1/2] Added tests for missing `deployContract` function --- .../src/tests/core/services/contracts.test.ts | 127 +++++++++++++++++- 1 file changed, 124 insertions(+), 3 deletions(-) diff --git a/packages/mcp-server/src/tests/core/services/contracts.test.ts b/packages/mcp-server/src/tests/core/services/contracts.test.ts index f382e132f..9a1635c90 100644 --- a/packages/mcp-server/src/tests/core/services/contracts.test.ts +++ b/packages/mcp-server/src/tests/core/services/contracts.test.ts @@ -1,5 +1,5 @@ import { describe, test, expect, jest, beforeEach } from '@jest/globals'; -import { readContract, writeContract, getLogs, isContract } from '../../../core/services/contracts.js'; +import { readContract, writeContract, getLogs, isContract, deployContract } from '../../../core/services/contracts.js'; import { getPublicClient, getWalletClient } from '../../../core/services/clients.js'; import { getPrivateKeyAsHex } from '../../../core/config.js'; import type { Hash, Abi, Address, GetLogsParameters, ReadContractParameters, WriteContractParameters } from 'viem'; @@ -18,11 +18,15 @@ describe('Contract Service', () => { const mockPublicClient = { readContract: jest.fn(), getLogs: jest.fn(), - getBytecode: jest.fn() + getBytecode: jest.fn(), + waitForTransactionReceipt: jest.fn() }; const mockWalletClient = { - writeContract: jest.fn() + writeContract: jest.fn(), + deployContract: jest.fn(), + account: '0x1234567890123456789012345678901234567890' as Address, + chain: { id: 1, name: 'Sei' } }; const mockPrivateKey = '0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef'; @@ -181,4 +185,121 @@ describe('Contract Service', () => { expect(getPublicClient).toHaveBeenCalledWith('sei'); }); }); + + describe('deployContract', () => { + const mockBytecode = '0x608060405234801561001057600080fd5b50' as Hash; + const mockAbi = [ + { + inputs: [{ name: 'name', type: 'string' }, { name: 'symbol', type: 'string' }], + stateMutability: 'nonpayable', + type: 'constructor' + } + ]; + const mockArgs = ['TestToken', 'TTK']; + const mockContractAddress = '0x9876543210987654321098765432109876543210' as Address; + const mockTransactionHash = '0xabcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890' as Hash; + + beforeEach(() => { + // Setup successful deployment mocks + mockWalletClient.deployContract.mockImplementation(() => Promise.resolve(mockTransactionHash)); + mockPublicClient.waitForTransactionReceipt.mockImplementation(() => Promise.resolve({ + contractAddress: mockContractAddress, + transactionHash: mockTransactionHash, + status: 'success' + })); + }); + + test('should deploy contract successfully with all parameters', async () => { + const result = await deployContract(mockBytecode, mockAbi, mockArgs, 'sei'); + + expect(getPrivateKeyAsHex).toHaveBeenCalled(); + expect(getWalletClient).toHaveBeenCalledWith(mockPrivateKey, 'sei'); + expect(mockWalletClient.deployContract).toHaveBeenCalledWith({ + abi: mockAbi, + bytecode: mockBytecode, + args: mockArgs, + account: mockWalletClient.account, + chain: mockWalletClient.chain + }); + expect(getPublicClient).toHaveBeenCalledWith('sei'); + expect(mockPublicClient.waitForTransactionReceipt).toHaveBeenCalledWith({ hash: mockTransactionHash }); + expect(result).toEqual({ + address: mockContractAddress, + transactionHash: mockTransactionHash + }); + }); + + test('should deploy contract successfully without constructor arguments', async () => { + const result = await deployContract(mockBytecode, mockAbi, undefined, 'sei'); + + expect(mockWalletClient.deployContract).toHaveBeenCalledWith({ + abi: mockAbi, + bytecode: mockBytecode, + args: [], + account: mockWalletClient.account, + chain: mockWalletClient.chain + }); + expect(result).toEqual({ + address: mockContractAddress, + transactionHash: mockTransactionHash + }); + }); + + test('should use default network when none is specified', async () => { + await deployContract(mockBytecode, mockAbi, mockArgs); + + expect(getWalletClient).toHaveBeenCalledWith(mockPrivateKey, 'sei'); + expect(getPublicClient).toHaveBeenCalledWith('sei'); + }); + + test('should throw error when private key is not available', async () => { + (getPrivateKeyAsHex as jest.Mock).mockReturnValue(null); + + await expect(deployContract(mockBytecode, mockAbi, mockArgs, 'sei')).rejects.toThrow( + 'Private key not available. Set the PRIVATE_KEY environment variable and restart the MCP server.' + ); + expect(getWalletClient).not.toHaveBeenCalled(); + }); + + test('should throw error when wallet client account is not available', async () => { + const mockWalletClientWithoutAccount = { + ...mockWalletClient, + account: undefined + }; + (getWalletClient as jest.Mock).mockReturnValue(mockWalletClientWithoutAccount); + + await expect(deployContract(mockBytecode, mockAbi, mockArgs, 'sei')).rejects.toThrow( + 'Wallet client account not available for contract deployment.' + ); + expect(mockWalletClientWithoutAccount.deployContract).not.toHaveBeenCalled(); + }); + + test('should throw error when contract deployment fails - no contract address returned', async () => { + mockPublicClient.waitForTransactionReceipt.mockImplementation(() => Promise.resolve({ + contractAddress: null, + transactionHash: mockTransactionHash, + status: 'success' + })); + + await expect(deployContract(mockBytecode, mockAbi, mockArgs, 'sei')).rejects.toThrow( + 'Contract deployment failed - no contract address returned' + ); + }); + + test('should handle deployment with empty args array', async () => { + const result = await deployContract(mockBytecode, mockAbi, [], 'sei'); + + expect(mockWalletClient.deployContract).toHaveBeenCalledWith({ + abi: mockAbi, + bytecode: mockBytecode, + args: [], + account: mockWalletClient.account, + chain: mockWalletClient.chain + }); + expect(result).toEqual({ + address: mockContractAddress, + transactionHash: mockTransactionHash + }); + }); + }); }); From 088eb9e3d69e681d2fa8378146e341085a3d43e9 Mon Sep 17 00:00:00 2001 From: carson Date: Wed, 11 Jun 2025 12:47:27 -0600 Subject: [PATCH 2/2] Added missing tests and fixed broken one --- packages/mcp-server/src/core/config.ts | 2 +- .../mcp-server/src/tests/core/config.test.ts | 9 +-- .../mcp-server/src/tests/core/tools.test.ts | 63 +++++++++++++++++++ 3 files changed, 65 insertions(+), 9 deletions(-) diff --git a/packages/mcp-server/src/core/config.ts b/packages/mcp-server/src/core/config.ts index 5104e8a99..31d6dc268 100644 --- a/packages/mcp-server/src/core/config.ts +++ b/packages/mcp-server/src/core/config.ts @@ -14,7 +14,7 @@ const envSchema = z.object({ const env = envSchema.safeParse(process.env); // Format private key with 0x prefix if it exists -const formatPrivateKey = (key?: string): string | undefined => { +export const formatPrivateKey = (key?: string): string | undefined => { if (!key) return undefined; // Ensure the private key has 0x prefix diff --git a/packages/mcp-server/src/tests/core/config.test.ts b/packages/mcp-server/src/tests/core/config.test.ts index 0a3ce0318..c68288220 100644 --- a/packages/mcp-server/src/tests/core/config.test.ts +++ b/packages/mcp-server/src/tests/core/config.test.ts @@ -1,6 +1,5 @@ import { afterEach, beforeEach, describe, expect, test } from '@jest/globals'; -import { config, getPrivateKeyAsHex } from '../../core/config.js'; -import { formatPrivateKey as mockFormatPrivateKey, initializeConfig, getPrivateKeyAsHex as mockGetPrivateKeyAsHex } from './helpers/config-utils.js'; +import { config, formatPrivateKey, getPrivateKeyAsHex } from '../../core/config.js'; /** * This file contains tests for both the actual config implementation and the mock config implementation. @@ -15,12 +14,6 @@ jest.mock('dotenv', () => ({ config: jest.fn() })); -// Get direct access to the formatPrivateKey function for testing -const formatPrivateKey = (key?: string): string | undefined => { - if (!key) return undefined; - return key.startsWith('0x') ? key : `0x${key}`; -}; - describe('Config Module - Actual Implementation', () => { // Store original environment const originalEnv = { ...process.env }; diff --git a/packages/mcp-server/src/tests/core/tools.test.ts b/packages/mcp-server/src/tests/core/tools.test.ts index 9aaa6e5ca..164e898f2 100644 --- a/packages/mcp-server/src/tests/core/tools.test.ts +++ b/packages/mcp-server/src/tests/core/tools.test.ts @@ -2709,4 +2709,67 @@ describe('EVM Tools', () => { expect(response).toHaveProperty('isError', true); expect(response.content[0].text).toContain('Error deploying contract: This is a string error'); }); + + test('deploy_contract - with ABI as string', async () => { + const tool = checkToolExists('deploy_contract'); + if (!tool) return; + + const mockDeployResult = { + address: '0x1234567890123456789012345678901234567890' as `0x${string}`, + transactionHash: '0xabcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890' as `0x${string}` + }; + + (services.deployContract as jest.Mock).mockImplementationOnce(() => { + return Promise.resolve(mockDeployResult); + }); + + const params = { + bytecode: '0x608060405234801561001057600080fd5b50', + abi: JSON.stringify([{ inputs: [], stateMutability: 'nonpayable', type: 'constructor' }]), // ABI as string + network: mockNetwork + }; + + const response = await testToolSuccess(tool, params); + + expect(response).toHaveProperty('content'); + expect(response.content[0]).toHaveProperty('type', 'text'); + }); + + test('is_contract - returns false (EOA)', async () => { + const tool = checkToolExists('is_contract'); + if (!tool) return; + + (services.isContract as jest.Mock).mockImplementationOnce(() => { + return Promise.resolve(false); + }); + + const params = { + address: '0x0987654321098765432109876543210987654321', + network: mockNetwork + }; + + const response = await tool.handler(params); + + expect(response.content[0].text).toContain('Externally Owned Account (EOA)'); + }); + + test('check_nft_ownership - returns false (does not own)', async () => { + const tool = checkToolExists('check_nft_ownership'); + if (!tool) return; + + (services.isNFTOwner as jest.Mock).mockImplementationOnce(() => { + return Promise.resolve(false); + }); + + const params = { + tokenAddress: mockTokenAddress, + tokenId: mockTokenId, + ownerAddress: '0x1234567890123456789012345678901234567890', + network: mockNetwork + }; + + const response = await tool.handler(params); + + expect(response.content[0].text).toContain('Address does not own this NFT'); + }); });