Algorand MCP
by GoPlausible
import { McpError, ErrorCode } from '@modelcontextprotocol/sdk/types.js';
import { GeneralTransactionManager, generalTransactionTools } from '../../../src/tools/transactionManager/generalTransaction.js';
import algosdk from 'algosdk';
import type { Transaction } from 'algosdk';
// Mock algosdk
jest.mock('algosdk');
// Create mock functions
const mockTransaction = jest.fn().mockImplementation((txn: any) => ({
...txn,
get_obj_for_encoding: () => txn,
}));
const mockAssignGroupID = jest.fn().mockImplementation((txns: any[]) =>
txns.map(txn => ({
...txn,
group: 'group1',
}))
);
// Override mock implementations
(algosdk as any).Transaction = mockTransaction;
(algosdk as any).assignGroupID = mockAssignGroupID;
describe('GeneralTransactionManager', () => {
beforeEach(() => {
jest.clearAllMocks();
});
describe('Tool Schemas', () => {
it('should have valid tool schemas', () => {
expect(generalTransactionTools).toHaveLength(1);
expect(generalTransactionTools.map((t: { name: string }) => t.name)).toEqual([
'assign_group_id',
]);
});
});
describe('Group ID Assignment', () => {
const mockTxn1 = { type: 'pay', from: 'sender1' };
const mockTxn2 = { type: 'pay', from: 'sender2' };
const mockGroupedTxns = [
{ ...mockTxn1, group: 'group1' },
{ ...mockTxn2, group: 'group1' },
];
beforeEach(() => {
// Reset mock implementations
mockTransaction.mockImplementation((txn: any) => ({
...txn,
get_obj_for_encoding: () => txn,
}));
mockAssignGroupID.mockImplementation((txns: any[]) =>
txns.map(txn => ({
...txn,
group: 'group1',
}))
);
});
it('should assign group ID to transactions', async () => {
const args = {
transactions: [mockTxn1, mockTxn2],
};
const result = await GeneralTransactionManager.handleTool('assign_group_id', args);
expect(result).toEqual({
content: [{
type: 'text',
text: JSON.stringify(mockGroupedTxns, null, 2),
}],
});
expect(mockTransaction).toHaveBeenCalledTimes(2);
expect(mockTransaction).toHaveBeenCalledWith(mockTxn1);
expect(mockTransaction).toHaveBeenCalledWith(mockTxn2);
expect(mockAssignGroupID).toHaveBeenCalledWith(
expect.arrayContaining([
expect.objectContaining(mockTxn1),
expect.objectContaining(mockTxn2),
])
);
});
it('should handle single transaction', async () => {
const args = {
transactions: [mockTxn1],
};
const result = await GeneralTransactionManager.handleTool('assign_group_id', args);
expect(mockTransaction).toHaveBeenCalledTimes(1);
expect(mockTransaction).toHaveBeenCalledWith(mockTxn1);
expect(mockAssignGroupID).toHaveBeenCalledWith(
expect.arrayContaining([
expect.objectContaining(mockTxn1),
])
);
});
});
describe('Error Handling', () => {
it('should throw error for unknown tool', async () => {
await expect(GeneralTransactionManager.handleTool('unknown_tool', {}))
.rejects
.toThrow(new McpError(ErrorCode.MethodNotFound, 'Unknown general transaction tool: unknown_tool'));
});
it('should throw error for missing transactions array', async () => {
await expect(GeneralTransactionManager.handleTool('assign_group_id', {}))
.rejects
.toThrow(new McpError(ErrorCode.InvalidParams, 'Transactions array is required'));
});
it('should throw error for invalid transactions array', async () => {
await expect(GeneralTransactionManager.handleTool('assign_group_id', {
transactions: 'not-an-array'
}))
.rejects
.toThrow(new McpError(ErrorCode.InvalidParams, 'Transactions array is required'));
});
it('should throw error for invalid transaction object', async () => {
await expect(GeneralTransactionManager.handleTool('assign_group_id', {
transactions: [null]
}))
.rejects
.toThrow(new McpError(ErrorCode.InvalidParams, 'Each transaction must be a valid transaction object'));
});
it('should handle transaction creation errors', async () => {
const error = new Error('Invalid transaction format');
mockTransaction.mockImplementation(() => {
throw error;
});
await expect(GeneralTransactionManager.handleTool('assign_group_id', {
transactions: [{ type: 'invalid' }]
}))
.rejects
.toThrow(new McpError(ErrorCode.InvalidParams, 'Failed to assign group ID: Invalid transaction format'));
});
it('should handle group ID assignment errors', async () => {
// Reset Transaction mock to succeed
mockTransaction.mockImplementation((txn: any) => ({
...txn,
get_obj_for_encoding: () => txn,
}));
// Make assignGroupID throw
const error = new Error('Group ID assignment failed');
mockAssignGroupID.mockImplementation(() => {
throw error;
});
await expect(GeneralTransactionManager.handleTool('assign_group_id', {
transactions: [
{ type: 'pay', from: 'sender1' },
{ type: 'pay', from: 'sender2' }
]
}))
.rejects
.toThrow(new McpError(ErrorCode.InvalidParams, 'Failed to assign group ID: Group ID assignment failed'));
});
});
describe('Transaction Object Conversion', () => {
beforeEach(() => {
// Reset mock implementations
mockTransaction.mockImplementation((txn: any) => ({
...txn,
get_obj_for_encoding: () => txn,
}));
mockAssignGroupID.mockImplementation((txns: any[]) =>
txns.map(txn => ({
...txn,
group: 'group1',
}))
);
});
it('should convert transaction objects to Transaction instances', async () => {
const mockTxn = {
type: 'pay',
from: 'sender',
to: 'receiver',
amount: 1000,
};
await GeneralTransactionManager.handleTool('assign_group_id', {
transactions: [mockTxn, { ...mockTxn, from: 'sender2' }]
});
expect(mockTransaction).toHaveBeenCalledWith(mockTxn);
});
it('should preserve transaction properties after conversion', async () => {
const mockTxn = {
type: 'pay',
from: 'sender',
to: 'receiver',
amount: 1000,
note: new Uint8Array([1, 2, 3]),
lease: new Uint8Array([4, 5, 6]),
};
const result = await GeneralTransactionManager.handleTool('assign_group_id', {
transactions: [mockTxn, { ...mockTxn, from: 'sender2' }]
});
const resultTxn = JSON.parse(result.content[0].text)[0];
expect(resultTxn).toEqual(expect.objectContaining({
type: 'pay',
from: 'sender',
to: 'receiver',
amount: 1000,
}));
});
});
});