diff --git a/src/js/ripple/base.js b/src/js/ripple/base.js index 76715be0..bcbe6d37 100644 --- a/src/js/ripple/base.js +++ b/src/js/ripple/base.js @@ -1,142 +1,74 @@ -var sjcl = require('./utils').sjcl; -var utils = require('./utils'); -var extend = require('extend'); +'use strict'; +var _ = require('lodash'); +var sjcl = require('./utils').sjcl; +var utils = require('./utils'); +var extend = require('extend'); +var convertBase = require('./baseconverter'); var Base = {}; var alphabets = Base.alphabets = { - ripple: 'rpshnaf39wBUDNEGHJKLM4PQRST7VWXYZ2bcdeCg65jkm8oFqi1tuvAxyz', - tipple: 'RPShNAF39wBUDnEGHJKLM4pQrsT7VWXYZ2bcdeCg65jkm8ofqi1tuvaxyz', - bitcoin: '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz' + ripple: 'rpshnaf39wBUDNEGHJKLM4PQRST7VWXYZ2bcdeCg65jkm8oFqi1tuvAxyz', + tipple: 'RPShNAF39wBUDnEGHJKLM4pQrsT7VWXYZ2bcdeCg65jkm8ofqi1tuvaxyz', + bitcoin: '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz' }; extend(Base, { - VER_NONE : 1, - VER_NODE_PUBLIC : 28, - VER_NODE_PRIVATE : 32, - VER_ACCOUNT_ID : 0, - VER_ACCOUNT_PUBLIC : 35, - VER_ACCOUNT_PRIVATE : 34, - VER_FAMILY_GENERATOR : 41, - VER_FAMILY_SEED : 33 + VER_NONE: 1, + VER_NODE_PUBLIC: 28, + VER_NODE_PRIVATE: 32, + VER_ACCOUNT_ID: 0, + VER_ACCOUNT_PUBLIC: 35, + VER_ACCOUNT_PRIVATE: 34, + VER_FAMILY_GENERATOR: 41, + VER_FAMILY_SEED: 33 }); function sha256(bytes) { - return sjcl.codec.bytes.fromBits(sjcl.hash.sha256.hash(sjcl.codec.bytes.toBits(bytes))); + return sjcl.codec.bytes.fromBits( + sjcl.hash.sha256.hash(sjcl.codec.bytes.toBits(bytes))); } -function sha256hash(bytes) { - return sha256(sha256(bytes)); -} - -function divmod58(number, startAt) { - var remainder = 0; - for (var i = startAt; i < number.length; i++) { - var digit256 = number[i] & 0xFF; - var temp = remainder * 256 + digit256; - number[i] = (temp / 58); - remainder = temp % 58; - } - return remainder; -} - -function divmod256(number58, startAt) { - var remainder = 0; - for (var i = startAt; i < number58.length; i++) { - var digit58 = number58[i] & 0xFF; - var temp = remainder * 58 + digit58; - number58[i] = (temp / 256); - remainder = temp % 256; - } - return remainder; -} - -function encodeString (alphabet, input) { - if (input.length == 0) { +function encodeString(alphabet, input) { + if (input.length === 0) { return []; } - // we need to copy the buffer for calc - scratch = input.slice(); - - // Count leading zeroes. - var zeroCount = 0; - while (zeroCount < scratch.length && - scratch[zeroCount] == 0) - ++zeroCount; - - // The actual encoding. - var out = new Array(scratch.length * 2); - var j = out.length; - var startAt = zeroCount; - - while (startAt < scratch.length) { - var mod = divmod58(scratch, startAt); - if (scratch[startAt] == 0) { - ++startAt; + var leadingZeros = _.takeWhile(input, function(d) { + return d === 0; + }); + var out = convertBase(input, 256, 58).map(function(digit) { + if (digit < 0 || digit >= alphabet.length) { + throw new Error('Value ' + digit + ' is out of bounds for encoding'); } - out[--j] = alphabet[mod]; - } - - // Strip extra 'r' if there are some after decoding. - while (j < out.length && out[j] == alphabet[0]) ++j; - // Add as many leading 'r' as there were leading zeros. - while (--zeroCount >= 0) out[--j] = alphabet[0]; - while(j--) out.shift(); - - return out.join(''); + return alphabet[digit]; + }); + var prefix = leadingZeros.map(function() { + return alphabet[0]; + }); + return prefix.concat(out).join(''); } -function decodeString(indexes, input) { - var isString = typeof input === 'string'; - - if (input.length == 0) { +function decodeString(indexes, input) { + if (input.length === 0) { return []; } - input58 = new Array(input.length); - - // Transform the String to a base58 byte sequence - for (var i = 0; i < input.length; ++i) { - if (isString) { - var c = input.charCodeAt(i); + var input58 = input.split('').map(function(c) { + var charCode = c.charCodeAt(0); + if (charCode >= indexes.length) { + throw new Error('Character ' + c + ' is not valid for encoding'); } - - var digit58 = -1; - if (c >= 0 && c < 128) { - digit58 = indexes[c]; - } - if (digit58 < 0) { - throw new Error("Illegal character " + c + " at " + i); - } - - input58[i] = digit58; - } - // Count leading zeroes - var zeroCount = 0; - while (zeroCount < input58.length && input58[zeroCount] == 0) { - ++zeroCount; - } - // The encoding - out = utils.arraySet(input.length, 0); - var j = out.length; - - var startAt = zeroCount; - while (startAt < input58.length) { - var mod = divmod256(input58, startAt); - if (input58[startAt] == 0) { - ++startAt; - } - out[--j] = mod; - } - - // Do no add extra leading zeroes, move j to first non null byte. - while (j < out.length && (out[j] == 0)) ++j; - - j -= zeroCount; - while(j--) out.shift(); - - return out; + return indexes[charCode]; + }); + var leadingZeros = _.takeWhile(input58, function(d) { + return d === 0; + }); + var out = convertBase(input58, 58, 256); + var prefix = leadingZeros.map(function() { + return 0; + }); + return prefix.concat(out); } function Base58(alphabet) { @@ -151,8 +83,8 @@ function Base58(alphabet) { } Base.encoders = {}; -Object.keys(alphabets).forEach(function(alphabet){ - Base.encoders[alphabet] = Base58(alphabets[alphabet]); +Object.keys(alphabets).forEach(function(alphabet) { + Base.encoders[alphabet] = new Base58(alphabets[alphabet]); }); // --> input: big-endian array of bytes. @@ -165,22 +97,21 @@ Base.encode = function(input, alpha) { // <-- array of bytes or undefined. Base.decode = function(input, alpha) { if (typeof input !== 'string') { - return void(0); + return undefined; } try { return this.encoders[alpha || 'ripple'].decode(input); - } - catch(e) { - return (void 0); + } catch (e) { + return undefined; } }; Base.verify_checksum = function(bytes) { - var computed = sha256hash(bytes.slice(0, -4)).slice(0, 4); + var computed = sha256(sha256(bytes.slice(0, -4))).slice(0, 4); var checksum = bytes.slice(-4); var result = true; - for (var i=0; i<4; i++) { + for (var i = 0; i < 4; i++) { if (computed[i] !== checksum[i]) { result = false; break; @@ -194,7 +125,7 @@ Base.verify_checksum = function(bytes) { // <-- String Base.encode_check = function(version, input, alphabet) { var buffer = [].concat(version, input); - var check = sha256(sha256(buffer)).slice(0, 4); + var check = sha256(sha256(buffer)).slice(0, 4); return Base.encode([].concat(buffer, check), alphabet); }; @@ -217,7 +148,7 @@ Base.decode_check = function(version, input, alphabet) { if (Array.isArray(version)) { var match = false; - for (var i=0, l=version.length; i 0) { + var qr = divmod(dividend, fromBase, toBase); + result.unshift(qr.remainder); + dividend = qr.quotient; + } + return normalize(result); +} + +module.exports = convertBase; diff --git a/src/js/ripple/index.js b/src/js/ripple/index.js index c2fc8816..1ce5583f 100644 --- a/src/js/ripple/index.js +++ b/src/js/ripple/index.js @@ -1,25 +1,27 @@ -exports.Remote = require('./remote').Remote; -exports.Request = require('./request').Request; -exports.Amount = require('./amount').Amount; -exports.Account = require('./account').Account; -exports.Transaction = require('./transaction').Transaction; -exports.Currency = require('./currency').Currency; -exports.Base = require('./base').Base; -exports.UInt128 = require('./uint128').UInt128; -exports.UInt160 = require('./uint160').UInt160; -exports.UInt256 = require('./uint256').UInt256; -exports.Seed = require('./seed').Seed; -exports.Meta = require('./meta').Meta; +'use strict'; +exports.Remote = require('./remote').Remote; +exports.Request = require('./request').Request; +exports.Amount = require('./amount').Amount; +exports.Account = require('./account').Account; +exports.Transaction = require('./transaction').Transaction; +exports.Currency = require('./currency').Currency; +exports.Base = require('./base').Base; +exports.UInt128 = require('./uint128').UInt128; +exports.UInt160 = require('./uint160').UInt160; +exports.UInt256 = require('./uint256').UInt256; +exports.Seed = require('./seed').Seed; +exports.Meta = require('./meta').Meta; exports.SerializedObject = require('./serializedobject').SerializedObject; -exports.RippleError = require('./rippleerror').RippleError; -exports.Message = require('./message').Message; -exports.binformat = require('./binformat'); -exports.utils = require('./utils'); -exports.Server = require('./server').Server; -exports.Wallet = require('./wallet'); -exports.Ledger = require('./ledger').Ledger; +exports.RippleError = require('./rippleerror').RippleError; +exports.Message = require('./message').Message; +exports.binformat = require('./binformat'); +exports.utils = require('./utils'); +exports.Server = require('./server').Server; +exports.Wallet = require('./wallet'); +exports.Ledger = require('./ledger').Ledger; exports.TransactionQueue = require('./transactionqueue').TransactionQueue; -exports.RangeSet = require('./rangeset').RangeSet; +exports.RangeSet = require('./rangeset').RangeSet; +exports.convertBase = require('./baseconverter'); // Important: We do not guarantee any specific version of SJCL or for any // specific features to be included. The version and configuration may change at @@ -28,35 +30,35 @@ exports.RangeSet = require('./rangeset').RangeSet; // However, for programs that are tied to a specific version of ripple.js like // the official client, it makes sense to expose the SJCL instance so we don't // have to include it twice. -exports.sjcl = require('./utils').sjcl; -exports.types = require('./serializedtypes'); +exports.sjcl = require('./utils').sjcl; +exports.types = require('./serializedtypes'); exports.config = require('./config'); // camelCase to under_scored API conversion -function attachUnderscored(c) { - var o = exports[c]; +function attachUnderscored(name) { + var o = exports[name]; - Object.keys(o.prototype).forEach(function(key) { - var UPPERCASE = /([A-Z]{1})[a-z]+/g; + Object.keys(o.prototype).forEach(function(key) { + var UPPERCASE = /([A-Z]{1})[a-z]+/g; - if (!UPPERCASE.test(key)) { - return; - } + if (!UPPERCASE.test(key)) { + return; + } - var underscored = key.replace(UPPERCASE, function(c) { - return '_' + c.toLowerCase(); - }); + var underscored = key.replace(UPPERCASE, function(c) { + return '_' + c.toLowerCase(); + }); - o.prototype[underscored] = o.prototype[key]; - }); -}; + o.prototype[underscored] = o.prototype[key]; + }); +} -[ 'Remote', - 'Request', - 'Transaction', - 'Account', - 'Server' +['Remote', + 'Request', + 'Transaction', + 'Account', + 'Server' ].forEach(attachUnderscored); // vim:sw=2:sts=2:ts=8:et diff --git a/test/baseconverter-test.js b/test/baseconverter-test.js new file mode 100644 index 00000000..d34cab80 --- /dev/null +++ b/test/baseconverter-test.js @@ -0,0 +1,53 @@ +'use strict'; +var assert = require('assert'); +var convertBase = require('ripple-lib').convertBase; + +// Test cases from RFC-1924 (a joke RFC) +var BASE85 = ('0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ' + + 'abcdefghijklmnopqrstuvwxyz!#$%&()*+-;<=>?@^_`{|}~'); +var BASE10 = BASE85.slice(0, 10); +var BASE16 = BASE85.slice(0, 16); + +var DATA16 = '108000000000000000080800200C417A'; +var DATA10 = '21932261930451111902915077091070067066'; +var DATA85 = '4)+k&C#VzJ4br>0wv%Yp'; + +function encode(digitArray, encoding) { + return digitArray.map(function(i) { + return encoding.charAt(i); + }).join(''); +} + +function decode(encoded, encoding) { + return encoded.split('').map(function(c) { + return encoding.indexOf(c); + }); +} + +function convertBaseEncoded(value, fromEncoding, toEncoding) { + var digitArray = decode(value, fromEncoding); + var converted = convertBase(digitArray, fromEncoding.length, + toEncoding.length); + return encode(converted, toEncoding); +} + +describe('convertBase', function() { + it('DEC -> HEX', function () { + assert.strictEqual(convertBaseEncoded(DATA10, BASE10, BASE16), DATA16); + }); + it('HEX -> DEC', function () { + assert.strictEqual(convertBaseEncoded(DATA16, BASE16, BASE10), DATA10); + }); + it('DEC -> B85', function () { + assert.strictEqual(convertBaseEncoded(DATA10, BASE10, BASE85), DATA85); + }); + it('HEX -> B85', function () { + assert.strictEqual(convertBaseEncoded(DATA16, BASE16, BASE85), DATA85); + }); + it('B85 -> DEC', function () { + assert.strictEqual(convertBaseEncoded(DATA85, BASE85, BASE10), DATA10); + }); + it('B85 -> HEX', function () { + assert.strictEqual(convertBaseEncoded(DATA85, BASE85, BASE16), DATA16); + }); +});