UNPKG

@chainreactionom/nano-mcp

Version:

NANO cryptocurrency wallet implementation for MCP with comprehensive testing

476 lines (403 loc) 15.6 kB
import { MCPServer, MCPMethod, MCPServerConfig, RPCConfig } from './mcp-types.js'; import { rpcCall } from './rpc.js'; import { convert, Unit } from 'nanocurrency'; import * as nanocurrency from 'nanocurrency'; import * as nanoWeb from 'nanocurrency-web'; import { block } from 'nanocurrency-web'; import { Logger } from './utils/logger.js'; import { KeyManager } from './utils/key-manager.js'; import path from 'path'; // Use a relative path for logs const LOG_DIR = path.join(process.cwd(), 'logs'); interface NanoBalance { balance: string; pending: string; receivable: string; } interface NanoAccountInfo { frontier: string; open_block: string; representative_block: string; balance: string; modified_timestamp: string; block_count: string; representative: string; } interface WalletInfo { address: string; privateKey: string; publicKey: string; } interface TransactionResult { success: boolean; hash?: string; error?: string; } export class NanoMCP extends MCPServer { private methods: Map<string, MCPMethod>; private logger: Logger; private keyManager: KeyManager; constructor(rpcConfig?: RPCConfig) { const serverConfig: MCPServerConfig = { name: 'nano', description: 'MCP server for NANO (XNO) cryptocurrency', version: '1.0.0', author: 'Your Name', }; super(serverConfig, rpcConfig); // Initialize logger and key manager this.logger = new Logger(LOG_DIR); this.keyManager = new KeyManager(this.logger); this.methods = new Map(); // Register MCP methods this.methods.set('getBalance', this.getBalance.bind(this)); this.methods.set('getAccountInfo', this.getAccountInfo.bind(this)); this.methods.set('getBlockCount', this.getBlockCount.bind(this)); this.methods.set('getVersion', this.getVersion.bind(this)); this.methods.set('convertToNano', async (rawAmount: string) => Promise.resolve(this.convertToNano(rawAmount))); this.methods.set('convertFromNano', async (nanoAmount: string) => Promise.resolve(this.convertFromNano(nanoAmount))); this.methods.set('generateWallet', this.generateWallet.bind(this)); this.methods.set('sendTransaction', this.sendTransaction.bind(this)); this.methods.set('receivePending', this.receivePending.bind(this)); this.methods.set('initializeAccount', this.initializeAccount.bind(this)); this.methods.set('createWallet', this.createWallet.bind(this)); this.methods.set('createAccount', this.createAccount.bind(this)); this.methods.set('send', this.send.bind(this)); this.methods.set('receiveAll', this.receiveAll.bind(this)); this.methods.set('backupWallet', this.backupWallet.bind(this)); } setRPCConfig(config: RPCConfig): void { this.rpcConfig = { ...this.rpcConfig, ...config }; } async getBalance(address: string): Promise<NanoBalance> { try { const formattedAddress = address.replace('xrb_', 'nano_'); this.logger.log('GET_BALANCE', { address: formattedAddress }); const response = await rpcCall('account_balance', { account: formattedAddress, include_only_confirmed: true }, this.rpcConfig); return { balance: response.balance || '0', pending: response.pending || '0', receivable: response.receivable || '0' }; } catch (error) { this.logger.logError('GET_BALANCE_ERROR', error); throw error; } } async getAccountInfo(address: string): Promise<NanoAccountInfo> { const response = await rpcCall('account_info', { account: address }, this.rpcConfig); return response as NanoAccountInfo; } async getBlockCount(): Promise<{ count: string; unchecked: string; }> { const response = await rpcCall('block_count', {}, this.rpcConfig); return { count: response.count, unchecked: response.unchecked }; } async getVersion(): Promise<{ node_vendor: string; }> { const response = await rpcCall('version', {}, this.rpcConfig); return { node_vendor: response.node_vendor }; } async generateWallet(): Promise<WalletInfo> { try { return await this.keyManager.generateKeyPair(); } catch (error) { this.logger.logError('GENERATE_WALLET_ERROR', error); throw error; } } async sendTransaction(fromAddress: string, privateKey: string, toAddress: string, amountRaw: string): Promise<TransactionResult> { try { const formattedFromAddress = fromAddress.replace('xrb_', 'nano_'); const formattedToAddress = toAddress.replace('xrb_', 'nano_'); const accountInfo = await rpcCall('account_info', { account: formattedFromAddress, representative: true, json_block: 'true' }, this.rpcConfig); if (accountInfo.error) { throw new Error(accountInfo.error); } const workData = await rpcCall('work_generate', { hash: accountInfo.frontier, difficulty: 'fffffff800000000' }, this.rpcConfig); if (!workData || !workData.work) { throw new Error('Failed to generate work'); } const blockData = { walletBalanceRaw: accountInfo.balance, fromAddress: formattedFromAddress, toAddress: formattedToAddress, representativeAddress: accountInfo.representative, frontier: accountInfo.frontier, amountRaw: amountRaw, work: workData.work }; const signedBlock = block.send(blockData, privateKey); const processData = await rpcCall('process', { json_block: 'true', subtype: 'send', block: signedBlock }, this.rpcConfig); return { success: true, hash: processData.hash }; } catch (error) { this.logger.logError('SEND_TRANSACTION_ERROR', error); return { success: false, error: error instanceof Error ? error.message : 'Unknown error' }; } } async receivePending(address: string, privateKey: string): Promise<TransactionResult> { try { const formattedAddress = address.replace('xrb_', 'nano_'); this.logger.log('RECEIVE_PENDING', { address: formattedAddress }); const pendingData = await rpcCall('pending', { account: formattedAddress, threshold: '1', source: true, include_active: true, include_only_confirmed: true }, this.rpcConfig); if (!pendingData.blocks || Object.keys(pendingData.blocks).length === 0) { return { success: true }; } let receivedCount = 0; for (const blockHash of Object.keys(pendingData.blocks)) { try { const blockInfo = await rpcCall('blocks_info', { hashes: [blockHash], json_block: 'true', pending: true, source: true }, this.rpcConfig); const accountInfo = await rpcCall('account_info', { account: formattedAddress, representative: true }, this.rpcConfig).catch(() => null); const previous = accountInfo?.frontier || '0'.repeat(64); const representative = accountInfo?.representative || formattedAddress; const currentBalance = accountInfo?.balance || '0'; const amountRaw = blockInfo.blocks[blockHash].amount; // Get public key from the private key const wallet = await nanoWeb.wallet.generate(privateKey); const publicKey = wallet.accounts[0].publicKey; const workHash = previous === '0'.repeat(64) ? publicKey : previous; const workData = await rpcCall('work_generate', { hash: workHash, difficulty: previous === '0'.repeat(64) ? 'fffffff800000000' : 'fffffe0000000000' }, this.rpcConfig); if (!workData || !workData.work) { throw new Error('Failed to generate work'); } const blockData = { walletBalanceRaw: currentBalance, fromAddress: blockInfo.blocks[blockHash].source, toAddress: formattedAddress, representativeAddress: representative, frontier: previous, amountRaw: amountRaw, work: workData.work, transactionHash: blockHash }; const signedBlock = block.receive(blockData, privateKey); const processResult = await rpcCall('process', { json_block: 'true', subtype: previous === '0'.repeat(64) ? 'open' : 'receive', block: signedBlock }, this.rpcConfig); if (processResult && processResult.hash) { receivedCount++; } } catch (error) { this.logger.logError('RECEIVE_BLOCK_ERROR', { blockHash, error: error instanceof Error ? error.message : 'Unknown error' }); } } return { success: true, hash: `Received ${receivedCount} blocks` }; } catch (error) { this.logger.logError('RECEIVE_PENDING_ERROR', error); return { success: false, error: error instanceof Error ? error.message : 'Unknown error' }; } } async initializeAccount(address: string, privateKey: string): Promise<any> { try { this.logger.log('INITIALIZE_ACCOUNT', { address }); if (!this.keyManager.validateKeyFormat(privateKey)) { throw new Error('Invalid private key format'); } const accountInfo = await rpcCall('account_info', { account: address, representative: true, receivable: true }, this.rpcConfig).catch(() => null); if (accountInfo && !accountInfo.error) { return { status: 'Account already initialized' }; } const publicKey = nanocurrency.derivePublicKey(privateKey); if (!this.keyManager.verifyKeyPair(privateKey, publicKey)) { throw new Error('Key pair verification failed'); } const workData = await rpcCall('work_generate', { hash: publicKey, difficulty: 'fffffff800000000' }, this.rpcConfig); if (!workData || !workData.work) { throw new Error('Failed to generate work'); } const blockData = { walletBalanceRaw: '0', fromAddress: address, toAddress: address, representativeAddress: address, frontier: publicKey, amountRaw: '0', work: workData.work }; const signedBlock = block.receive(blockData, privateKey); const processResult = await rpcCall('process', { json_block: 'true', subtype: 'open', block: signedBlock }, this.rpcConfig); return processResult; } catch (error) { this.logger.logError('INITIALIZE_ACCOUNT_ERROR', error); throw error; } } async createWallet(): Promise<string> { try { const response = await rpcCall('wallet_create', {}, this.rpcConfig); if (response.wallet) { return response.wallet; } throw new Error('Failed to create wallet'); } catch (error: unknown) { const errorMessage = error instanceof Error ? error.message : 'Unknown error'; throw new Error(`Failed to create wallet: ${errorMessage}`); } } async createAccount(): Promise<string> { if (!this.rpcConfig.walletId) { throw new Error('Wallet ID not configured. Call setRPCConfig with walletId first.'); } try { const response = await rpcCall('account_create', { wallet: this.rpcConfig.walletId }, this.rpcConfig); if (response.account) { return response.account; } throw new Error('Failed to create account'); } catch (error: unknown) { const errorMessage = error instanceof Error ? error.message : 'Unknown error'; throw new Error(`Failed to create account: ${errorMessage}`); } } async send(toAddress: string, fromAddress: string, amountNano: string): Promise<string> { if (!this.rpcConfig.walletId) { throw new Error('Wallet ID not configured. Call setRPCConfig with walletId first.'); } try { const amountRaw = this.convertFromNano(amountNano); const response = await rpcCall('send', { wallet: this.rpcConfig.walletId, source: fromAddress, destination: toAddress, amount: amountRaw }, this.rpcConfig); if (response.block) { return response.block; } throw new Error('Failed to send transaction'); } catch (error: unknown) { const errorMessage = error instanceof Error ? error.message : 'Unknown error'; throw new Error(`Failed to send transaction: ${errorMessage}`); } } async receiveAll(address: string): Promise<{ received: number }> { if (!this.rpcConfig.walletId) { throw new Error('Wallet ID not configured. Call setRPCConfig with walletId first.'); } try { const pendingResponse = await rpcCall('pending', { account: address, threshold: '1', source: true, include_active: true, include_only_confirmed: true }, this.rpcConfig); if (!pendingResponse.blocks || Object.keys(pendingResponse.blocks).length === 0) { return { received: 0 }; } let receivedCount = 0; for (const blockHash of Object.keys(pendingResponse.blocks)) { try { const receiveResponse = await rpcCall('receive', { wallet: this.rpcConfig.walletId, account: address, block: blockHash }, this.rpcConfig); if (receiveResponse.block) { receivedCount++; } } catch (error) { console.error('Error receiving block:', blockHash, error); } } return { received: receivedCount }; } catch (error: unknown) { const errorMessage = error instanceof Error ? error.message : 'Unknown error'; throw new Error(`Failed to receive pending transactions: ${errorMessage}`); } } async backupWallet(walletId: string): Promise<any> { try { const response = await rpcCall('wallet_export', { wallet: walletId }, this.rpcConfig); if (response.json) { return response.json; } throw new Error('Failed to export wallet'); } catch (error: unknown) { const errorMessage = error instanceof Error ? error.message : 'Unknown error'; throw new Error(`Failed to backup wallet: ${errorMessage}`); } } // Helper methods private convertToNano(rawAmount: string): string { return convert(rawAmount, { from: Unit.raw, to: Unit.NANO }); } private convertFromNano(nanoAmount: string): string { return convert(nanoAmount, { from: Unit.NANO, to: Unit.raw }); } // Override MCPServer method to handle requests async handleRequest(method: string, params: any[]): Promise<any> { const handler = this.methods.get(method); if (!handler) { throw new Error(`Method ${method} not found`); } return await handler(...params); } }