Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion packages/mcp-server/src/core/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
9 changes: 1 addition & 8 deletions packages/mcp-server/src/tests/core/config.test.ts
Original file line number Diff line number Diff line change
@@ -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.
Expand All @@ -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 };
Expand Down
127 changes: 124 additions & 3 deletions packages/mcp-server/src/tests/core/services/contracts.test.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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';
Expand Down Expand Up @@ -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
});
});
});
});
63 changes: 63 additions & 0 deletions packages/mcp-server/src/tests/core/tools.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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');
});
});