nano-mcp
Version:
NANO MCP (Nano Cryptocurrency) Server for AI Assistants - A JSON-RPC 2.0 API server for Nano cryptocurrency operations with QR code generation, local work generation and auto-receive pending blocks
915 lines (812 loc) • 43.8 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.
// ⚠️⚠️⚠️ CRITICAL: RPC NODE CONFIGURATION ⚠️⚠️⚠️
// ⚠️ DO NOT CHANGE THE RPC NODE: https://uk1.public.xnopay.com/proxy
// ⚠️ DO NOT add fallback nodes or alternative RPC endpoints
// ⚠️ This node has been specifically configured by the user
// ⚠️ Work generation is done LOCALLY - not on the RPC node
// ⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️
;
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');
const { EnhancedErrorHandler } = require('./error-handler');
const { BalanceConverter } = require('./balance-converter');
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.rpcNodes = customConfig?.rpcNodes || [customConfig?.apiUrl] || [globalConfig.rpcUrl] || ['https://rpc.nano.to'];
this.currentNodeIndex = 0;
// Allow null rpcKey to be explicitly set
this.rpcKey = customConfig?.hasOwnProperty('rpcKey') ? customConfig.rpcKey : (globalConfig.rpcKey || null);
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(', ')}`);
}
}
this.failoverAttempts = 0;
this.maxFailoverAttempts = this.rpcNodes.length * 2; // Try each node twice before giving up
// Track recently attempted blocks to prevent duplicates
this.recentBlockAttempts = new Map(); // key: blockHash, value: { timestamp, attempts }
this.duplicateBlockTTL = 60000; // 60 seconds
console.log('[NanoTransactions] Initialized without work cache to prevent cache-related issues');
console.log('[NanoTransactions] Duplicate block detection enabled (60s TTL)');
}
async getCurrentRpcNode() {
return this.rpcNodes[this.currentNodeIndex];
}
switchToNextNode() {
this.currentNodeIndex = (this.currentNodeIndex + 1) % this.rpcNodes.length;
console.log(`Switching to RPC node: ${this.rpcNodes[this.currentNodeIndex]}`);
}
/**
* 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 = {}) {
let lastError = null;
this.failoverAttempts = 0;
while (this.failoverAttempts < this.maxFailoverAttempts) {
const currentNode = await this.getCurrentRpcNode();
console.log(`Making RPC call to ${currentNode}:`, { action, ...params });
try {
const requestBody = {
action,
...params
};
// Only include key if it's not null
if (this.rpcKey) {
requestBody.key = this.rpcKey;
}
const response = await node_fetch_1.default(currentNode, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(requestBody)
});
// Check for rate limiting response
if (response.status === 429) {
console.log('Rate limit hit, switching nodes...');
this.switchToNextNode();
this.failoverAttempts++;
continue;
}
if (!response.ok) {
throw new Error(`RPC call failed: ${response.statusText}`);
}
const data = await response.json();
console.log('RPC Response:', data);
// Reset failover attempts on successful call
this.failoverAttempts = 0;
return data;
} catch (error) {
console.error(`Error with RPC node ${currentNode}:`, error.message);
lastError = error;
this.switchToNextNode();
this.failoverAttempts++;
}
}
// If we've tried all nodes and still failed, throw the last error
throw new Error(`All RPC nodes failed. Last error: ${lastError?.message}`);
}
/**
* 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 using RPC node with timeout protection.
* Uses RPC node's work_generate action for fast, reliable work generation.
* @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 using RPC node for hash:', hash);
try {
// NANO network difficulty thresholds
// Receive/Open blocks: lower difficulty (fffffe0000000000)
// Send/Change blocks: higher difficulty (fffffff800000000)
const difficulty = isOpen ? 'fffffe0000000000' : 'fffffff800000000';
console.log(`Requesting work generation from RPC with ${isOpen ? 'RECEIVE/OPEN' : 'SEND/CHANGE'} difficulty: ${difficulty}`);
// Set timeout based on block type (10s for send, 5s for receive)
const timeout = isOpen ? 5000 : 10000;
// Use RPC node's work_generate with timeout protection
const work = await this._generateWorkWithTimeout(hash, difficulty, timeout);
if (!work) {
throw new Error('Work generation returned null - RPC node failed to generate work');
}
console.log('Work generated successfully via RPC:', work);
return work;
} catch (error) {
console.error('RPC work generation failed:', error);
throw new Error(`Failed to generate work via RPC: ${error.message}`);
}
}
/**
* Internal method: Generate work via RPC with timeout protection
* @private
*/
async _generateWorkWithTimeout(hash, difficulty, timeoutMs) {
return new Promise(async (resolve, reject) => {
const timer = setTimeout(() => {
reject(new Error(`Work generation timed out after ${timeoutMs}ms. RPC node may be slow or unavailable. Please retry.`));
}, timeoutMs);
try {
const workResult = await this.rpcCall('work_generate', {
hash,
difficulty
});
clearTimeout(timer);
if (workResult.error) {
reject(new Error(`RPC work_generate error: ${workResult.error}`));
return;
}
if (!workResult.work) {
reject(new Error('RPC returned no work value'));
return;
}
resolve(workResult.work);
} catch (error) {
clearTimeout(timer);
reject(error);
}
});
}
/**
* Generates proof-of-work using RPC node with retry logic and exponential backoff.
* This is useful for production environments where RPC work generation may occasionally fail.
* Much faster than local work generation (typically <1 second vs 10-15 seconds).
* @param {string} hash - The hash to generate work for.
* @param {boolean} isOpen - Whether this is for an open block (lower difficulty).
* @param {number} maxRetries - Maximum number of retry attempts (default: 3).
* @returns {Promise<string>} - The generated work value.
*/
async generateWorkWithRetry(hash, isOpen = false, maxRetries = 3) {
console.log(`Generating work via RPC with retry logic (max ${maxRetries} attempts)...`);
let lastError;
for (let attempt = 1; attempt <= maxRetries; attempt++) {
try {
console.log(`RPC work generation attempt ${attempt}/${maxRetries}...`);
const work = await this.generateWork(hash, isOpen);
console.log(`RPC work generation succeeded on attempt ${attempt}`);
return work;
} catch (error) {
lastError = error;
console.error(`RPC work generation attempt ${attempt} failed:`, error.message);
if (attempt < maxRetries) {
// Exponential backoff: 1s, 2s, 4s, etc.
const backoffMs = Math.pow(2, attempt - 1) * 1000;
console.log(`Retrying RPC work generation in ${backoffMs}ms...`);
await new Promise(resolve => setTimeout(resolve, backoffMs));
} else {
console.error(`All ${maxRetries} RPC work generation attempts failed`);
}
}
}
throw new Error(`RPC work generation failed after ${maxRetries} retry attempts. Last error: ${lastError.message}`);
}
/**
* 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 via RPC with retry logic for production reliability
console.log('Generating work via RPC with retry protection...');
const workValue = await this.generateWorkWithRetry(workHash, !accountInfo || accountInfo.error, 2);
const work = { work: workValue };
// 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 for verification
let newBalance;
let currentBalance;
if (accountInfo && !accountInfo.error) {
// For existing accounts, add pending amount to current balance
currentBalance = accountInfo.balance;
newBalance = (BigInt(currentBalance) + BigInt(pendingAmount)).toString();
} else {
// For new accounts, balance starts at 0
currentBalance = '0';
newBalance = pendingAmount;
}
console.log('Current balance:', currentBalance);
console.log('Pending amount:', pendingAmount);
console.log('Expected new balance:', newBalance);
// Prepare block data for block.receive
// IMPORTANT: nanocurrency-web's block.receive() calculates newBalance internally by doing:
// newBalance = walletBalanceRaw + amountRaw
// So walletBalanceRaw should be the CURRENT balance (before receive), NOT the new balance
const receiveBlockData = {
walletBalanceRaw: currentBalance, // Current balance BEFORE receive
toAddress: nanoAccount,
representativeAddress: representative,
frontier: accountInfo && !accountInfo.error ? accountInfo.frontier : '0000000000000000000000000000000000000000000000000000000000000000',
transactionHash: pendingBlock,
amountRaw: pendingAmount, // Amount to receive (will be added by library)
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) {
// Handle blockchain errors with enhanced messaging
const errorResult = EnhancedErrorHandler.blockchainError(
processResult.error,
'receive block',
{
account: account,
pendingBlock: pendingBlock,
pendingAmount: pendingAmount,
hash: workHash,
work: workValue,
blockType: accountInfo && !accountInfo.error ? 'receive' : 'open'
}
);
// For createReceiveBlock, throw the enhanced error so it can be caught by caller
const error = new Error(errorResult.error);
error.enhancedError = errorResult;
throw error;
}
return processResult;
} catch (error) {
console.error('Error in createReceiveBlock:', error);
// If it's an enhanced error, return it directly
if (error.enhancedError) {
throw error.enhancedError;
}
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 {
// Log the raw amount (avoid RPC call for conversion)
console.log(`Receiving ${blockInfo.amount} raw 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);
// If it's an enhanced error object, include it directly
if (error.errorCode) {
results.push(error);
} else {
results.push({ error: error.message });
}
}
}
}
return results;
}
/**
* Initializes a NANO account by receiving the first pending block.
* This is used to open/activate a new account that has received funds.
* @param {string} address - The Nano account address to initialize.
* @param {string} privateKey - The private key for signing the block.
* @returns {Promise<Object>} - Initialization result with status and details.
*/
async initializeAccount(address, privateKey) {
try {
// Check if account is already initialized
let accountInfo;
try {
accountInfo = await this.getAccountInfo(address);
if (accountInfo && !accountInfo.error) {
return {
initialized: true,
alreadyInitialized: true,
message: 'Account is already initialized',
representative: accountInfo.representative,
balance: accountInfo.balance,
frontier: accountInfo.frontier
};
}
} catch (error) {
// Account doesn't exist yet, which is expected
console.log('Account not yet initialized, checking for pending blocks...');
}
// Get pending blocks to initialize the account
const pending = await this.getPendingBlocks(address);
if (!pending.blocks || Object.keys(pending.blocks).length === 0) {
return {
initialized: false,
message: 'No pending blocks to initialize the account. Send some NANO to this address first.',
address: address
};
}
// Process the first pending block to initialize the account
const firstBlockHash = Object.keys(pending.blocks)[0];
const firstBlockInfo = pending.blocks[firstBlockHash];
console.log(`Initializing account with pending block ${firstBlockHash}...`);
const result = await this.createReceiveBlock(
address,
privateKey,
firstBlockHash,
firstBlockInfo.amount,
null // null accountInfo indicates this is a new account
);
if (result.hash) {
// Get updated account info
const updatedAccountInfo = await this.getAccountInfo(address);
return {
initialized: true,
alreadyInitialized: false,
message: 'Account successfully initialized',
blockHash: result.hash,
representative: updatedAccountInfo.representative || this.defaultRepresentative,
balance: updatedAccountInfo.balance,
frontier: updatedAccountInfo.frontier
};
} else {
throw new Error('Failed to process initialization block');
}
} catch (error) {
console.error('Error initializing account:', error);
throw new Error(`Failed to initialize account: ${error.message}`);
}
}
/**
* 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) {
// Define these outside try block so they're available in catch and throughout function
let formattedFromAddress;
let formattedToAddress;
let privateKeyString;
let amountRawString;
try {
// Ensure all parameters are properly typed
formattedFromAddress = String(fromAddress).replace('xrb_', 'nano_');
formattedToAddress = String(toAddress).replace('xrb_', 'nano_');
privateKeyString = String(privateKey);
amountRawString = String(amountRaw);
// Log the requested amount IMMEDIATELY to track any doubling
console.log('[AmountTrack] ========== SEND TRANSACTION STARTED ==========');
console.log('[AmountTrack] Amount REQUESTED by user (raw):', amountRawString);
try {
const requestedNano = BalanceConverter.rawToNano(amountRawString);
console.log('[AmountTrack] Amount REQUESTED by user (NANO):', requestedNano);
} catch (e) {
console.log('[AmountTrack] Could not convert to NANO (invalid raw value?)');
}
// IMPORTANT: Check for pending blocks and receive them first
console.log('Checking for pending blocks before sending...');
const pending = await this.getPendingBlocks(formattedFromAddress);
let lastFrontier = null;
if (pending.blocks && Object.keys(pending.blocks).length > 0) {
console.log(`Found ${Object.keys(pending.blocks).length} pending block(s). Receiving them first...`);
// Get frontier BEFORE receiving
const preReceiveInfo = await this.makeRequest('account_info', {
account: formattedFromAddress
});
lastFrontier = preReceiveInfo.frontier;
console.log('[FrontierTrack] Frontier before receive:', lastFrontier);
const receiveResults = await this.receiveAllPending(formattedFromAddress, privateKeyString);
console.log('Received all pending blocks:', receiveResults);
// Get the new frontier from the last successful receive
const lastSuccessfulReceive = receiveResults.filter(r => r.hash).pop();
if (lastSuccessfulReceive && lastSuccessfulReceive.hash) {
lastFrontier = lastSuccessfulReceive.hash;
console.log('[FrontierTrack] New frontier from receive:', lastFrontier);
}
// Wait for RPC node to update with retry mechanism
console.log('[FrontierTrack] Waiting for RPC node to confirm new frontier...');
let retries = 0;
const maxRetries = 5;
while (retries < maxRetries) {
await new Promise(resolve => setTimeout(resolve, 2000)); // Wait 2 seconds
const checkInfo = await this.makeRequest('account_info', {
account: formattedFromAddress
});
console.log(`[FrontierTrack] Retry ${retries + 1}/${maxRetries} - Current frontier:`, checkInfo.frontier);
if (checkInfo.frontier === lastFrontier) {
console.log('[FrontierTrack] ✅ Frontier confirmed! RPC node is up to date.');
break;
}
retries++;
if (retries === maxRetries) {
console.warn('[FrontierTrack] ⚠️ Max retries reached. Proceeding with last known frontier.');
}
}
} else {
console.log('No pending blocks to receive.');
}
// Get account info for current balance and frontier
const accountInfo = await this.makeRequest('account_info', {
account: formattedFromAddress,
representative: true,
json_block: 'true'
});
console.log('[FrontierTrack] Final frontier for send transaction:', accountInfo.frontier);
if (accountInfo.error) {
if (accountInfo.error === 'Account not found') {
// Check if there are pending blocks to initialize with
const pendingCheck = await this.getPendingBlocks(formattedFromAddress);
return EnhancedErrorHandler.accountNotInitialized(formattedFromAddress, pendingCheck);
}
throw new Error(accountInfo.error);
}
console.log('Account info:', accountInfo);
// Calculate new balance after sending
const currentBalance = BigInt(accountInfo.balance);
const sendAmount = BigInt(amountRawString);
console.log('[AmountTrack] Current balance (raw):', currentBalance.toString());
console.log('[AmountTrack] Amount to SEND (raw):', sendAmount.toString());
console.log('[AmountTrack] Verifying: sendAmount === requested?', sendAmount.toString() === amountRawString);
// Check if sufficient balance BEFORE attempting transaction
if (currentBalance < sendAmount) {
console.log('Insufficient balance detected');
return EnhancedErrorHandler.insufficientBalance(
accountInfo.balance,
amountRawString,
formattedFromAddress
);
}
const newBalance = (currentBalance - sendAmount).toString();
console.log('Current balance:', currentBalance.toString());
console.log('Send amount:', sendAmount.toString());
console.log('New balance after send:', newBalance);
console.log('[AmountTrack] Remaining balance after send (raw):', newBalance);
try {
const remainingNano = BalanceConverter.rawToNano(newBalance);
const sendingNano = BalanceConverter.rawToNano(sendAmount.toString());
console.log('[AmountTrack] Amount SENDING (NANO):', sendingNano);
console.log('[AmountTrack] Remaining balance (NANO):', remainingNano);
} catch (e) {
console.log('[AmountTrack] Conversion error:', e.message);
}
// Generate work via RPC with retry logic for production reliability
console.log('Generating work via RPC for send transaction with retry protection...');
const workValue = await this.generateWorkWithRetry(accountInfo.frontier, false, 2);
const workData = { work: workValue };
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
// IMPORTANT: nanocurrency-web's block.send() calculates newBalance internally by doing:
// newBalance = walletBalanceRaw - amountRaw
// So walletBalanceRaw should be the CURRENT balance (before send), NOT the remaining balance
const blockData = {
walletBalanceRaw: currentBalance.toString(), // Current balance BEFORE send
fromAddress: formattedFromAddress,
toAddress: formattedToAddress,
representativeAddress: representative,
frontier: accountInfo.frontier,
amountRaw: amountRawString, // Amount to send (will be subtracted by library)
work: workData.work
};
console.log('Block data for send:', blockData);
console.log('[AmountTrack] ========== BLOCK DATA VERIFICATION ==========');
console.log('[AmountTrack] Block.amountRaw (what receiver will get):', amountRawString);
console.log('[AmountTrack] Block.walletBalanceRaw (sender current balance BEFORE send):', currentBalance.toString());
console.log('[AmountTrack] Expected balance AFTER send:', newBalance);
console.log('[AmountTrack] IMPORTANT: nanocurrency-web will calculate: finalBalance = walletBalanceRaw - amountRaw');
console.log('[AmountTrack] IMPORTANT: This prevents double-deduction bug');
// Log block-determining parameters (these create the block hash)
console.log('[BlockHash] Block hash is determined by:');
console.log('[BlockHash] - Frontier:', accountInfo.frontier);
console.log('[BlockHash] - New Balance:', newBalance);
console.log('[BlockHash] - Amount:', amountRawString);
console.log('[BlockHash] - Destination:', formattedToAddress);
console.log('[BlockHash] - Representative:', representative);
console.log('[BlockHash] NOTE: Work is NOT part of hash, only validates PoW');
// Sign the block using nanocurrency-web
const signedBlock = nanocurrency_web_1.block.send(blockData, privateKeyString);
console.log('Signed block:', signedBlock);
console.log('[BlockHash] Generated block hash:', signedBlock.hash || 'N/A');
console.log('[AmountTrack] ========== SIGNED BLOCK VERIFICATION ==========');
if (signedBlock.link) {
console.log('[AmountTrack] Signed block link (destination):', signedBlock.link);
}
if (signedBlock.balance) {
console.log('[AmountTrack] Signed block balance (remaining after send):', signedBlock.balance);
console.log('[AmountTrack] NOTE: This is the REMAINING balance, NOT the sent amount!');
}
console.log('[AmountTrack] Amount being sent to receiver:', amountRawString);
// Check for duplicate block attempts
const blockHash = signedBlock.hash;
const now = Date.now();
// Clean up old entries
for (const [hash, data] of this.recentBlockAttempts.entries()) {
if (now - data.timestamp > this.duplicateBlockTTL) {
this.recentBlockAttempts.delete(hash);
}
}
// Check if this exact block was recently attempted
if (this.recentBlockAttempts.has(blockHash)) {
const attemptData = this.recentBlockAttempts.get(blockHash);
attemptData.attempts++;
attemptData.lastAttempt = now;
console.warn('[BlockHash] ⚠️ DUPLICATE BLOCK DETECTED!');
console.warn(`[BlockHash] This exact block hash has been attempted ${attemptData.attempts} time(s) in the last 60 seconds`);
console.warn('[BlockHash] Block hash:', blockHash);
console.warn('[BlockHash] This means identical parameters: frontier, balance, amount, destination');
console.warn('[BlockHash] CRITICAL: If previous attempt failed, frontier may be stale!');
console.warn('[BlockHash] Proceeding with attempt, but this will likely fail...');
} else {
this.recentBlockAttempts.set(blockHash, {
timestamp: now,
lastAttempt: now,
attempts: 1,
parameters: {
frontier: accountInfo.frontier,
balance: newBalance,
amount: amountRawString,
destination: formattedToAddress
}
});
console.log('[BlockHash] First attempt with this block hash');
}
// Process the block
const processResult = await this.makeRequest('process', {
json_block: 'true',
subtype: 'send',
block: signedBlock
});
if (processResult.error) {
// Log duplicate block info if this was a duplicate attempt
if (this.recentBlockAttempts.has(blockHash)) {
const attemptData = this.recentBlockAttempts.get(blockHash);
console.error('[BlockHash] ❌ Transaction failed and this was a DUPLICATE block!');
console.error('[BlockHash] Previous attempts:', attemptData.attempts);
console.error('[BlockHash] Likely cause: Stale frontier data (account state not updated)');
console.error('[BlockHash] Solution: Wait a moment for network to settle, then fetch fresh account_info');
}
// Handle blockchain errors with enhanced messaging
const errorResponse = EnhancedErrorHandler.blockchainError(
processResult.error,
'send transaction',
{
fromAddress: formattedFromAddress,
toAddress: formattedToAddress,
amountRaw: amountRawString,
currentBalance: accountInfo.balance,
hash: accountInfo.frontier,
work: workData.work,
blockType: 'send',
duplicateBlock: this.recentBlockAttempts.has(blockHash),
blockHash: blockHash
}
);
// Add duplicate block guidance if detected
if (this.recentBlockAttempts.has(blockHash)) {
errorResponse.duplicateBlockDetected = true;
errorResponse.duplicateBlockGuidance = [
"⚠️ DUPLICATE BLOCK: Same block hash was attempted multiple times",
"This happens when retrying with identical: frontier, balance, amount, destination",
"SOLUTION: Wait 5-10 seconds for network to settle before retry",
"SOLUTION: Check account_info to get fresh frontier before retry",
"SOLUTION: If sending full balance, try sending slightly less",
"CRITICAL: Do NOT retry immediately - you'll create same invalid block!"
];
}
return errorResponse;
}
// Success! Clean up this block from recent attempts
this.recentBlockAttempts.delete(blockHash);
console.log('[BlockHash] ✅ Block processed successfully, hash:', processResult.hash);
return { success: true, hash: processResult.hash };
} catch (error) {
console.error('Send Transaction Error:', error);
// Return enhanced error if it's already an enhanced error object
if (error.errorCode) {
return error;
}
// Otherwise wrap in blockchain error
return EnhancedErrorHandler.blockchainError(
error.message,
'send transaction preparation',
{ fromAddress: formattedFromAddress, toAddress: formattedToAddress }
);
}
}
/**
* 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
};
}
/**
* Generate QR code for Nano payment
* @param {string} address - Nano address to receive payment
* @param {string} amount - Amount in decimal XNO (e.g., "0.140366")
* @returns {Promise<Object>} - QR code data with base64 image and payment string
*/
async generateQrCode(address, amount) {
try {
// Validate address format
if (!address || !address.startsWith('nano_')) {
throw new Error('Invalid Nano address format. Address must start with "nano_"');
}
// Validate amount
const amountNum = parseFloat(amount);
if (isNaN(amountNum) || amountNum <= 0) {
throw new Error('Invalid amount. Must be a positive number');
}
// Use format: nano:nano_address?amount=0.140366 (decimal, not raw)
const paymentString = `nano:${address}?amount=${amount}`;
console.log('Generating QR code for payment:', paymentString);
// Generate QR code using the qrcode library
const QRCode = require('qrcode');
const qrDataUrl = await QRCode.toDataURL(paymentString, {
errorCorrectionLevel: 'L',
width: 400,
margin: 4,
color: {
dark: '#000000',
light: '#FFFFFF',
},
});
console.log('QR code generated successfully');
return {
success: true,
qrCode: qrDataUrl,
paymentString: paymentString,
address: address,
amount: amount,
format: 'base64 Data URL (PNG)'
};
} catch (error) {
console.error('Error generating QR code:', error);
throw new Error(`Failed to generate QR code: ${error.message}`);
}
}
}
exports.NanoTransactions = NanoTransactions;