nano-mcp
Version:
NANO MCP (Nano Cryptocurrency) Server for AI Assistants - A JSON-RPC 2.0 API server for Nano cryptocurrency operations
393 lines (360 loc) • 16.7 kB
JavaScript
// WARNING: This file is locked and should not be modified in the future.
// Any changes to this file may affect the compatibility of nano-mcp.
// Please refer to the documentation for any updates or modifications.
;
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.NanoTransactions = void 0;
const node_fetch_1 = __importDefault(require("node-fetch"));
const nanocurrency_web_1 = require("nanocurrency-web");
const nanocurrency = require('nanocurrency');
const { block } = require('nanocurrency-web');
class NanoTransactions {
/**
* Constructs a NanoTransactions instance with custom and global configuration.
* @param {Object} customConfig - Custom configuration for this instance.
* @param {Object} config - Optional global configuration object.
*/
constructor(customConfig, config) {
const globalConfig = config?.getNanoConfig() || {};
this.apiUrl = customConfig?.apiUrl || globalConfig.rpcUrl || 'https://rpc.nano.to';
this.rpcKey = customConfig?.rpcKey || globalConfig.rpcKey || 'RPC-KEY-BAB822FCCDAE42ECB7A331CCAAAA23';
this.gpuKey = customConfig?.gpuKey || globalConfig.gpuKey;
this.defaultRepresentative = customConfig?.defaultRepresentative || globalConfig.defaultRepresentative || 'nano_3qya5xpjfsbk3ndfebo9dsrj6iy6f6idmogqtn1mtzdtwnxu6rw3dz18i6xf';
this.config = config;
if (config) {
const errors = config.validateConfig();
if (errors.length > 0) {
throw new Error(`Configuration errors: ${errors.join(', ')}`);
}
}
}
/**
* Makes a generic RPC call to the configured Nano node.
* @param {string} action - The RPC action to perform.
* @param {Object} params - Additional parameters for the RPC call.
* @returns {Promise<Object>} - The response from the Nano node.
*/
async rpcCall(action, params = {}) {
console.log('Making RPC call:', { action, ...params });
const response = await (0, node_fetch_1.default)(this.apiUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
action,
...params,
key: this.rpcKey
})
});
if (!response.ok) {
throw new Error(`RPC call failed: ${response.statusText}`);
}
const data = await response.json();
console.log('RPC Response:', data);
return data;
}
/**
* Validates the provided configuration and throws if errors are found.
* @param {Array<string>} errors - List of configuration errors.
* @returns {Promise<Object>} - Validation result object.
*/
async validateConfig(errors) {
if (errors?.length > 0) {
throw new Error(`Configuration errors: ${errors.join(', ')}`);
}
return { isValid: true, errors: [], warnings: [] };
}
/**
* Retrieves account information for a given Nano account.
* @param {string} account - The Nano account address.
* @returns {Promise<Object>} - Account information from the node.
*/
async getAccountInfo(account) {
const info = await this.rpcCall('account_info', { account });
return info;
}
/**
* Retrieves pending (unreceived) blocks for a given Nano account.
* @param {string} account - The Nano account address.
* @returns {Promise<Object>} - Pending blocks information.
*/
async getPendingBlocks(account) {
const pending = await this.rpcCall('pending', {
account,
count: '1',
source: 'true'
});
return pending;
}
/**
* Generates proof-of-work for a given hash, with difficulty based on block type.
* @param {string} hash - The hash to generate work for.
* @param {boolean} isOpen - Whether this is for an open block (lower difficulty).
* @returns {Promise<string>} - The generated work value.
*/
async generateWork(hash, isOpen = false) {
if (!hash) {
throw new Error('Hash is required for work generation');
}
console.log('Generating work for hash:', hash);
const workResult = await this.rpcCall('work_generate', {
hash,
key: this.rpcKey,
difficulty: isOpen ? 'fffffe0000000000' : 'fffffff800000000' // Use lower difficulty for receive blocks
});
if (!workResult.work) {
throw new Error('Failed to generate work');
}
return workResult.work;
}
/**
* Creates and processes a receive (or open) block for a pending transaction.
* Handles both new and existing accounts, calculates the correct new balance, and submits the block.
* @param {string} account - The Nano account address.
* @param {string} privateKey - The private key for signing the block.
* @param {string} pendingBlock - The hash of the pending block to receive.
* @param {string|number|BigInt} pendingAmount - The amount to receive (in raw).
* @param {Object|null} accountInfo - Optional account info; if null, treated as a new account.
* @returns {Promise<Object>} - The result of the process RPC call.
*/
async createReceiveBlock(account, privateKey, pendingBlock, pendingAmount, accountInfo = null) {
try {
// For first receive (opening account), use the account's public key
// For subsequent receives, use the account's frontier
let workHash;
if (accountInfo && !accountInfo.error) {
workHash = accountInfo.frontier;
console.log('Using frontier as work hash:', workHash);
} else {
// For new accounts, we need to use the account's public key
console.log('Account for public key derivation:', account);
workHash = nanocurrency_web_1.tools.addressToPublicKey(account);
console.log('Derived public key from address:', workHash);
}
if (!workHash) {
throw new Error('Failed to determine work hash');
}
console.log('Using work hash:', workHash);
// Generate work with appropriate difficulty
const work = await this.rpcCall('work_generate', {
hash: workHash,
difficulty: !accountInfo || accountInfo.error ? 'fffffe0000000000' : 'fffffff800000000' // Lower difficulty for open blocks
});
if (!work.work) {
throw new Error('Failed to generate work');
}
// Ensure account is in nano_ format
const nanoAccount = account.replace('xrb_', 'nano_');
// Get the representative
let representative = this.defaultRepresentative;
if (accountInfo && !accountInfo.error && accountInfo.representative) {
representative = accountInfo.representative;
}
console.log('Using representative:', representative);
// Calculate new balance
let newBalance;
if (accountInfo && !accountInfo.error) {
// For existing accounts, add pending amount to current balance
newBalance = (BigInt(accountInfo.balance) + BigInt(pendingAmount)).toString();
} else {
// For new accounts, balance is just the pending amount
newBalance = pendingAmount;
}
console.log('New balance:', newBalance);
// Prepare block data for block.receive
const receiveBlockData = {
walletBalanceRaw: accountInfo && !accountInfo.error ? accountInfo.balance : '0',
toAddress: nanoAccount,
representativeAddress: representative,
frontier: accountInfo && !accountInfo.error ? accountInfo.frontier : '0000000000000000000000000000000000000000000000000000000000000000',
transactionHash: pendingBlock,
amountRaw: pendingAmount,
work: work.work
};
console.log('Block data for nanocurrency-web:', receiveBlockData);
// Sign the block using nanocurrency-web's block.receive
const signedBlock = block.receive(receiveBlockData, privateKey);
console.log('Signed block:', signedBlock);
// Process the block using the signed block
const processResult = await this.rpcCall('process', {
json_block: 'true',
subtype: accountInfo && !accountInfo.error ? 'receive' : 'open',
block: signedBlock
});
if (processResult.error) {
throw new Error(`Failed to process block: ${processResult.error}`);
}
return processResult;
} catch (error) {
console.error('Error in createReceiveBlock:', error);
throw error;
}
}
/**
* Receives all pending blocks for a given account, processing each one sequentially.
* @param {string} address - The Nano account address.
* @param {string} privateKey - The private key for signing receive blocks.
* @returns {Promise<Array<Object>>} - Results of processing each pending block.
*/
async receiveAllPending(address, privateKey) {
// Get account info
let accountInfo;
try {
accountInfo = await this.getAccountInfo(address);
} catch (error) {
// Account not opened yet, which is fine
accountInfo = null;
}
// Get pending blocks
const pending = await this.getPendingBlocks(address);
const results = [];
if (pending.blocks && Object.keys(pending.blocks).length > 0) {
for (const [hash, blockInfo] of Object.entries(pending.blocks)) {
try {
// Convert raw amount to nano for display
const rawToNanoResult = await this.rpcCall('raw_to_nano', {
amount: blockInfo.amount
});
console.log(`Receiving ${rawToNanoResult.nano} NANO from block ${hash}`);
const result = await this.createReceiveBlock(
address,
privateKey,
hash,
blockInfo.amount,
accountInfo
);
results.push(result);
// Update accountInfo for next iteration if the block was processed
if (result.hash) {
try {
accountInfo = await this.getAccountInfo(address);
} catch (error) {
console.error('Failed to update account info:', error);
}
}
} catch (error) {
console.error('Failed to process pending block:', error);
results.push({ error: error.message });
}
}
}
return results;
}
/**
* Generates a new Nano wallet (seed, account, private/public key).
* @returns {Promise<Object>} - The generated wallet information.
*/
async generateWallet() {
const walletData = nanocurrency_web_1.wallet.generateLegacy();
return {
address: walletData.accounts[0].address,
privateKey: walletData.accounts[0].privateKey,
publicKey: walletData.accounts[0].publicKey,
seed: walletData.seed
};
}
/**
* Makes a generic RPC request to the Nano node.
* @param {string} method - The RPC method/action.
* @param {Object} params - Parameters for the RPC call.
* @returns {Promise<Object>} - The response from the Nano node.
*/
async makeRequest(method, params) {
return this.rpcCall(method, params);
}
/**
* Sends a transaction from one Nano account to another.
* Handles work generation, block signing, and submission.
* @param {string} fromAddress - The sender's Nano account address.
* @param {string} privateKey - The sender's private key.
* @param {string} toAddress - The recipient's Nano account address.
* @param {string|number|BigInt} amountRaw - The amount to send (in raw).
* @returns {Promise<Object>} - Success status and block hash or error message.
*/
async sendTransaction(fromAddress, privateKey, toAddress, amountRaw) {
try {
const formattedFromAddress = fromAddress.replace('xrb_', 'nano_');
const formattedToAddress = toAddress.replace('xrb_', 'nano_');
// Ensure amountRaw is a string
const amountRawString = amountRaw.toString();
// Get account info for current balance and frontier
const accountInfo = await this.makeRequest('account_info', {
account: formattedFromAddress,
representative: true,
json_block: 'true'
});
if (accountInfo.error) {
if (accountInfo.error === 'Account not found') {
throw new Error('Account has no previous blocks. Please make sure it has received some NANO first.');
}
throw new Error(accountInfo.error);
}
console.log('Account info:', accountInfo);
// Calculate new balance after sending
const currentBalance = BigInt(accountInfo.balance);
const sendAmount = BigInt(amountRawString);
const newBalance = (currentBalance - sendAmount).toString();
console.log('Current balance:', currentBalance.toString());
console.log('Send amount:', sendAmount.toString());
console.log('New balance after send:', newBalance);
// Generate work
const workData = await this.makeRequest('work_generate', {
hash: accountInfo.frontier,
difficulty: 'fffffff800000000' // Higher difficulty for send blocks
});
if (!workData || !workData.work) {
throw new Error('Failed to generate work');
}
// Get the representative
let representative = this.defaultRepresentative;
if (accountInfo && accountInfo.representative) {
representative = accountInfo.representative;
}
console.log('Using representative:', representative);
// Prepare block data
const blockData = {
walletBalanceRaw: newBalance,
fromAddress: formattedFromAddress,
toAddress: formattedToAddress,
representativeAddress: representative,
frontier: accountInfo.frontier,
amountRaw: amountRawString,
work: workData.work
};
console.log('Block data for send:', blockData);
// Sign the block using nanocurrency-web
const signedBlock = nanocurrency_web_1.block.send(blockData, privateKey);
console.log('Signed block:', signedBlock);
// Process the block
const processResult = await this.makeRequest('process', {
json_block: 'true',
subtype: 'send',
block: signedBlock
});
if (processResult.error) {
throw new Error(processResult.error);
}
return { success: true, hash: processResult.hash };
} catch (error) {
console.error('Send Transaction Error:', error);
return { success: false, error: error.message };
}
}
/**
* Retrieves the balance for a given Nano account.
* @param {string} account - The Nano account address.
* @returns {Promise<Object>} - Balance information from the node.
*/
async getBalance(account) {
const balance = await this.rpcCall('account_balance', { account });
return {
balance: balance.balance,
pending: balance.pending
};
}
}
exports.NanoTransactions = NanoTransactions;