diff --git a/src/js/ripple/binformat.js b/src/js/ripple/binformat.js index cd5868c3..35f8ce9e 100644 --- a/src/js/ripple/binformat.js +++ b/src/js/ripple/binformat.js @@ -2,6 +2,9 @@ * Data type map. * * Mapping of type ids to data types. The type id is specified by the high + * + * For reference, see rippled's definition: + * https://github.com/ripple/rippled/blob/develop/src/ripple/data/protocol/SField.cpp */ var TYPES_MAP = exports.types = [ void(0), @@ -375,7 +378,7 @@ exports.ledger = { ['Balance', REQUIRED], ['LowLimit', REQUIRED], ['HighLimit', REQUIRED]]) -} +}; exports.metadata = [ [ 'TransactionIndex' , REQUIRED ], diff --git a/src/js/ripple/serializedobject.js b/src/js/ripple/serializedobject.js index 841a9778..4ce7078d 100644 --- a/src/js/ripple/serializedobject.js +++ b/src/js/ripple/serializedobject.js @@ -42,7 +42,7 @@ function SerializedObject(buf) { SerializedObject.from_json = function(obj) { // Create a copy of the object so we don't modify it - var obj = extend({}, obj); + var obj = extend(true, {}, obj); var so = new SerializedObject(); var typedef; diff --git a/src/js/ripple/serializedtypes.js b/src/js/ripple/serializedtypes.js index 5eab078f..5e282fcd 100644 --- a/src/js/ripple/serializedtypes.js +++ b/src/js/ripple/serializedtypes.js @@ -24,6 +24,7 @@ var Currency = amount.Currency; // Shortcuts var hex = sjcl.codec.hex; var bytes = sjcl.codec.bytes; +var utf8 = sjcl.codec.utf8String; var BigInteger = utils.jsbn.BigInteger; @@ -52,7 +53,7 @@ function isBigInteger(val) { return val instanceof BigInteger; }; -function serialize_hex(so, hexData, noLength) { +function serializeHex(so, hexData, noLength) { var byteData = bytes.fromBits(hex.toBits(hexData)); if (!noLength) { SerializedType.serialize_varint(so, byteData.length); @@ -63,10 +64,18 @@ function serialize_hex(so, hexData, noLength) { /** * parses bytes as hex */ -function convert_bytes_to_hex (byte_array) { +function convertByteArrayToHex (byte_array) { return sjcl.codec.hex.fromBits(sjcl.codec.bytes.toBits(byte_array)).toUpperCase(); }; +function convertStringToHex(string) { + return hex.fromBits(utf8.toBits(string)).toUpperCase(); +} + +function convertHexToString(hexString) { + return utf8.fromBits(hex.toBits(hexString)); +} + SerializedType.serialize_varint = function (so, val) { if (val < 0) { throw new Error('Variable integers are unsigned.'); @@ -115,7 +124,7 @@ SerializedType.prototype.parse_varint = function (so) { * * The result is appended to the serialized object ('so'). */ -function append_byte_array(so, val, bytes) { +function convertIntegerToByteArray(val, bytes) { if (!isNumber(val)) { throw new Error('Value is not a number', bytes); } @@ -130,7 +139,7 @@ function append_byte_array(so, val, bytes) { newBytes.unshift(val >>> (i * 8) & 0xff); } - so.append(newBytes); + return newBytes; }; // Convert a certain number of bytes from the serialized object ('so') into an integer. @@ -152,7 +161,7 @@ function readAndSum(so, bytes) { var STInt8 = exports.Int8 = new SerializedType({ serialize: function (so, val) { - append_byte_array(so, val, 1); + so.append(convertIntegerToByteArray(val, 1)); }, parse: function (so) { return readAndSum(so, 1); @@ -163,7 +172,7 @@ STInt8.id = 16; var STInt16 = exports.Int16 = new SerializedType({ serialize: function (so, val) { - append_byte_array(so, val, 2); + so.append(convertIntegerToByteArray(val, 2)); }, parse: function (so) { return readAndSum(so, 2); @@ -174,7 +183,7 @@ STInt16.id = 1; var STInt32 = exports.Int32 = new SerializedType({ serialize: function (so, val) { - append_byte_array(so, val, 4); + so.append(convertIntegerToByteArray(val, 4)); }, parse: function (so) { return readAndSum(so, 4); @@ -217,7 +226,7 @@ var STInt64 = exports.Int64 = new SerializedType({ hex = '0' + hex; } - serialize_hex(so, hex, true); //noLength = true + serializeHex(so, hex, true); //noLength = true }, parse: function (so) { var bytes = so.read(8); @@ -237,7 +246,7 @@ var STHash128 = exports.Hash128 = new SerializedType({ if (!hash.is_valid()) { throw new Error('Invalid Hash128'); } - serialize_hex(so, hash.to_hex(), true); //noLength = true + serializeHex(so, hash.to_hex(), true); //noLength = true }, parse: function (so) { return UInt128.from_bytes(so.read(16)); @@ -252,7 +261,7 @@ var STHash256 = exports.Hash256 = new SerializedType({ if (!hash.is_valid()) { throw new Error('Invalid Hash256'); } - serialize_hex(so, hash.to_hex(), true); //noLength = true + serializeHex(so, hash.to_hex(), true); //noLength = true }, parse: function (so) { return UInt256.from_bytes(so.read(32)); @@ -267,7 +276,7 @@ var STHash160 = exports.Hash160 = new SerializedType({ if (!hash.is_valid()) { throw new Error('Invalid Hash160'); } - serialize_hex(so, hash.to_hex(), true); //noLength = true + serializeHex(so, hash.to_hex(), true); //noLength = true }, parse: function (so) { return UInt160.from_bytes(so.read(20)); @@ -294,7 +303,7 @@ var STCurrency = new SerializedType({ // UInt160 value and consider it valid. But it doesn't, so for the // deserialization to be usable, we need to allow invalid results for now. //if (!currency.is_valid()) { - // throw new Error('Invalid currency: '+convert_bytes_to_hex(bytes)); + // throw new Error('Invalid currency: '+convertByteArrayToHex(bytes)); //} return currency; } @@ -409,15 +418,16 @@ STAmount.id = 6; var STVL = exports.VariableLength = exports.VL = new SerializedType({ serialize: function (so, val) { + if (typeof val === 'string') { - serialize_hex(so, val); + serializeHex(so, val); } else { throw new Error('Unknown datatype.'); } }, parse: function (so) { var len = this.parse_varint(so); - return convert_bytes_to_hex(so.read(len)); + return convertByteArrayToHex(so.read(len)); } }); @@ -429,7 +439,7 @@ var STAccount = exports.Account = new SerializedType({ if (!account.is_valid()) { throw new Error('Invalid account!'); } - serialize_hex(so, account.to_hex()); + serializeHex(so, account.to_hex()); }, parse: function (so) { var len = this.parse_varint(so); @@ -441,7 +451,6 @@ var STAccount = exports.Account = new SerializedType({ var result = UInt160.from_bytes(so.read(len)); result.set_version(Base.VER_ACCOUNT_ID); - //console.log('PARSED 160:', result.to_json()); if (false && !result.is_valid()) { throw new Error('Invalid Account'); } @@ -593,6 +602,105 @@ var STVector256 = exports.Vector256 = new SerializedType({ STVector256.id = 19; +// Internal +var STMemo = exports.STMemo = new SerializedType({ + serialize: function(so, val, no_marker) { + + var keys = []; + + Object.keys(val).forEach(function (key) { + // Ignore lowercase field names - they're non-serializable fields by + // convention. + if (key[0] === key[0].toLowerCase()) { + return; + } + + if (typeof binformat.fieldsInverseMap[key] === 'undefined') { + throw new Error('JSON contains unknown field: "' + key + '"'); + } + + keys.push(key); + }); + + // Sort fields + keys = sort_fields(keys); + + // store that we're dealing with json + var isJson = val.MemoFormat === 'json'; + + for (var i=0; i'); var expected = [ { Memo: { - MemoType: new Buffer('testkey').toString('hex'), - MemoData: new Buffer('testvalue').toString('hex') + MemoType: 'testkey', + MemoData: 'testvalue' }}, { Memo: { - MemoType: new Buffer('testkey2').toString('hex'), - MemoData: new Buffer('testvalue2').toString('hex') + MemoType: 'testkey2', + MemoData: 'testvalue2' }}, { Memo: { - MemoType: new Buffer('testkey3').toString('hex') + MemoType: 'testkey3', + MemoFormat: 'text/html' }}, { Memo: { - MemoData: new Buffer('testvalue4').toString('hex') - } } + MemoData: 'testvalue4' + }}, + { Memo: { + MemoType: 'testkey4', + MemoFormat: 'text/html', + MemoData: '' + }} ]; assert.deepEqual(transaction.tx_json.Memos, expected); @@ -1085,13 +1143,76 @@ describe('Transaction', function() { }, /^Error: MemoType must be a string$/); }); - it('Add Memo - invalid MemoData', function() { + it('Add Memo - invalid ASCII MemoType', function() { var transaction = new Transaction(); transaction.tx_json.TransactionType = 'Payment'; assert.throws(function() { - transaction.addMemo('key', 1); - }, /^Error: MemoData must be a string$/); + transaction.addMemo('한국어'); + }, /^Error: MemoType must be valid ASCII$/); + }); + + it('Add Memo - invalid MemoFormat', function() { + var transaction = new Transaction(); + transaction.tx_json.TransactionType = 'Payment'; + + assert.throws(function() { + transaction.addMemo(void(0), 1); + }, /^Error: MemoFormat must be a string$/); + }); + + it('Add Memo - invalid ASCII MemoFormat', function() { + var transaction = new Transaction(); + transaction.tx_json.TransactionType = 'Payment'; + + assert.throws(function() { + transaction.addMemo(void(0), 'России'); + }, /^Error: MemoFormat must be valid ASCII$/); + }); + + it('Add Memo - MemoData string', function() { + var transaction = new Transaction(); + transaction.tx_json.TransactionType = 'Payment'; + + transaction.addMemo({memoData:'some_string'}); + + assert.deepEqual(transaction.tx_json.Memos, [ + { + Memo: { + MemoData: 'some_string' + } + } + ]); + }); + + it('Add Memo - MemoData complex object', function() { + var transaction = new Transaction(); + transaction.tx_json.TransactionType = 'Payment'; + + var memo = { + memoData: { + string: 'string', + int: 1, + array: [ + { + string: 'string' + } + ], + object: { + string: 'string' + } + } + }; + + transaction.addMemo(memo); + + assert.deepEqual(transaction.tx_json.Memos, [ + { + Memo: { + MemoData: memo.memoData + } + } + ]); }); it('Construct AccountSet transaction', function() { @@ -1268,7 +1389,7 @@ describe('Transaction', function() { var bid = '1/USD/rsLEU1TPdCJPPysqhWYw9jD97xtG5WqSJm'; var ask = '1/EUR/rsLEU1TPdCJPPysqhWYw9jD97xtG5WqSJm'; assert.throws(function() { - var transaction = new Transaction().offerCreate('xrsLEU1TPdCJPPysqhWYw9jD97xtG5WqSJm', bid, ask); + new Transaction().offerCreate('xrsLEU1TPdCJPPysqhWYw9jD97xtG5WqSJm', bid, ask); }); }); @@ -1301,13 +1422,13 @@ describe('Transaction', function() { it('Construct SetRegularKey transaction - invalid account', function() { assert.throws(function() { - var transaction = new Transaction().setRegularKey('xrsLEU1TPdCJPPysqhWYw9jD97xtG5WqSJm', 'r36xtKNKR43SeXnGn7kN4r4JdQzcrkqpWe'); + new Transaction().setRegularKey('xrsLEU1TPdCJPPysqhWYw9jD97xtG5WqSJm', 'r36xtKNKR43SeXnGn7kN4r4JdQzcrkqpWe'); }); }); it('Construct SetRegularKey transaction - invalid regularKey', function() { assert.throws(function() { - var transaction = new Transaction().setRegularKey('rsLEU1TPdCJPPysqhWYw9jD97xtG5WqSJm', 'xr36xtKNKR43SeXnGn7kN4r4JdQzcrkqpWe'); + new Transaction().setRegularKey('rsLEU1TPdCJPPysqhWYw9jD97xtG5WqSJm', 'xr36xtKNKR43SeXnGn7kN4r4JdQzcrkqpWe'); }); });