UNPKG

@chainreactionom/nano-mcp

Version:

NANO cryptocurrency wallet implementation for MCP with comprehensive testing

237 lines (199 loc) 8.75 kB
import dotenv from 'dotenv'; import fetch from 'node-fetch'; import crypto from 'crypto'; import * as nanocurrency from 'nanocurrency'; import * as nanoWeb from 'nanocurrency-web'; import qrcode from 'qrcode-terminal'; import { Logger } from './logger.js'; import path from 'path'; import { fileURLToPath } from 'url'; import { KeyManager } from './key-manager.js'; import { block } from 'nanocurrency-web'; import axios from 'axios'; dotenv.config(); const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); export class WalletService { constructor() { this.apiUrl = 'https://rpc.nano.to'; this.apiKey = 'RPC-KEY-BAB822FCCDAE42ECB7A331CCAAAA23'; this.logger = new Logger(path.join(__dirname, 'logs')); this.keyManager = new KeyManager(this.logger); } async makeRpcCall(action, params = {}) { try { this.logger.log('RPC_CALL', { action, params }); const response = await fetch(this.apiUrl, { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${this.apiKey}` }, body: JSON.stringify({ action, ...params }) }); const text = await response.text(); this.logger.log('RPC_RESPONSE_RAW', text); if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}, body: ${text}`); } let data; try { data = text ? JSON.parse(text) : {}; } catch (parseError) { this.logger.logError('RPC_PARSE_ERROR', { error: parseError.message, responseText: text }); throw new Error(`Failed to parse response: ${text}`); } this.logger.log('RPC_RESPONSE', data); return data; } catch (error) { this.logger.logError('RPC_ERROR', { message: error.message, stack: error.stack }); throw error; } } async generateWallet() { try { const wallet = await this.keyManager.generateKeyPair(); console.log('\nGenerated new wallet:'); console.log('Public Key:', wallet.publicKey); console.log('Address:', wallet.address); return wallet; } catch (error) { this.logger.logError('WALLET_GENERATION_ERROR', error); throw error; } } async getBalance(address) { try { const formattedAddress = address.replace('xrb_', 'nano_'); console.log('\nChecking balance for address:', formattedAddress); const data = await this.makeRpcCall('account_balance', { account: formattedAddress, include_only_confirmed: true }); return { balance: data.balance || '0', pending: data.pending || data.receivable || '0' }; } catch (error) { if (error.message.includes('Account not found')) { console.log('Account not opened yet, returning zero balance'); return { balance: '0', pending: '0' }; } throw error; } } async receivePending(address, privateKey) { try { const formattedAddress = address.replace('xrb_', 'nano_'); console.log('\nChecking pending blocks for:', formattedAddress); const pendingData = await this.makeRpcCall('pending', { account: formattedAddress, threshold: '1', source: true, include_active: true, include_only_confirmed: true }); if (!pendingData.blocks || Object.keys(pendingData.blocks).length === 0) { console.log('No pending blocks found'); return { received: 0 }; } let receivedCount = 0; for (const blockHash of Object.keys(pendingData.blocks)) { try { const accountInfo = await this.makeRpcCall('account_info', { account: formattedAddress, representative: true }).catch(() => null); const previous = accountInfo?.frontier || '0'.repeat(64); const representative = accountInfo?.representative || formattedAddress; const currentBalance = accountInfo?.balance || '0'; const amountRaw = pendingData.blocks[blockHash].amount; const workHash = previous === '0'.repeat(64) ? await nanoWeb.wallet.derivePublicKey(privateKey) : previous; const workData = await this.makeRpcCall('work_generate', { hash: workHash }); if (!workData || !workData.work) { throw new Error('Failed to generate work'); } const blockData = { walletBalanceRaw: currentBalance, fromAddress: pendingData.blocks[blockHash].source, toAddress: formattedAddress, representativeAddress: representative, frontier: previous, amountRaw: amountRaw, work: workData.work }; const signedBlock = await nanoWeb.block.receive(blockData, privateKey); const processResult = await this.makeRpcCall('process', { json_block: 'true', subtype: previous === '0'.repeat(64) ? 'open' : 'receive', block: signedBlock }); if (processResult && processResult.hash) { console.log('Successfully received block:', blockHash); receivedCount++; } } catch (error) { console.error('Error receiving block:', blockHash, error); } } return { received: receivedCount }; } catch (error) { console.error('Receive Error:', error); throw error; } } async sendTransaction(fromAddress, privateKey, toAddress, amountRaw) { try { const formattedFromAddress = fromAddress.replace('xrb_', 'nano_'); const formattedToAddress = toAddress.replace('xrb_', 'nano_'); const accountInfo = await this.makeRpcCall('account_info', { account: formattedFromAddress, representative: true }); if (!accountInfo || accountInfo.error) { throw new Error('Account not found or not initialized'); } const workData = await this.makeRpcCall('work_generate', { hash: accountInfo.frontier }); 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 = await nanoWeb.block.send(blockData, privateKey); const processResult = await this.makeRpcCall('process', { json_block: 'true', subtype: 'send', block: signedBlock }); return { success: true, hash: processResult.hash }; } catch (error) { console.error('Send Transaction Error:', error); return { success: false, error: error.message }; } } } // Create an instance and export const walletService = new WalletService(); export default walletService;