diff --git a/src/transaction/sign.ts b/src/transaction/sign.ts index cd80a27a..ae90fb8d 100644 --- a/src/transaction/sign.ts +++ b/src/transaction/sign.ts @@ -60,6 +60,72 @@ function signWithKeypair( } } +/** + * Compares two objects and creates a diff. + * + * @param a An object to compare. + * @param b The other object to compare with. + * + * @returns An object containing the differences between the two objects. + */ +function objectDiff(a: object, b: object): object { + const diffs = {} + + // Compare two items and push non-matches to object + const compare = function (i1: any, i2: any, k: string): void { + const type1 = Object.prototype.toString.call(i1) + const type2 = Object.prototype.toString.call(i2) + if (type2 === '[object Undefined]') { + diffs[k] = null // Indicate that the item has been removed + return + } + if (type1 !== type2) { + diffs[k] = i2 // Indicate that the item has changed types + return + } + if (type1 === '[object Object]') { + const objDiff = objectDiff(i1, i2) + if (Object.keys(objDiff).length > 0) { + diffs[k] = objDiff + } + return + } + if (type1 === '[object Array]') { + if (!isEqual(i1, i2)) { + diffs[k] = i2 // If arrays do not match, add second item to diffs + } + return + } + if (type1 === '[object Function]') { + if (i1.toString() !== i2.toString()) { + diffs[k] = i2 // If functions differ, add second one to diffs + } + return + } + if (i1 !== i2) { + diffs[k] = i2 + } + } + + // Check items in first object + for (const key in a) { + if (a.hasOwnProperty(key)) { + compare(a[key], b[key], key) + } + } + + // Get items that are in the second object but not the first + for (const key in b) { + if (b.hasOwnProperty(key)) { + if (!a[key] && a[key] !== b[key]) { + diffs[key] = b[key] + } + } + } + + return diffs +} + /** * Decode a serialized transaction, remove the fields that are added during the signing process, * and verify that it matches the transaction prior to signing. @@ -93,11 +159,12 @@ function checkTxSerialization(serialized: string, tx: utils.TransactionJSON): vo if (!isEqual(decoded, tx)) { const error = new utils.common.errors.ValidationError( - 'Serialized transaction does not match original txJSON' + 'Serialized transaction does not match original txJSON. See `error.data`' ) error.data = { decoded, - tx + tx, + diff: objectDiff(tx, decoded) } throw error } diff --git a/test/api-test.js b/test/api-test.js index 4d070c8e..b20f7a6b 100644 --- a/test/api-test.js +++ b/test/api-test.js @@ -2679,6 +2679,37 @@ describe('RippleAPI', function () { ); }); + it('sign - throws when encoded tx does not match decoded tx - prepared order', async function () { + const order = { + direction: 'sell', + quantity: { + currency: 'USD', + counterparty: 'rvYAfWj5gh67oV6fW32ZzP3Aw4Eubs59B', + value: '3.140000' + }, + totalPrice: { + currency: 'XRP', + value: '31415' + } + }; + const prepared = await this.api.prepareOrder('r9cZA1mLK5R5Am25ArfXFmqgNwjZgnfk59', order, { + sequence: 123 + }); + const secret = 'shsWGZcmZz6YsWWmcnpfr6fLTdtFV'; + try { + this.api.sign(prepared.txJSON, secret); + return Promise.reject(new Error('api.sign should have thrown')); + } catch (error) { + assert.equal(error.name, 'ValidationError'); + assert.equal(error.message, 'Serialized transaction does not match original txJSON. See `error.data`'); + assert.deepEqual(error.data.diff, { + TakerGets: { + value: '3.14' + } + }); + } + }); + it('sign - throws when encoded tx does not match decoded tx - AccountSet', function () { const secret = 'shsWGZcmZz6YsWWmcnpfr6fLTdtFV'; const request = {