UNPKG

@chainreactionom/nano-mcp

Version:

NANO cryptocurrency wallet implementation for MCP with comprehensive testing

446 lines (373 loc) 14.2 kB
import { Logger } from './logger.js'; import path from 'path'; import { fileURLToPath } from 'url'; import dotenv from 'dotenv'; import { wallet, block, tools } from "nanocurrency-web"; // Load environment variables dotenv.config(); const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); // API settings const WORK_API_KEY = "RPC-KEY-BAB822FCCDAE42ECB7A331CCAAAA23"; const RPC_API_KEY = "PUBLIC-KEY-FA9CE81226BF478291D34836A09D8B06"; const MIN_DELAY_BETWEEN_REQUESTS = 1000; // 1 second minimum delay let lastRequestTime = 0; async function sleep(ms: number) { return new Promise(resolve => setTimeout(resolve, ms)); } async function rpc(request: any, useWorkKey = false) { // Implement rate limiting const now = Date.now(); const timeSinceLastRequest = now - lastRequestTime; if (timeSinceLastRequest < MIN_DELAY_BETWEEN_REQUESTS) { await sleep(MIN_DELAY_BETWEEN_REQUESTS - timeSinceLastRequest); } lastRequestTime = Date.now(); // Determine which API key to use based on the action const shouldUseWorkKey = request.action === 'work_generate'; // Add API key to request const requestWithKey = { ...request, key: shouldUseWorkKey ? WORK_API_KEY : RPC_API_KEY }; console.log('Request:', { ...requestWithKey, key: shouldUseWorkKey ? 'WORK_API_KEY' : 'RPC_API_KEY' // Hide actual key in logs }); const response = await fetch(`https://rpc.nano.to`, { method: 'POST', headers: { 'Content-Type': 'application/json', 'Accept': 'application/json' }, body: JSON.stringify(requestWithKey) }); let data; try { data = await response.json(); } catch { if (response.status === 429) { console.log("Rate limited, waiting 5 seconds..."); await sleep(5000); return rpc(request, useWorkKey); // Retry the request } throw Error(`RPC status ${response.status}: failed to parse JSON`); } // Handle rate limiting responses if (response.status === 429) { console.log("Rate limited by status code, waiting 5 seconds..."); await sleep(5000); return rpc(request, useWorkKey); // Retry the request } // Handle error responses if ('error' in data) { const errorMessage = typeof data.error === 'string' ? data.error : JSON.stringify(data.error); // Check for rate limiting errors if (errorMessage.toLowerCase().includes('rate') || errorMessage.toLowerCase().includes('too many requests')) { console.log("Rate limited by error response, waiting 5 seconds..."); await sleep(5000); return rpc(request, useWorkKey); // Retry the request } console.error(`RPC error: ${errorMessage}`); throw Error(errorMessage); } if (!response.ok) { throw Error(`RPC status ${response.status}: ${JSON.stringify(data, null, 4)}`); } console.log(data); return data; } async function generateWork(hash: string, isOpen = false): Promise<string> { try { const data = await rpc({ action: "work_generate", hash: hash, difficulty: isOpen ? 'fffffff800000000' : 'fffffff800000000' }); // No need to pass useWorkKey flag anymore since we check action type if (!data || !data.work) { throw Error("No work returned from RPC"); } return data.work; } catch (error: any) { throw Error(`Failed to generate work: ${error?.message || 'Unknown error'}`); } } async function balance(account: string) { let retries = 3; while (retries > 0) { try { const data = await rpc({ action: "account_balance", account: account, include_only_confirmed: true }); if (!data || !data.balance || !data.receivable) { throw Error("Failed to get balance."); } return { balance: tools.convert(data.balance, 'RAW', 'NANO'), receivable: tools.convert(data.receivable, 'RAW', 'NANO'), } } catch (error) { console.error(`Balance check failed (${retries} retries left):`, error); retries--; if (retries === 0) throw error; await sleep(2000); // Wait 2 seconds before retrying } } throw Error("Failed to get balance after all retries"); } async function createAccount(): Promise<string> { // Generate a new wallet const walletData = wallet.generate(); return walletData.accounts[0].address; } async function send(destination: string, source: string, amount: string): Promise<string> { // Try to get account info, but don't throw if account doesn't exist let accountInfo; try { accountInfo = await rpc({ action: "account_info", account: source, representative: true, receivable: true, include_confirmed: true }); } catch (error) { throw Error("Source account must exist and have a balance to send"); } const previous = accountInfo?.frontier; const balance = accountInfo?.balance; const representative = accountInfo?.representative || destination; if (!previous || !balance) { throw Error("Source account must exist and have a balance to send"); } // Convert NANO to raw const amountRaw = tools.convert(amount, 'NANO', 'RAW'); const newBalance = (BigInt(balance) - BigInt(amountRaw)).toString(); if (BigInt(newBalance) < BigInt(0)) { throw Error("Insufficient balance"); } // Generate work for the block const work = await generateWork(previous); // Create the block data const blockData = { walletBalanceRaw: balance, fromAddress: source, toAddress: destination, representativeAddress: representative, frontier: previous, amountRaw: amountRaw, work: work }; // Sign the block const signedBlock = block.send(blockData, process.env.PRIVATE_KEY!); // Process the block const data = await rpc({ action: "process", json_block: "true", subtype: "send", block: signedBlock }); if (!data || !data.hash) { throw Error("Failed to send nano."); } console.log("sent " + amount + " nano to " + destination); return data.hash; } async function receive(account: string, blockHash: string) : Promise<string> { // Get pending block info first const blockInfo = await rpc({ action: "blocks_info", hashes: [blockHash], json_block: "true", include_not_found: "true" }); if (!blockInfo || !blockInfo.blocks || !blockInfo.blocks[blockHash]) { throw Error("Failed to get block info"); } // Try to get account info, but don't throw if account doesn't exist let accountInfo; try { accountInfo = await rpc({ action: "account_info", account: account, representative: true, receivable: true, include_confirmed: true }); } catch (error) { // Account doesn't exist yet, this is fine console.log("Account doesn't exist yet, will be opened with this receive"); } const previous = accountInfo?.frontier || "0".repeat(64); const representative = accountInfo?.representative || account; const currentBalance = accountInfo?.balance || "0"; const amountRaw = blockInfo.blocks[blockHash].amount; const newBalance = (BigInt(currentBalance) + BigInt(amountRaw)).toString(); // Generate work for the block const work = await generateWork( previous === "0".repeat(64) ? tools.addressToPublicKey(account) : previous, previous === "0".repeat(64) ); // Create the block data const blockData = { walletBalanceRaw: currentBalance, toAddress: account, representativeAddress: representative, frontier: previous, transactionHash: blockHash, amountRaw: amountRaw, work: work }; // Sign the block const signedBlock = block.receive(blockData, process.env.PRIVATE_KEY!); // Process the block const data = await rpc({ action: "process", json_block: "true", subtype: previous === "0".repeat(64) ? "open" : "receive", block: signedBlock }); if (!data || !data.hash) { throw Error("Failed to receive nano") } return data.hash; } async function receive_all(account: string) { const data = await rpc({ action: "receivable", account: account, count: "10", source: "true", include_active: "true", include_only_confirmed: "true", sorting: "amount", threshold: "1" }); if (!data || !data.blocks) { return { received: 0 }; } const blocks = Object.keys(data.blocks); let received = 0; for (const blockHash of blocks) { try { await receive(account, blockHash); received++; } catch (error) { console.error("Failed to receive block:", error); } } return { received }; } async function testWalletTransfer() { // Initialize logger const logger = new Logger(path.join(__dirname, 'logs')); try { logger.log('TEST_START', 'Starting Wallet Transfer Test'); // Step 1: Create first wallet logger.log('WALLET1_CREATE', 'Creating first wallet'); const wallet1Address = await createAccount(); logger.log('WALLET1_CREATED', { address: wallet1Address }); console.log('\n=== WALLET 1 ADDRESS ==='); console.log('Please send exactly 0.00001 NANO to this address:'); console.log(wallet1Address); console.log('Waiting for funds...'); // Step 2: Wait and check for incoming transaction logger.log('WAITING_FOR_FUNDS', 'Waiting for incoming transaction'); let funded = false; let startTime = Date.now(); const timeoutMs = 300000; // 5 minutes timeout let lastBalance = { balance: '0', receivable: '0' }; while (!funded && (Date.now() - startTime) < timeoutMs) { try { const balanceInfo = await balance(wallet1Address); // Only log if balance or receivable has changed if (balanceInfo.balance !== lastBalance.balance || balanceInfo.receivable !== lastBalance.receivable) { logger.log('BALANCE_UPDATE', { account: wallet1Address, balance: balanceInfo.balance, receivable: balanceInfo.receivable }); lastBalance = balanceInfo; } if (parseFloat(balanceInfo.balance) > 0 || parseFloat(balanceInfo.receivable) > 0) { logger.log('FUNDS_RECEIVED', { account: wallet1Address, balance: balanceInfo.balance, receivable: balanceInfo.receivable }); // Receive any pending funds await receive_all(wallet1Address); funded = true; } else { await sleep(5000); // Check every 5 seconds } } catch (error) { logger.logError('CHECK_BALANCE_ERROR', error); await sleep(5000); } } if (!funded) { throw new Error('Timeout waiting for funds'); } // Step 3: Create second wallet logger.log('WALLET2_CREATE', 'Creating second wallet'); const wallet2Address = await createAccount(); logger.log('WALLET2_CREATED', { address: wallet2Address }); // Step 4: Send to wallet2 logger.log('SENDING_TO_WALLET2', 'Sending funds from wallet 1 to wallet 2'); const sendAmount = '0.00001'; const sendHash = await send(wallet2Address, wallet1Address, sendAmount); logger.log('SENT_TO_WALLET2', { hash: sendHash, from: wallet1Address, to: wallet2Address, amount: sendAmount }); // Step 5: Receive in wallet2 logger.log('RECEIVING_IN_WALLET2', 'Receiving funds in wallet 2'); await sleep(5000); // Wait for transaction to propagate await receive_all(wallet2Address); // Step 6: Send back to wallet1 logger.log('SENDING_BACK_TO_WALLET1', 'Sending funds back to wallet 1'); const sendBackHash = await send(wallet1Address, wallet2Address, sendAmount); logger.log('SENT_TO_WALLET1', { hash: sendBackHash, from: wallet2Address, to: wallet1Address, amount: sendAmount }); // Step 7: Receive back in wallet1 logger.log('RECEIVING_BACK_IN_WALLET1', 'Receiving funds back in wallet 1'); await sleep(5000); // Wait for transaction to propagate await receive_all(wallet1Address); // Test Summary const testResults = { total: 7, passed: 7, failed: 0, duration: Date.now() - startTime, wallet1: { address: wallet1Address }, wallet2: { address: wallet2Address } }; logger.summarize(testResults); logger.log('TEST_COMPLETE', 'Wallet Transfer Test completed successfully'); } catch (error) { logger.logError('TEST_FAILURE', error); throw error; } } // Run the test testWalletTransfer().catch(error => { console.error('Test failed:', error); process.exit(1); });