[FEATURE] improve memo support

- add MemoFormat property for memo
- MemoFormat and MemoType must be valid ASCII
- Memo content is converted on the serialization level
- add parsed_* version of Memo content if the parser understand the format
- support `text` and `json` MemoFormat

[FIX] double serialization overriding Memo contents

The copy made in from_json wasn't a deep copy
This commit is contained in:
Geert Weening
2014-11-05 17:07:31 -08:00
parent 666e4348e0
commit 1704ac4ae1
6 changed files with 553 additions and 67 deletions

View File

@@ -2,6 +2,9 @@
* Data type map. * Data type map.
* *
* Mapping of type ids to data types. The type id is specified by the high * 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 = [ var TYPES_MAP = exports.types = [
void(0), void(0),
@@ -375,7 +378,7 @@ exports.ledger = {
['Balance', REQUIRED], ['Balance', REQUIRED],
['LowLimit', REQUIRED], ['LowLimit', REQUIRED],
['HighLimit', REQUIRED]]) ['HighLimit', REQUIRED]])
} };
exports.metadata = [ exports.metadata = [
[ 'TransactionIndex' , REQUIRED ], [ 'TransactionIndex' , REQUIRED ],

View File

@@ -42,7 +42,7 @@ function SerializedObject(buf) {
SerializedObject.from_json = function(obj) { SerializedObject.from_json = function(obj) {
// Create a copy of the object so we don't modify it // 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 so = new SerializedObject();
var typedef; var typedef;

View File

@@ -24,6 +24,7 @@ var Currency = amount.Currency;
// Shortcuts // Shortcuts
var hex = sjcl.codec.hex; var hex = sjcl.codec.hex;
var bytes = sjcl.codec.bytes; var bytes = sjcl.codec.bytes;
var utf8 = sjcl.codec.utf8String;
var BigInteger = utils.jsbn.BigInteger; var BigInteger = utils.jsbn.BigInteger;
@@ -52,7 +53,7 @@ function isBigInteger(val) {
return val instanceof BigInteger; return val instanceof BigInteger;
}; };
function serialize_hex(so, hexData, noLength) { function serializeHex(so, hexData, noLength) {
var byteData = bytes.fromBits(hex.toBits(hexData)); var byteData = bytes.fromBits(hex.toBits(hexData));
if (!noLength) { if (!noLength) {
SerializedType.serialize_varint(so, byteData.length); SerializedType.serialize_varint(so, byteData.length);
@@ -63,10 +64,18 @@ function serialize_hex(so, hexData, noLength) {
/** /**
* parses bytes as hex * 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(); 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) { SerializedType.serialize_varint = function (so, val) {
if (val < 0) { if (val < 0) {
throw new Error('Variable integers are unsigned.'); 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'). * The result is appended to the serialized object ('so').
*/ */
function append_byte_array(so, val, bytes) { function convertIntegerToByteArray(val, bytes) {
if (!isNumber(val)) { if (!isNumber(val)) {
throw new Error('Value is not a number', bytes); 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); 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. // 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({ var STInt8 = exports.Int8 = new SerializedType({
serialize: function (so, val) { serialize: function (so, val) {
append_byte_array(so, val, 1); so.append(convertIntegerToByteArray(val, 1));
}, },
parse: function (so) { parse: function (so) {
return readAndSum(so, 1); return readAndSum(so, 1);
@@ -163,7 +172,7 @@ STInt8.id = 16;
var STInt16 = exports.Int16 = new SerializedType({ var STInt16 = exports.Int16 = new SerializedType({
serialize: function (so, val) { serialize: function (so, val) {
append_byte_array(so, val, 2); so.append(convertIntegerToByteArray(val, 2));
}, },
parse: function (so) { parse: function (so) {
return readAndSum(so, 2); return readAndSum(so, 2);
@@ -174,7 +183,7 @@ STInt16.id = 1;
var STInt32 = exports.Int32 = new SerializedType({ var STInt32 = exports.Int32 = new SerializedType({
serialize: function (so, val) { serialize: function (so, val) {
append_byte_array(so, val, 4); so.append(convertIntegerToByteArray(val, 4));
}, },
parse: function (so) { parse: function (so) {
return readAndSum(so, 4); return readAndSum(so, 4);
@@ -217,7 +226,7 @@ var STInt64 = exports.Int64 = new SerializedType({
hex = '0' + hex; hex = '0' + hex;
} }
serialize_hex(so, hex, true); //noLength = true serializeHex(so, hex, true); //noLength = true
}, },
parse: function (so) { parse: function (so) {
var bytes = so.read(8); var bytes = so.read(8);
@@ -237,7 +246,7 @@ var STHash128 = exports.Hash128 = new SerializedType({
if (!hash.is_valid()) { if (!hash.is_valid()) {
throw new Error('Invalid Hash128'); 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) { parse: function (so) {
return UInt128.from_bytes(so.read(16)); return UInt128.from_bytes(so.read(16));
@@ -252,7 +261,7 @@ var STHash256 = exports.Hash256 = new SerializedType({
if (!hash.is_valid()) { if (!hash.is_valid()) {
throw new Error('Invalid Hash256'); 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) { parse: function (so) {
return UInt256.from_bytes(so.read(32)); return UInt256.from_bytes(so.read(32));
@@ -267,7 +276,7 @@ var STHash160 = exports.Hash160 = new SerializedType({
if (!hash.is_valid()) { if (!hash.is_valid()) {
throw new Error('Invalid Hash160'); 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) { parse: function (so) {
return UInt160.from_bytes(so.read(20)); 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 // 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. // deserialization to be usable, we need to allow invalid results for now.
//if (!currency.is_valid()) { //if (!currency.is_valid()) {
// throw new Error('Invalid currency: '+convert_bytes_to_hex(bytes)); // throw new Error('Invalid currency: '+convertByteArrayToHex(bytes));
//} //}
return currency; return currency;
} }
@@ -409,15 +418,16 @@ STAmount.id = 6;
var STVL = exports.VariableLength = exports.VL = new SerializedType({ var STVL = exports.VariableLength = exports.VL = new SerializedType({
serialize: function (so, val) { serialize: function (so, val) {
if (typeof val === 'string') { if (typeof val === 'string') {
serialize_hex(so, val); serializeHex(so, val);
} else { } else {
throw new Error('Unknown datatype.'); throw new Error('Unknown datatype.');
} }
}, },
parse: function (so) { parse: function (so) {
var len = this.parse_varint(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()) { if (!account.is_valid()) {
throw new Error('Invalid account!'); throw new Error('Invalid account!');
} }
serialize_hex(so, account.to_hex()); serializeHex(so, account.to_hex());
}, },
parse: function (so) { parse: function (so) {
var len = this.parse_varint(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)); var result = UInt160.from_bytes(so.read(len));
result.set_version(Base.VER_ACCOUNT_ID); result.set_version(Base.VER_ACCOUNT_ID);
//console.log('PARSED 160:', result.to_json());
if (false && !result.is_valid()) { if (false && !result.is_valid()) {
throw new Error('Invalid Account'); throw new Error('Invalid Account');
} }
@@ -593,6 +602,105 @@ var STVector256 = exports.Vector256 = new SerializedType({
STVector256.id = 19; 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<keys.length; i++) {
var key = keys[i];
var value = val[key];
switch (key) {
// MemoType and MemoFormat are always ASCII strings
case 'MemoType':
case 'MemoFormat':
value = convertStringToHex(value);
break;
// MemoData can be a JSON object, otherwise it's a string
case 'MemoData':
if (typeof value !== 'string') {
if (isJson) {
try {
value = convertStringToHex(JSON.stringify(value));
} catch (e) {
throw new Error('MemoFormat json with invalid JSON in MemoData field');
}
} else {
throw new Error('MemoData can only be a JSON object with a valid json MemoFormat');
}
} else if (isString(value)) {
value = convertStringToHex(value);
}
break;
}
serialize(so, key, value);
}
if (!no_marker) {
//Object ending marker
STInt8.serialize(so, 0xe1);
}
},
parse: function(so) {
var output = {};
while (so.peek(1)[0] !== 0xe1) {
var keyval = parse(so);
output[keyval[0]] = keyval[1];
}
if (output['MemoType'] !== void(0)) {
output['parsed_memo_type'] = convertHexToString(output['MemoType']);
}
if (output['MemoFormat'] !== void(0)) {
output['parsed_memo_format'] = convertHexToString(output['MemoFormat']);
}
if (output['MemoData'] !== void(0)) {
// see if we can parse JSON
if (output['parsed_memo_format'] === 'json') {
try {
output['parsed_memo_data'] = JSON.parse(convertHexToString(output['MemoData']));
} catch(e) {
// fail, which is fine, we just won't add the memo_data field
}
} else if(output['parsed_memo_format'] === 'text') {
output['parsed_memo_data'] = convertHexToString(output['MemoData']);
}
}
so.read(1);
return output;
}
});
exports.serialize = exports.serialize_whatever = serialize; exports.serialize = exports.serialize_whatever = serialize;
function serialize(so, field_name, value) { function serialize(so, field_name, value) {
@@ -622,9 +730,15 @@ function serialize(so, field_name, value) {
STInt8.serialize(so, field_bits); STInt8.serialize(so, field_bits);
} }
// Get the serializer class (ST...) for a field based on the type bits. // Get the serializer class (ST...)
var serialized_object_type = exports[binformat.types[type_bits]]; var serialized_object_type;
//do something with val[keys] and val[keys[i]]; if (field_name === 'Memo' && typeof value === 'object') {
// for Memo we override the default behavior with our STMemo serializer
serialized_object_type = exports.STMemo;
} else {
// for a field based on the type bits.
serialized_object_type = exports[binformat.types[type_bits]];
}
try { try {
serialized_object_type.serialize(so, value); serialized_object_type.serialize(so, value);
@@ -645,18 +759,21 @@ function parse(so) {
type_bits = so.read(1)[0]; type_bits = so.read(1)[0];
} }
// Get the parser class (ST...) for a field based on the type bits.
var type = exports[binformat.types[type_bits]];
assert(type, 'Unknown type - header byte is 0x' + tag_byte.toString(16));
var field_bits = tag_byte & 0x0f; var field_bits = tag_byte & 0x0f;
var field_name = (field_bits === 0) var field_name = (field_bits === 0)
? field_name = binformat.fields[type_bits][so.read(1)[0]] ? field_name = binformat.fields[type_bits][so.read(1)[0]]
: field_name = binformat.fields[type_bits][field_bits]; : field_name = binformat.fields[type_bits][field_bits];
assert(field_name, 'Unknown field - header byte is 0x' + tag_byte.toString(16)); assert(field_name, 'Unknown field - header byte is 0x' + tag_byte.toString(16));
// Get the parser class (ST...) for a field based on the type bits.
var type = (field_name === 'Memo')
? exports.STMemo
: exports[binformat.types[type_bits]];
assert(type, 'Unknown type - header byte is 0x' + tag_byte.toString(16));
return [ field_name, type.parse(so) ]; //key, value return [ field_name, type.parse(so) ]; //key, value
}; };
@@ -678,18 +795,20 @@ function sort_fields(keys) {
var STObject = exports.Object = new SerializedType({ var STObject = exports.Object = new SerializedType({
serialize: function (so, val, no_marker) { serialize: function (so, val, no_marker) {
var keys = Object.keys(val); var keys = [];
// Ignore lowercase field names - they're non-serializable fields by Object.keys(val).forEach(function (key) {
// convention. // Ignore lowercase field names - they're non-serializable fields by
keys = keys.filter(function (key) { // convention.
return key[0] !== key[0].toLowerCase(); if (key[0] === key[0].toLowerCase()) {
}); return;
}
keys.forEach(function (key) {
if (typeof binformat.fieldsInverseMap[key] === 'undefined') { if (typeof binformat.fieldsInverseMap[key] === 'undefined') {
throw new Error('JSON contains unknown field: "' + key + '"'); throw new Error('JSON contains unknown field: "' + key + '"');
} }
keys.push(key);
}); });
// Sort fields // Sort fields

View File

@@ -801,47 +801,56 @@ Transaction.prototype.setFlags = function(flags) {
}; };
/** /**
* Add a Memo to transaction * Add a Memo to transaction.
* *
* @param {String} memoType * @param {String} memoType
* - describes what the data represents, needs to be valid ASCII * - describes what the data represents, needs to be valid ASCII
* * @param {String} memoFormat
* - describes what format the data is in, MIME type, needs to be valid ASCII
* @param {String} memoData * @param {String} memoData
* - data for the memo, can be any JS object. Any object other than string will * - data for the memo, can be any JS object. Any object other than string will
* be stringified (JSON) for transport * be stringified (JSON) for transport
*/ */
Transaction.prototype.addMemo = function(memoType, memoData) { Transaction.prototype.addMemo = function(memoType, memoFormat, memoData) {
if (typeof memoType === 'object') { if (typeof memoType === 'object') {
var opts = memoType; var opts = memoType;
memoType = opts.memoType; memoType = opts.memoType;
memoFormat = opts.memoFormat;
memoData = opts.memoData; memoData = opts.memoData;
} }
if (!/(undefined|string)/.test(typeof memoType)) { if (!/(undefined|string)/.test(typeof memoType)) {
throw new Error('MemoType must be a string'); throw new Error('MemoType must be a string');
} else if (!Transaction.ASCII_REGEX.test(memoType)) {
throw new Error('MemoType must be valid ASCII');
} }
if (!/(undefined|string)/.test(typeof memoData)) { if (!/(undefined|string)/.test(typeof memoFormat)) {
throw new Error('MemoData must be a string'); throw new Error('MemoFormat must be a string');
} else if (!Transaction.ASCII_REGEX.test(memoFormat)) {
throw new Error('MemoFormat must be valid ASCII');
} }
function toHex(str) { var memo = {};
return sjcl.codec.hex.fromBits(sjcl.codec.utf8String.toBits(str));
};
var memo = { };
if (memoType) { if (memoType) {
if (Transaction.MEMO_TYPES[memoType]) { if (Transaction.MEMO_TYPES[memoType]) {
//XXX Maybe in the future we want a schema validator for memo types //XXX Maybe in the future we want a schema validator for
//memo types
memo.MemoType = Transaction.MEMO_TYPES[memoType]; memo.MemoType = Transaction.MEMO_TYPES[memoType];
} else { } else {
memo.MemoType = toHex(memoType); memo.MemoType = memoType;
} }
} }
if (memoFormat) {
memo.MemoFormat = memoFormat;
}
if (memoData) { if (memoData) {
memo.MemoData = toHex(memoData); memo.MemoData = memoData;
} }
this.tx_json.Memos = (this.tx_json.Memos || []).concat({ Memo: memo }); this.tx_json.Memos = (this.tx_json.Memos || []).concat({ Memo: memo });

View File

@@ -1,8 +1,23 @@
var utils = require('./testutils'); var utils = require('./testutils');
var assert = require('assert'); var assert = require('assert');
var SerializedObject = utils.load_module('serializedobject').SerializedObject; var SerializedObject = utils.load_module('serializedobject').SerializedObject;
var sjcl = require('./../src/js/ripple/utils').sjcl;
// Shortcuts
var hex = sjcl.codec.hex;
var bytes = sjcl.codec.bytes;
var utf8 = sjcl.codec.utf8String;
describe('Serialized object', function() { describe('Serialized object', function() {
function convertStringToHex(string) {
return hex.fromBits(utf8.toBits(string)).toUpperCase();
}
function convertHexToString(hexString) {
return utf8.fromBits(hex.toBits(hexString));
}
describe('#from_json(v).to_json() == v', function(){ describe('#from_json(v).to_json() == v', function(){
it('outputs same as passed to from_json', function() { it('outputs same as passed to from_json', function() {
var input_json = { var input_json = {
@@ -35,6 +50,7 @@ describe('Serialized object', function() {
assert.deepEqual(input_json, output_json); assert.deepEqual(input_json, output_json);
}); });
}); });
describe('#from_json', function() { describe('#from_json', function() {
it('understands TransactionType as a Number', function() { it('understands TransactionType as a Number', function() {
var input_json = { var input_json = {
@@ -52,6 +68,7 @@ describe('Serialized object', function() {
assert.equal(0, input_json.TransactionType); assert.equal(0, input_json.TransactionType);
assert.equal("Payment", output_json.TransactionType); assert.equal("Payment", output_json.TransactionType);
}); });
it('understands LedgerEntryType as a Number', function() { it('understands LedgerEntryType as a Number', function() {
var input_json = { var input_json = {
// no, non required fields // no, non required fields
@@ -65,6 +82,7 @@ describe('Serialized object', function() {
assert.equal(100, input_json.LedgerEntryType); assert.equal(100, input_json.LedgerEntryType);
assert.equal("DirectoryNode", output_json.LedgerEntryType); assert.equal("DirectoryNode", output_json.LedgerEntryType);
}); });
describe('Format validation', function() { describe('Format validation', function() {
// Peercover actually had a problem submitting transactions without a `Fee` // Peercover actually had a problem submitting transactions without a `Fee`
// and rippled was only informing of "transaction is invalid" // and rippled was only informing of "transaction is invalid"
@@ -80,15 +98,231 @@ describe('Serialized object', function() {
}; };
assert.throws ( assert.throws (
function() { function() {
var output_json = SerializedObject.from_json(input_json); SerializedObject.from_json(input_json);
}, },
/Payment is missing fields: \["Fee"\]/ /Payment is missing fields: \["Fee"\]/
); );
}); });
}); });
}) describe('Memos', function() {
var input_json;
beforeEach(function() {
input_json = {
"Flags": 2147483648,
"TransactionType": "Payment",
"Account": "rhXzSyt1q9J8uiFXpK3qSugAAPJKXLtnrF",
"Amount": "1",
"Destination": "radqi6ppXFxVhJdjzaATRBxdrPcVTf1Ung",
"Sequence": 281,
"SigningPubKey": "03D642E6457B8AB4D140E2C66EB4C484FAFB1BF267CB578EC4815FE6CD06379C51",
"Fee": "12000",
"LastLedgerSequence": 10074214,
"TxnSignature": "304402201180636F2CE215CE97A29CD302618FAE60D63EBFC8903DE17A356E857A449C430220290F4A54F9DE4AC79034C8BEA5F1F8757F7505F1A6FF04D2E19B6D62E867256B"
};
});
it('should serialize and parse - full memo, all strings text/plain ', function() {
input_json.Memos = [
{
"Memo": {
"MemoType": "test",
"MemoFormat": "text",
"MemoData": "some data"
}
}
];
var so = SerializedObject.from_json(input_json).to_json();
input_json.Memos[0].Memo.parsed_memo_type = 'test';
input_json.Memos[0].Memo.parsed_memo_format = 'text';
input_json.Memos[0].Memo.parsed_memo_data = 'some data';
input_json.Memos[0].Memo.MemoType = convertStringToHex('test');
input_json.Memos[0].Memo.MemoFormat = convertStringToHex('text');
input_json.Memos[0].Memo.MemoData = convertStringToHex('some data');
assert.deepEqual(so, input_json);
});
it('should serialize and parse - full memo, all strings, invalid MemoFormat', function() {
input_json.Memos = [
{
"Memo": {
"MemoType": "test",
"MemoFormat": "application/json",
"MemoData": "some data"
}
}
];
var so = SerializedObject.from_json(input_json).to_json();
input_json.Memos[0].Memo.parsed_memo_type = 'test';
input_json.Memos[0].Memo.parsed_memo_format = 'application/json';
input_json.Memos[0].Memo.MemoType = convertStringToHex('test');
input_json.Memos[0].Memo.MemoFormat = convertStringToHex('application/json');
input_json.Memos[0].Memo.MemoData = convertStringToHex('some data');
assert.deepEqual(so, input_json);
assert.strictEqual(input_json.Memos[0].Memo.parsed_memo_data, void(0));
});
it('should throw an error - full memo, json data, invalid MemoFormat', function() {
input_json.Memos = [
{
"Memo": {
"MemoType": "test",
"MemoFormat": "text",
"MemoData": {
"string" : "some_string",
"boolean" : true
}
}
}
];
assert.throws(function() {
SerializedObject.from_json(input_json);
}, /^Error: MemoData can only be a JSON object with a valid json MemoFormat \(Memo\) \(Memos\)/);
});
it('should serialize and parse - full memo, json data, valid MemoFormat, ignored field', function() {
input_json.Memos = [
{
"Memo": {
"MemoType": "test",
"MemoFormat": "json",
"ignored" : "ignored",
"MemoData": {
"string" : "some_string",
"boolean" : true
}
}
}
];
var so = SerializedObject.from_json(input_json).to_json();
delete input_json.Memos[0].Memo.ignored;
input_json.Memos[0].Memo.parsed_memo_type = 'test';
input_json.Memos[0].Memo.parsed_memo_format = 'json';
input_json.Memos[0].Memo.parsed_memo_data = {
"string" : "some_string",
"boolean" : true
};
input_json.Memos[0].Memo.MemoType = convertStringToHex('test');
input_json.Memos[0].Memo.MemoFormat = convertStringToHex('json');
input_json.Memos[0].Memo.MemoData = convertStringToHex(JSON.stringify(
{
"string" : "some_string",
"boolean" : true
}
));
assert.deepEqual(so, input_json);
});
it('should serialize and parse - full memo, json data, valid MemoFormat', function() {
input_json.Memos = [
{
"Memo": {
"MemoType": "test",
"MemoFormat": "json",
"MemoData": {
"string" : "some_string",
"boolean" : true
}
}
}
];
var so = SerializedObject.from_json(input_json).to_json();
input_json.Memos[0].Memo.parsed_memo_type = 'test';
input_json.Memos[0].Memo.parsed_memo_format = 'json';
input_json.Memos[0].Memo.parsed_memo_data = {
"string" : "some_string",
"boolean" : true
};
input_json.Memos[0].Memo.MemoType = convertStringToHex('test');
input_json.Memos[0].Memo.MemoFormat = convertStringToHex('json');
input_json.Memos[0].Memo.MemoData = convertStringToHex(JSON.stringify(
{
"string" : "some_string",
"boolean" : true
}
));
assert.deepEqual(so, input_json);
});
it('should serialize and parse - full memo, json data, valid MemoFormat, integer', function() {
input_json.Memos = [
{
"Memo": {
"MemoType": "test",
"MemoFormat": "json",
"MemoData": 3
}
}
];
var so = SerializedObject.from_json(input_json).to_json();
input_json.Memos[0].Memo.parsed_memo_type = 'test';
input_json.Memos[0].Memo.parsed_memo_format = 'json';
input_json.Memos[0].Memo.parsed_memo_data = 3;
input_json.Memos[0].Memo.MemoType = convertStringToHex('test');
input_json.Memos[0].Memo.MemoFormat = convertStringToHex('json');
input_json.Memos[0].Memo.MemoData = convertStringToHex(JSON.parse(3));
assert.deepEqual(so, input_json);
});
it('should throw an error - invalid Memo field', function() {
input_json.Memos = [
{
"Memo": {
"MemoType": "test",
"MemoParty": "json",
"MemoData": 3
}
}
];
assert.throws(function() {
SerializedObject.from_json(input_json);
}, /^Error: JSON contains unknown field: "MemoParty" \(Memo\) \(Memos\)/);
});
it('should serialize json with memo - match hex output', function() {
var input_json = {
Flags: 2147483648,
TransactionType: 'Payment',
Account: 'rhXzSyt1q9J8uiFXpK3qSugAAPJKXLtnrF',
Amount: '1',
Destination: 'radqi6ppXFxVhJdjzaATRBxdrPcVTf1Ung',
Memos: [
{
Memo: {
MemoType: 'image'
}
}
],
Sequence: 294,
SigningPubKey: '03D642E6457B8AB4D140E2C66EB4C484FAFB1BF267CB578EC4815FE6CD06379C51',
Fee: '12000',
LastLedgerSequence: 10404607,
TxnSignature: '304402206B53EDFA6EFCF6FE5BA76C81BABB60A3B55E9DE8A1462DEDC5F387879575E498022015AE7B59AA49E735D7F2E252802C4406CD00689BCE5057C477FE979D38D2DAC9'
};
var serializedHex = '12000022800000002400000126201B009EC2FF614000000000000001684000000000002EE0732103D642E6457B8AB4D140E2C66EB4C484FAFB1BF267CB578EC4815FE6CD06379C517446304402206B53EDFA6EFCF6FE5BA76C81BABB60A3B55E9DE8A1462DEDC5F387879575E498022015AE7B59AA49E735D7F2E252802C4406CD00689BCE5057C477FE979D38D2DAC9811426C4CFB3BD05A9AA23936F2E81634C66A9820C9483143DD06317D19C6110CAFF150AE528F58843BE2CA1F9EA7C05696D616765E1F1';
assert.strictEqual(SerializedObject.from_json(input_json).to_hex(), serializedHex);
});
});
});
}); });
// vim:sw=2:sts=2:ts=8:et
// vim:sw=2:sts=2:ts=8:et

View File

@@ -1051,26 +1051,84 @@ describe('Transaction', function() {
var transaction = new Transaction(); var transaction = new Transaction();
transaction.tx_json.TransactionType = 'Payment'; transaction.tx_json.TransactionType = 'Payment';
transaction.addMemo('testkey', 'testvalue'); var memoType = 'message';
transaction.addMemo('testkey2', 'testvalue2'); var memoFormat = 'application/json';
transaction.addMemo('testkey3'); var memoData = {
transaction.addMemo(void(0), 'testvalue4'); string: 'value',
bool: true,
integer: 1
};
transaction.addMemo(memoType, memoFormat, memoData);
var expected = [
{
Memo:
{
MemoType: memoType,
MemoFormat: memoFormat,
MemoData: memoData
}
}
];
assert.deepEqual(transaction.tx_json.Memos, expected);
});
it('Add Memo - by object', function() {
var transaction = new Transaction();
transaction.tx_json.TransactionType = 'Payment';
var memo = {
memoType: 'type',
memoData: 'data'
};
transaction.addMemo(memo);
var expected = [
{
Memo: {
MemoType: memo.memoType,
MemoData: memo.memoData
}
}
];
assert.deepEqual(transaction.tx_json.Memos, expected);
});
it('Add Memos', function() {
var transaction = new Transaction();
transaction.tx_json.TransactionType = 'Payment';
transaction.addMemo('testkey', void(0), 'testvalue');
transaction.addMemo('testkey2', void(0), 'testvalue2');
transaction.addMemo('testkey3', 'text/html');
transaction.addMemo(void(0), void(0), 'testvalue4');
transaction.addMemo('testkey4', 'text/html', '<html>');
var expected = [ var expected = [
{ Memo: { { Memo: {
MemoType: new Buffer('testkey').toString('hex'), MemoType: 'testkey',
MemoData: new Buffer('testvalue').toString('hex') MemoData: 'testvalue'
}}, }},
{ Memo: { { Memo: {
MemoType: new Buffer('testkey2').toString('hex'), MemoType: 'testkey2',
MemoData: new Buffer('testvalue2').toString('hex') MemoData: 'testvalue2'
}}, }},
{ Memo: { { Memo: {
MemoType: new Buffer('testkey3').toString('hex') MemoType: 'testkey3',
MemoFormat: 'text/html'
}}, }},
{ Memo: { { Memo: {
MemoData: new Buffer('testvalue4').toString('hex') MemoData: 'testvalue4'
} } }},
{ Memo: {
MemoType: 'testkey4',
MemoFormat: 'text/html',
MemoData: '<html>'
}}
]; ];
assert.deepEqual(transaction.tx_json.Memos, expected); assert.deepEqual(transaction.tx_json.Memos, expected);
@@ -1085,13 +1143,76 @@ describe('Transaction', function() {
}, /^Error: MemoType must be a string$/); }, /^Error: MemoType must be a string$/);
}); });
it('Add Memo - invalid MemoData', function() { it('Add Memo - invalid ASCII MemoType', function() {
var transaction = new Transaction(); var transaction = new Transaction();
transaction.tx_json.TransactionType = 'Payment'; transaction.tx_json.TransactionType = 'Payment';
assert.throws(function() { assert.throws(function() {
transaction.addMemo('key', 1); transaction.addMemo('한국어');
}, /^Error: MemoData must be a string$/); }, /^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() { it('Construct AccountSet transaction', function() {
@@ -1268,7 +1389,7 @@ describe('Transaction', function() {
var bid = '1/USD/rsLEU1TPdCJPPysqhWYw9jD97xtG5WqSJm'; var bid = '1/USD/rsLEU1TPdCJPPysqhWYw9jD97xtG5WqSJm';
var ask = '1/EUR/rsLEU1TPdCJPPysqhWYw9jD97xtG5WqSJm'; var ask = '1/EUR/rsLEU1TPdCJPPysqhWYw9jD97xtG5WqSJm';
assert.throws(function() { 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() { it('Construct SetRegularKey transaction - invalid account', function() {
assert.throws(function() { assert.throws(function() {
var transaction = new Transaction().setRegularKey('xrsLEU1TPdCJPPysqhWYw9jD97xtG5WqSJm', 'r36xtKNKR43SeXnGn7kN4r4JdQzcrkqpWe'); new Transaction().setRegularKey('xrsLEU1TPdCJPPysqhWYw9jD97xtG5WqSJm', 'r36xtKNKR43SeXnGn7kN4r4JdQzcrkqpWe');
}); });
}); });
it('Construct SetRegularKey transaction - invalid regularKey', function() { it('Construct SetRegularKey transaction - invalid regularKey', function() {
assert.throws(function() { assert.throws(function() {
var transaction = new Transaction().setRegularKey('rsLEU1TPdCJPPysqhWYw9jD97xtG5WqSJm', 'xr36xtKNKR43SeXnGn7kN4r4JdQzcrkqpWe'); new Transaction().setRegularKey('rsLEU1TPdCJPPysqhWYw9jD97xtG5WqSJm', 'xr36xtKNKR43SeXnGn7kN4r4JdQzcrkqpWe');
}); });
}); });