Algorand MCP
by GoPlausible
import { McpError, ErrorCode } from '@modelcontextprotocol/sdk/types.js';
import { AppTransactionManager, appTransactionTools } from '../../../src/tools/transactionManager/appTransactions.js';
import { algodClient } from '../../../src/algorand-client.js';
import algosdk, { OnApplicationComplete } from 'algosdk';
// Mock algosdk
jest.mock('algosdk', () => ({
makeApplicationCreateTxnFromObject: jest.fn(),
makeApplicationUpdateTxnFromObject: jest.fn(),
makeApplicationDeleteTxnFromObject: jest.fn(),
makeApplicationOptInTxnFromObject: jest.fn(),
makeApplicationCloseOutTxnFromObject: jest.fn(),
makeApplicationClearStateTxnFromObject: jest.fn(),
makeApplicationNoOpTxnFromObject: jest.fn(),
OnApplicationComplete: {
NoOpOC: 0,
OptInOC: 1,
CloseOutOC: 2,
ClearStateOC: 3,
UpdateApplicationOC: 4,
DeleteApplicationOC: 5,
},
}));
// Mock algodClient
jest.mock('../../../src/algorand-client.js', () => ({
algodClient: {
getTransactionParams: jest.fn().mockReturnValue({
do: jest.fn().mockResolvedValue({
firstRound: 1000,
lastRound: 2000,
genesisHash: 'hash',
genesisID: 'testnet-v1.0',
fee: 1000,
flatFee: true,
}),
}),
},
}));
describe('AppTransactionManager', () => {
const mockSuggestedParams = {
firstRound: 1000,
lastRound: 2000,
genesisHash: 'hash',
genesisID: 'testnet-v1.0',
fee: 1000,
flatFee: true,
};
beforeEach(() => {
jest.clearAllMocks();
(algodClient.getTransactionParams as jest.Mock)().do.mockResolvedValue(mockSuggestedParams);
});
describe('Tool Schemas', () => {
it('should have valid tool schemas', () => {
expect(appTransactionTools).toHaveLength(7);
expect(appTransactionTools.map((t: { name: string }) => t.name)).toEqual([
'make_app_create_txn',
'make_app_update_txn',
'make_app_delete_txn',
'make_app_optin_txn',
'make_app_closeout_txn',
'make_app_clear_txn',
'make_app_call_txn',
]);
});
});
describe('Application Creation', () => {
const mockAppCreateTxn = { type: 'appl', from: 'sender' };
beforeEach(() => {
(algosdk.makeApplicationCreateTxnFromObject as jest.Mock).mockReturnValue(mockAppCreateTxn);
});
it('should create a basic application creation transaction', async () => {
const args = {
from: 'sender',
approvalProgram: '#pragma version 6\nint 1',
clearProgram: '#pragma version 6\nint 1',
globalByteSlices: 1,
globalInts: 1,
localByteSlices: 1,
localInts: 1,
};
const result = await AppTransactionManager.handleTool('make_app_create_txn', args);
expect(result).toEqual({
content: [{
type: 'text',
text: JSON.stringify(mockAppCreateTxn, null, 2),
}],
});
expect(algosdk.makeApplicationCreateTxnFromObject).toHaveBeenCalledWith(
expect.objectContaining({
from: 'sender',
approvalProgram: new TextEncoder().encode(args.approvalProgram),
clearProgram: new TextEncoder().encode(args.clearProgram),
globalByteSlices: 1,
globalInts: 1,
localByteSlices: 1,
localInts: 1,
onComplete: 0,
suggestedParams: expect.objectContaining(mockSuggestedParams),
})
);
});
it('should create an application with optional parameters', async () => {
const args = {
from: 'sender',
approvalProgram: '#pragma version 6\nint 1',
clearProgram: '#pragma version 6\nint 1',
globalByteSlices: 1,
globalInts: 1,
localByteSlices: 1,
localInts: 1,
extraPages: 1,
note: 'test note',
lease: 'test lease',
rekeyTo: 'rekey-addr',
appArgs: ['arg1', 'arg2'],
accounts: ['acc1', 'acc2'],
foreignApps: [1, 2],
foreignAssets: [3, 4],
};
const result = await AppTransactionManager.handleTool('make_app_create_txn', args);
expect(algosdk.makeApplicationCreateTxnFromObject).toHaveBeenCalledWith(
expect.objectContaining({
from: 'sender',
approvalProgram: new TextEncoder().encode(args.approvalProgram),
clearProgram: new TextEncoder().encode(args.clearProgram),
globalByteSlices: 1,
globalInts: 1,
localByteSlices: 1,
localInts: 1,
extraPages: 1,
note: new TextEncoder().encode('test note'),
lease: new TextEncoder().encode('test lease'),
rekeyTo: 'rekey-addr',
appArgs: args.appArgs.map(arg => new TextEncoder().encode(arg)),
accounts: ['acc1', 'acc2'],
foreignApps: [1, 2],
foreignAssets: [3, 4],
onComplete: 0,
suggestedParams: expect.objectContaining(mockSuggestedParams),
})
);
});
});
describe('Application Update', () => {
const mockAppUpdateTxn = { type: 'appl', from: 'sender' };
beforeEach(() => {
(algosdk.makeApplicationUpdateTxnFromObject as jest.Mock).mockReturnValue(mockAppUpdateTxn);
});
it('should create an application update transaction', async () => {
const args = {
from: 'sender',
appID: 123,
approvalProgram: '#pragma version 6\nint 1',
clearProgram: '#pragma version 6\nint 1',
};
const result = await AppTransactionManager.handleTool('make_app_update_txn', args);
expect(algosdk.makeApplicationUpdateTxnFromObject).toHaveBeenCalledWith(
expect.objectContaining({
from: 'sender',
appID: 123,
approvalProgram: new TextEncoder().encode(args.approvalProgram),
clearProgram: new TextEncoder().encode(args.clearProgram),
onComplete: 4,
suggestedParams: expect.objectContaining(mockSuggestedParams),
})
);
});
});
describe('Application Delete', () => {
const mockAppDeleteTxn = { type: 'appl', from: 'sender' };
beforeEach(() => {
(algosdk.makeApplicationDeleteTxnFromObject as jest.Mock).mockReturnValue(mockAppDeleteTxn);
});
it('should create an application delete transaction', async () => {
const args = {
from: 'sender',
appID: 123,
};
const result = await AppTransactionManager.handleTool('make_app_delete_txn', args);
expect(algosdk.makeApplicationDeleteTxnFromObject).toHaveBeenCalledWith(
expect.objectContaining({
from: 'sender',
appID: 123,
onComplete: 5,
suggestedParams: expect.objectContaining(mockSuggestedParams),
})
);
});
});
describe('Application Opt-In', () => {
const mockAppOptInTxn = { type: 'appl', from: 'sender' };
beforeEach(() => {
(algosdk.makeApplicationOptInTxnFromObject as jest.Mock).mockReturnValue(mockAppOptInTxn);
});
it('should create an application opt-in transaction', async () => {
const args = {
from: 'sender',
appID: 123,
};
const result = await AppTransactionManager.handleTool('make_app_optin_txn', args);
expect(algosdk.makeApplicationOptInTxnFromObject).toHaveBeenCalledWith(
expect.objectContaining({
from: 'sender',
appID: 123,
onComplete: 1,
suggestedParams: expect.objectContaining(mockSuggestedParams),
})
);
});
});
describe('Application Close-Out', () => {
const mockAppCloseOutTxn = { type: 'appl', from: 'sender' };
beforeEach(() => {
(algosdk.makeApplicationCloseOutTxnFromObject as jest.Mock).mockReturnValue(mockAppCloseOutTxn);
});
it('should create an application close-out transaction', async () => {
const args = {
from: 'sender',
appID: 123,
};
const result = await AppTransactionManager.handleTool('make_app_closeout_txn', args);
expect(algosdk.makeApplicationCloseOutTxnFromObject).toHaveBeenCalledWith(
expect.objectContaining({
from: 'sender',
appID: 123,
onComplete: 2,
suggestedParams: expect.objectContaining(mockSuggestedParams),
})
);
});
});
describe('Application Clear State', () => {
const mockAppClearTxn = { type: 'appl', from: 'sender' };
beforeEach(() => {
(algosdk.makeApplicationClearStateTxnFromObject as jest.Mock).mockReturnValue(mockAppClearTxn);
});
it('should create an application clear state transaction', async () => {
const args = {
from: 'sender',
appID: 123,
};
const result = await AppTransactionManager.handleTool('make_app_clear_txn', args);
expect(algosdk.makeApplicationClearStateTxnFromObject).toHaveBeenCalledWith(
expect.objectContaining({
from: 'sender',
appID: 123,
onComplete: 3,
suggestedParams: expect.objectContaining(mockSuggestedParams),
})
);
});
});
describe('Application Call', () => {
const mockAppCallTxn = { type: 'appl', from: 'sender' };
beforeEach(() => {
(algosdk.makeApplicationNoOpTxnFromObject as jest.Mock).mockReturnValue(mockAppCallTxn);
});
it('should create an application call transaction', async () => {
const args = {
from: 'sender',
appID: 123,
appArgs: ['method', 'arg1'],
accounts: ['acc1'],
foreignApps: [1],
foreignAssets: [2],
};
const result = await AppTransactionManager.handleTool('make_app_call_txn', args);
expect(algosdk.makeApplicationNoOpTxnFromObject).toHaveBeenCalledWith(
expect.objectContaining({
from: 'sender',
appID: 123,
appArgs: args.appArgs.map(arg => new TextEncoder().encode(arg)),
accounts: ['acc1'],
foreignApps: [1],
foreignAssets: [2],
onComplete: 0,
suggestedParams: expect.objectContaining(mockSuggestedParams),
})
);
});
});
describe('Error Handling', () => {
it('should throw error for unknown tool', async () => {
await expect(AppTransactionManager.handleTool('unknown_tool', {}))
.rejects
.toThrow(new McpError(ErrorCode.MethodNotFound, 'Unknown application transaction tool: unknown_tool'));
});
it('should throw error for missing application creation parameters', async () => {
await expect(AppTransactionManager.handleTool('make_app_create_txn', {}))
.rejects
.toThrow(new McpError(ErrorCode.InvalidParams, 'Invalid application creation parameters'));
});
it('should throw error for missing application update parameters', async () => {
await expect(AppTransactionManager.handleTool('make_app_update_txn', {}))
.rejects
.toThrow(new McpError(ErrorCode.InvalidParams, 'Invalid application update parameters'));
});
it('should throw error for missing application delete parameters', async () => {
await expect(AppTransactionManager.handleTool('make_app_delete_txn', {}))
.rejects
.toThrow(new McpError(ErrorCode.InvalidParams, 'Invalid application delete parameters'));
});
it('should throw error for missing application opt-in parameters', async () => {
await expect(AppTransactionManager.handleTool('make_app_optin_txn', {}))
.rejects
.toThrow(new McpError(ErrorCode.InvalidParams, 'Invalid application opt-in parameters'));
});
it('should throw error for missing application close-out parameters', async () => {
await expect(AppTransactionManager.handleTool('make_app_closeout_txn', {}))
.rejects
.toThrow(new McpError(ErrorCode.InvalidParams, 'Invalid application close out parameters'));
});
it('should throw error for missing application clear state parameters', async () => {
await expect(AppTransactionManager.handleTool('make_app_clear_txn', {}))
.rejects
.toThrow(new McpError(ErrorCode.InvalidParams, 'Invalid application clear state parameters'));
});
it('should throw error for missing application call parameters', async () => {
await expect(AppTransactionManager.handleTool('make_app_call_txn', {}))
.rejects
.toThrow(new McpError(ErrorCode.InvalidParams, 'Invalid application call parameters'));
});
it('should handle algod client errors', async () => {
const error = new Error('Network error');
(algodClient.getTransactionParams as jest.Mock)().do.mockRejectedValue(error);
await expect(AppTransactionManager.handleTool('make_app_create_txn', {
from: 'sender',
approvalProgram: '#pragma version 6\nint 1',
clearProgram: '#pragma version 6\nint 1',
globalByteSlices: 1,
globalInts: 1,
localByteSlices: 1,
localInts: 1,
}))
.rejects
.toThrow(error);
});
});
});