@chainreactionom/nano-mcp
Version:
NANO cryptocurrency wallet implementation for MCP with comprehensive testing
237 lines (199 loc) • 8.75 kB
JavaScript
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;