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
245 lines (220 loc) • 9.57 kB
JavaScript
/**
* Centralized Nano (XNO) Conversion Utilities
*
* Uses string-based BigInt arithmetic for exact precision.
* NO floating-point math to avoid rounding errors.
*
* Nano uses 30 decimal places (10^30 raw units = 1 XNO)
*
* This utility helps clients who don't know about Nano conversion,
* numbers, and formats. It provides clear, precise conversions with
* comprehensive logging and validation.
*/
;
const ONE_XNO_RAW = BigInt("1000000000000000000000000000000"); // 10^30
class NanoConverter {
/**
* Convert XNO amount to raw units
* @param {number|string} xno - Amount in XNO (number or string)
* @returns {string} Raw amount as string
*
* @example
* xnoToRaw(1) // "1000000000000000000000000000000"
* xnoToRaw(0.000001) // "1000000000000000000000000"
* xnoToRaw("0.1") // "100000000000000000000000000000"
*/
static xnoToRaw(xno) {
try {
console.log(`[NanoConverter] Converting ${xno} XNO to raw`);
// Convert to string - use toString() to avoid floating-point precision errors
// DO NOT use toFixed() as it exposes floating-point representation errors
let xnoStr;
if (typeof xno === 'number') {
// Convert to string using toString() which gives clean representation
xnoStr = xno.toString();
// Handle scientific notation (e.g., 1e-9 becomes 0.000000001)
if (xnoStr.includes('e')) {
xnoStr = xno.toFixed(30).replace(/0+$/, '');
}
} else {
xnoStr = xno;
}
// Validate input
if (!xnoStr || xnoStr.trim() === '') {
throw new Error('XNO amount cannot be empty');
}
// Split into whole and decimal parts
const [whole, decimal = ''] = xnoStr.split('.');
// Pad decimal to exactly 30 digits, truncate if longer
const paddedDecimal = decimal.padEnd(30, '0').slice(0, 30);
// Concatenate and convert to BigInt
const rawStr = whole + paddedDecimal;
const result = BigInt(rawStr).toString();
console.log(`[NanoConverter] Result: ${result} raw`);
return result;
} catch (error) {
console.error('[NanoConverter] Error converting XNO to raw:', error);
throw new Error(`Invalid XNO amount: ${xno}. ${error.message}`);
}
}
/**
* Convert raw units to XNO amount
* @param {string} raw - Raw amount as string
* @returns {string} XNO amount as string
*
* @example
* rawToXNO("1000000000000000000000000000000") // "1"
* rawToXNO("1000000000000000000000000") // "0.000001"
* rawToXNO("100000000000000000000000000000") // "0.1"
*/
static rawToXNO(raw) {
try {
console.log(`[NanoConverter] Converting ${raw} raw to XNO`);
const rawBigInt = BigInt(raw);
// Convert to string and pad to at least 30 digits
const rawStr = rawBigInt.toString().padStart(30, '0');
// Split into whole and decimal parts (last 30 digits are decimal)
const whole = rawStr.slice(0, -30) || '0';
const decimal = rawStr.slice(-30);
// Remove trailing zeros from decimal
const trimmedDecimal = decimal.replace(/0+$/, '');
// Return formatted number
let result;
if (trimmedDecimal === '') {
result = whole;
} else {
result = `${whole}.${trimmedDecimal}`;
}
console.log(`[NanoConverter] Result: ${result} XNO`);
return result;
} catch (error) {
console.error('[NanoConverter] Error converting raw to XNO:', error);
throw new Error(`Invalid raw amount: ${raw}. ${error.message}`);
}
}
/**
* Validate Nano address format
* @param {string} address - Nano address to validate
* @returns {boolean} true if valid format
*
* @example
* isValidNanoAddress("nano_3xxx...") // true
* isValidNanoAddress("xrb_1xxx...") // true
* isValidNanoAddress("invalid") // false
*/
static isValidNanoAddress(address) {
try {
const isValid = /^(nano|xrb)_[13]{1}[13456789abcdefghijkmnopqrstuwxyz]{59}$/.test(address);
console.log(`[NanoConverter] Address validation for ${address}: ${isValid}`);
return isValid;
} catch (error) {
console.error('[NanoConverter] Error validating address:', error);
return false;
}
}
/**
* Format XNO amount for display
* @param {string|number} xno - XNO amount as string or number
* @param {number} decimals - Number of decimal places to show (default: 6)
* @returns {string} Formatted XNO string
*
* @example
* formatXNO("0.123456789", 6) // "0.123457"
* formatXNO("1.5", 2) // "1.50"
*/
static formatXNO(xno, decimals = 6) {
try {
console.log(`[NanoConverter] Formatting ${xno} XNO to ${decimals} decimals`);
const xnoNum = typeof xno === 'string' ? parseFloat(xno) : xno;
const result = xnoNum.toFixed(decimals);
console.log(`[NanoConverter] Formatted result: ${result}`);
return result;
} catch (error) {
console.error('[NanoConverter] Error formatting XNO:', error);
throw new Error(`Invalid XNO amount for formatting: ${xno}. ${error.message}`);
}
}
/**
* Get conversion examples for user guidance
* @returns {Object} Conversion examples
*/
static getConversionExamples() {
return {
"0.000001_XNO": "1000000000000000000000000",
"0.00001_XNO": "10000000000000000000000000",
"0.0001_XNO": "100000000000000000000000000",
"0.001_XNO": "1000000000000000000000000000",
"0.01_XNO": "10000000000000000000000000000",
"0.1_XNO": "100000000000000000000000000000",
"1_XNO": "1000000000000000000000000000000",
"10_XNO": "10000000000000000000000000000000"
};
}
/**
* Get human-readable conversion help
* @returns {Object} Conversion help information
*/
static getConversionHelp() {
return {
description: "Nano (XNO) uses raw units for all on-chain operations. 1 XNO = 10^30 raw. This ensures exact precision without floating-point errors.",
formula: "raw = XNO × 10^30",
reverseFormula: "XNO = raw ÷ 10^30",
decimalPlaces: 30,
examples: this.getConversionExamples(),
commonMistakes: [
"Using XNO value instead of raw in amountRaw parameter",
"Providing decimal numbers as raw units",
"Not converting before API calls",
"Using floating-point arithmetic which causes rounding errors",
"Confusing NANO with XNO (they are the same currency)"
],
tools: {
xnoToRaw: "NanoConverter.xnoToRaw('0.1') => '100000000000000000000000000000'",
rawToXNO: "NanoConverter.rawToXNO('100000000000000000000000000000') => '0.1'",
isValidNanoAddress: "NanoConverter.isValidNanoAddress('nano_3xxx...') => true",
formatXNO: "NanoConverter.formatXNO('0.123456789', 6) => '0.123457'"
},
bestPractices: [
"Always use string-based BigInt arithmetic for conversions",
"Never use floating-point math for currency calculations",
"Validate addresses before transactions",
"Use formatXNO for display purposes only, not for calculations",
"Store amounts in raw format for precision"
]
};
}
/**
* Validate if a value is valid raw amount
* @param {string} rawAmount - Amount to validate
* @returns {boolean} True if valid
*/
static isValidRaw(rawAmount) {
try {
const raw = BigInt(rawAmount);
return raw >= 0;
} catch (error) {
console.error('[NanoConverter] Error validating raw amount:', error);
return false;
}
}
/**
* Format balance for display with both raw and XNO
* @param {string} rawAmount - Amount in raw
* @returns {Object} Formatted balance
*/
static formatBalance(rawAmount) {
try {
const xno = this.rawToXNO(rawAmount);
return {
raw: rawAmount,
xno: xno,
formatted: this.formatXNO(xno, 6),
display: `${this.formatXNO(xno, 6)} XNO`
};
} catch (error) {
console.error('[NanoConverter] Error formatting balance:', error);
throw new Error(`Invalid raw amount for formatting: ${rawAmount}. ${error.message}`);
}
}
}
module.exports = { NanoConverter };