diff --git a/lib/bcoin/utils.js b/lib/bcoin/utils.js index c4470c73..3f56af23 100644 --- a/lib/bcoin/utils.js +++ b/lib/bcoin/utils.js @@ -648,69 +648,80 @@ utils.assert.fatal = function fatal(value, message) { }; /** - * Convert satoshis to a BTC string. - * This function explicitly avoids - * any floating point arithmetic. - * @param {Number} satoshi + * Safely convert satoshis to a BTC string. + * This function explicitly avoids any + * floating point arithmetic. + * @param {Number} value - Satoshis. * @returns {String} BTC string. */ -utils.btc = function btc(satoshi) { +utils.btc = function btc(value) { var negative = false; - var btc; + var hi, lo, result; - if (utils.isBTC(satoshi)) - return satoshi; + if (utils.isFloat(value)) + return value; - assert(utils.isSatoshi(satoshi), 'Non-satoshi value for conversion.'); + assert(utils.isInt(value), 'Non-satoshi value for conversion.'); - if (satoshi < 0) { - satoshi = -satoshi; + if (value < 0) { + value = -value; negative = true; } - btc = satoshi.toString(10); + assert(value <= utils.MAX_SAFE_INTEGER, 'Number exceeds 2^53-1.'); - while (btc.length < 9) - btc = '0' + btc; + // assert(value <= 21000000 * 100000000, 'Number exceeds MAX_MONEY.'); - btc = btc.slice(0, -8) + '.' + btc.slice(-8); + value = value.toString(10); - btc = btc.replace(/0+$/, ''); + assert(value.length <= 16, 'Number exceeds 2^53-1.'); - if (btc[btc.length - 1] === '.') - btc = btc.slice(0, -1); + while (value.length < 9) + value = '0' + value; + + hi = value.slice(0, -8); + lo = value.slice(-8); + + lo = lo.replace(/0+$/, ''); + + if (lo.length === 0) + lo += '0'; + + result = hi + '.' + lo; if (negative) - btc = '-' + btc; + result = '-' + result; - return btc; + return result; }; /** - * Convert BTC string to satoshis. - * This function explicitly avoids - * any floating point arithmetic. - * @param {String} btc + * Safely convert a BTC string to satoshis. + * This function explicitly avoids any + * floating point arithmetic. It also does + * extra validation to ensure the resulting + * Number will be 53 bits or less. + * @param {String} value - BTC * @returns {Number} Satoshis. * @throws on parse error */ -utils.satoshi = function satoshi(btc) { +utils.satoshi = function satoshi(value) { var negative = false; - var satoshi, parts, hi, lo; + var parts, hi, lo, result; - if (utils.isSatoshi(btc)) - return btc; + if (utils.isInt(value)) + return value; - assert(utils.isBTC(btc), 'Non-BTC value for conversion.'); + assert(utils.isFloat(value), 'Non-BTC value for conversion.'); - if (btc[0] === '-') { + if (value[0] === '-') { negative = true; - btc = btc.substring(1); + value = value.substring(1); } - parts = btc.split('.'); + parts = value.split('.'); assert(parts.length <= 2, 'Bad decimal point.'); @@ -720,53 +731,93 @@ utils.satoshi = function satoshi(btc) { hi = hi.replace(/^0+/, ''); lo = lo.replace(/0+$/, ''); - assert(hi.length <= 8, 'Number exceeds MAX_MONEY.'); - assert(+hi < 21000000, 'Number exceeds MAX_MONEY.'); - + assert(hi.length <= 8, 'Number exceeds 2^53-1.'); assert(lo.length <= 8, 'Too many decimal places.'); + if (hi.length === 0) + hi = '0'; + while (lo.length < 8) lo += '0'; - satoshi = parseInt(hi + lo, 10); + hi = parseInt(hi, 10); + lo = parseInt(lo, 10); + + assert(hi < 90071992 || (hi === 90071992 && lo <= 54740991), + 'Number exceeds 2^53-1.'); + + // assert(hi < 21000000 || (hi === 21000000 && lo === 0), + // 'Number exceeds MAX_MONEY.'); + + result = hi * 100000000 + lo; if (negative) - satoshi = -satoshi; + result = -result; - return satoshi; + return result; }; /** * Test whether a number is both a Number and finite. - * @param {Number?} val + * @param {Number?} value * @returns {Boolean} */ -utils.isNumber = function isNumber(val) { - return typeof val === 'number' && isFinite(val); +utils.isNumber = function isNumber(value) { + return typeof value === 'number' && isFinite(value); }; /** - * Test whether an object qualifies as satoshis. - * @param {BN?} val + * Test whether a string qualifies as a float. + * @param {String?} value * @returns {Boolean} */ -utils.isSatoshi = function isSatoshi(val) { - return utils.isNumber(val) && val % 1 === 0; +utils.isFloat = function isFloat(value) { + return typeof value === 'string' + && /^-?(\d+)?(?:\.\d*)?$/.test(value) + && value.length !== 0 + && value !== '-'; }; /** - * Test whether an object qualifies as a BTC string. - * @param {String?} val + * Test whether an object is a finite number and int. + * @param {BN?} value * @returns {Boolean} */ -utils.isBTC = function isBTC(val) { - return typeof val === 'string' - && /^-?(\d+)?(?:\.\d*)?$/.test(val) - && val.length !== 0 - && val !== '-'; +utils.isInt = function isInt(value) { + return utils.isNumber(value) && value % 1 === 0; +}; + +/** + * Test and validate a satoshi value (Number). + * @param {Number?} value + * @returns {Boolean} + */ + +utils.isSatoshi = function isSatoshi(value) { + try { + utils.satoshi(value); + return true; + } catch (e) { + return false; + } +}; + +/** + * Test and validate a BTC string. + * @param {String?} value + * @returns {Boolean} + */ + +utils.isBTC = function isBTC(value) { + try { + utils.btc(value); + return true; + } catch (e) { + return false; + } }; /** @@ -1600,6 +1651,15 @@ utils.writeU64NBE = function writeU64NBE(dst, num, off) { utils.MAX_SAFE_INTEGER = 0x1fffffffffffff; +/** + * Max 52 bit integer (safe for additions). + * (MAX_SAFE_INTEGER - 1) / 2 + * @type Number + * @const + */ + +utils.MAX_SAFE_ADDITION = 0xfffffffffffff; + /** * Write a javascript number as an int64le (faster than big numbers). * @param {Number} value @@ -1613,8 +1673,6 @@ utils.write64N = function write64N(dst, num, off, be) { off = off >>> 0; - assert(num <= utils.MAX_SAFE_INTEGER, 'Number exceeds 2^53-1'); - negative = num < 0; if (negative) { @@ -1622,6 +1680,8 @@ utils.write64N = function write64N(dst, num, off, be) { num -= 1; } + assert(num <= utils.MAX_SAFE_INTEGER, 'Number exceeds 2^53-1'); + hi = num / 0x100000000 | 0; lo = num % 0x100000000; diff --git a/test/utils-test.js b/test/utils-test.js index e2db19cf..5ba9b470 100644 --- a/test/utils-test.js +++ b/test/utils-test.js @@ -27,7 +27,7 @@ describe('Utils', function() { btc = utils.btc(54678 * 1000000); assert.equal(btc, '546.78'); btc = utils.btc(5460 * 10000000); - assert.equal(btc, '546'); + assert.equal(btc, '546.0'); }); it('should convert btc to satoshi', function() { @@ -37,6 +37,42 @@ describe('Utils', function() { assert(btc === 54678 * 1000000); btc = utils.satoshi('546'); assert(btc === 5460 * 10000000); + btc = utils.satoshi('546.0'); + assert(btc === 5460 * 10000000); + btc = utils.satoshi('546.0000'); + assert(btc === 5460 * 10000000); + assert.doesNotThrow(function() { + utils.satoshi('546.00000000000000000'); + }); + assert.throws(function() { + utils.satoshi('546.00000000000000001'); + }); + /* + assert.doesNotThrow(function() { + utils.satoshi('21000000'); + }); + assert.doesNotThrow(function() { + utils.satoshi('021000000'); + }); + assert.throws(function() { + utils.satoshi('21000001'); + }); + assert.throws(function() { + utils.satoshi('121000000'); + }); + */ + assert.doesNotThrow(function() { + utils.satoshi('90071992.54740991'); + }); + assert.doesNotThrow(function() { + utils.satoshi('090071992.547409910'); + }); + assert.throws(function() { + utils.satoshi('90071992.54740992'); + }); + assert.throws(function() { + utils.satoshi('190071992.54740991'); + }); }); var unsigned = [