diff --git a/Gruntfile.js b/Gruntfile.js index 462a485f..a1b1bf5e 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -41,6 +41,7 @@ module.exports = function(grunt) { "src/js/sjcl-custom/sjcl-extramath.js", "src/js/sjcl-custom/sjcl-montgomery.js", "src/js/sjcl-custom/sjcl-validecc.js", + "src/js/sjcl-custom/sjcl-ecdsa-canonical.js", "src/js/sjcl-custom/sjcl-ecdsa-der.js", "src/js/sjcl-custom/sjcl-jacobi.js" ], diff --git a/README.md b/README.md index 9eb999f4..695bee1b 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,7 @@ #The Ripple JavaScript Library +[![NPM](https://nodei.co/npm/ripple-lib.png)](https://www.npmjs.org/package/ripple-lib) + `ripple-lib` connects to the Ripple network via the WebSocket protocol and runs in Node.js as well as in the browser. **Use ripple-lib for** diff --git a/build/sjcl.js b/build/sjcl.js index 8e2c9a2a..b2c436dc 100644 --- a/build/sjcl.js +++ b/build/sjcl.js @@ -4043,6 +4043,24 @@ sjcl.ecc.ecdsa.publicKey.prototype = { } }; +sjcl.ecc.ecdsa.secretKey.prototype.canonicalizeSignature = function(rs) { + var w = sjcl.bitArray, + R = this._curve.r, + l = R.bitLength(); + + var r = sjcl.bn.fromBits(w.bitSlice(rs,0,l)), + s = sjcl.bn.fromBits(w.bitSlice(rs,l,2*l)); + + // For a canonical signature we want the lower of two possible values for s + // 0 < s <= n/2 + if (!R.copy().halveM().greaterEquals(s)) { + s = R.sub(s); + } + + return w.concat(r.toBits(l), s.toBits(l)); +}; + + sjcl.ecc.ecdsa.secretKey.prototype.signDER = function(hash, paranoia) { return this.encodeDER(this.sign(hash, paranoia)); }; diff --git a/src/js/ripple/amount.js b/src/js/ripple/amount.js index d5dfc5ba..1d4440ad 100644 --- a/src/js/ripple/amount.js +++ b/src/js/ripple/amount.js @@ -71,8 +71,8 @@ Amount.from_json = function (j) { return (new Amount()).parse_json(j); }; -Amount.from_quality = function (q, c, i) { - return (new Amount()).parse_quality(q, c, i); +Amount.from_quality = function (quality, currency, issuer, opts) { + return (new Amount()).parse_quality(quality, currency, issuer, opts); }; Amount.from_human = function (j, opts) { @@ -159,6 +159,29 @@ Amount.prototype.add = function (v) { return result; }; +/** + * Turn this amount into its inverse. + * + * @private + */ +Amount.prototype._invert = function () { + this._value = consts.bi_1e32.divide(this._value); + this._offset = -32 - this._offset; + this.canonicalize(); + + return this; +}; + +/** + * Return the inverse of this amount. + * + * @return {Amount} New Amount object with same currency and issuer, but the + * inverse of the value. + */ +Amount.prototype.invert = function () { + return this.copy()._invert(); +}; + Amount.prototype.canonicalize = function () { if (!(this._value instanceof BigInteger)) { // NaN. @@ -349,9 +372,14 @@ Amount.prototype.divide = function (d) { * * @this {Amount} The numerator (top half) of the fraction. * @param {Amount} denominator The denominator (bottom half) of the fraction. + * @param opts Options for the calculation. + * @param opts.reference_date {Date|Number} Date based on which demurrage/interest + * should be applied. Can be given as JavaScript Date or int for Ripple epoch. * @return {Amount} The resulting ratio. Unit will be the same as numerator. */ -Amount.prototype.ratio_human = function (denominator) { +Amount.prototype.ratio_human = function (denominator, opts) { + opts = opts || {}; + if (typeof denominator === 'number' && parseInt(denominator, 10) === denominator) { // Special handling of integer arguments denominator = Amount.from_json('' + denominator + '.0'); @@ -367,6 +395,14 @@ Amount.prototype.ratio_human = function (denominator) { return Amount.NaN(); } + // Apply interest/demurrage + // + // We only need to apply it to the second factor, because the currency unit of + // the first factor will carry over into the result. + if (opts.reference_date) { + denominator = denominator.applyInterest(opts.reference_date); + } + // Special case: The denominator is a native (XRP) amount. // // In that case, it's going to be expressed as base units (1 XRP = @@ -402,9 +438,14 @@ Amount.prototype.ratio_human = function (denominator) { * * @this {Amount} The first factor of the product. * @param {Amount} factor The second factor of the product. + * @param opts Options for the calculation. + * @param opts.reference_date {Date|Number} Date based on which demurrage/interest + * should be applied. Can be given as JavaScript Date or int for Ripple epoch. * @return {Amount} The product. Unit will be the same as the first factor. */ -Amount.prototype.product_human = function (factor) { +Amount.prototype.product_human = function (factor, opts) { + opts = opts || {}; + if (typeof factor === 'number' && parseInt(factor, 10) === factor) { // Special handling of integer arguments factor = Amount.from_json(String(factor) + '.0'); @@ -417,6 +458,14 @@ Amount.prototype.product_human = function (factor) { return Amount.NaN(); } + // Apply interest/demurrage + // + // We only need to apply it to the second factor, because the currency unit of + // the first factor will carry over into the result. + if (opts.reference_date) { + factor = factor.applyInterest(opts.reference_date); + } + var product = this.multiply(factor); // Special case: The second factor is a native (XRP) amount expressed as base @@ -618,17 +667,81 @@ Amount.prototype.parse_issuer = function (issuer) { return this; }; -// --> h: 8 hex bytes quality or 32 hex bytes directory index. -Amount.prototype.parse_quality = function (q, c, i) { +/** + * Decode a price from a BookDirectory index. + * + * BookDirectory ledger entries each encode the offer price in their index. This + * method can decode that information and populate an Amount object with it. + * + * It is possible not to provide a currency or issuer, but be aware that Amount + * objects behave differently based on the currency, so you may get incorrect + * results. + * + * Prices involving demurraging currencies are tricky, since they depend on the + * base and counter currencies. + * + * @param quality {String} 8 hex bytes quality or 32 hex bytes BookDirectory + * index. + * @param counterCurrency {Currency|String} Currency of the resulting Amount + * object. + * @param counterIssuer {Issuer|String} Issuer of the resulting Amount object. + * @param opts Additional options + * @param opts.inverse {Boolean} If true, return the inverse of the price + * encoded in the quality. + * @param opts.base_currency {Currency|String} The other currency. This plays a + * role with interest-bearing or demurrage currencies. In that case the + * demurrage has to be applied when the quality is decoded, otherwise the + * price will be false. + * @param opts.reference_date {Date|Number} Date based on which demurrage/interest + * should be applied. Can be given as JavaScript Date or int for Ripple epoch. + * @param opts.xrp_as_drops {Boolean} Whether XRP amount should be treated as + * drops. When the base currency is XRP, the quality is calculated in drops. + * For human use however, we want to think of 1000000 drops as 1 XRP and + * prices as per-XRP instead of per-drop. + */ +Amount.prototype.parse_quality = function (quality, counterCurrency, counterIssuer, opts) +{ + opts = opts || {}; + + var baseCurrency = Currency.from_json(opts.base_currency); + this._is_negative = false; - this._value = new BigInteger(q.substring(q.length-14), 16); - this._offset = parseInt(q.substring(q.length-16, q.length-14), 16)-100; - this._currency = Currency.from_json(c); - this._issuer = UInt160.from_json(i); + this._value = new BigInteger(quality.substring(quality.length-14), 16); + this._offset = parseInt(quality.substring(quality.length-16, quality.length-14), 16)-100; + this._currency = Currency.from_json(counterCurrency); + this._issuer = UInt160.from_json(counterIssuer); this._is_native = this._currency.is_native(); + // Correct offset if xrp_as_drops option is not set and base currency is XRP + if (!opts.xrp_as_drops && + baseCurrency.is_valid() && + baseCurrency.is_native()) { + if (opts.inverse) { + this._offset -= 6; + } else { + this._offset += 6; + } + } + + if (opts.inverse) { + this._invert(); + } + this.canonicalize(); + if (opts.reference_date && baseCurrency.is_valid() && baseCurrency.has_interest()) { + var interest = baseCurrency.get_interest_at(opts.reference_date); + + // XXX If we had better math utilities, we wouldn't need this hack. + var interestTempAmount = Amount.from_json(""+interest+"/1/1"); + + if (interestTempAmount.is_valid()) { + var v = this.divide(interestTempAmount); + this._value = v._value; + this._offset = v._offset; + } + } + return this; } @@ -850,6 +963,39 @@ Amount.prototype.to_text = function (allow_nan) { return result; }; +/** + * Calculate present value based on currency and a reference date. + * + * This only affects demurraging and interest-bearing currencies. + * + * User should not store amount objects after the interest is applied. This is + * intended by display functions such as toHuman(). + * + * @param referenceDate {Date|Number} Date based on which demurrage/interest + * should be applied. Can be given as JavaScript Date or int for Ripple epoch. + * @return {Amount} The amount with interest applied. + */ +Amount.prototype.applyInterest = function (referenceDate) { + if (this._currency.has_interest()) { + var interest = this._currency.get_interest_at(referenceDate); + + // XXX Because the Amount parsing routines don't support some of the things + // that JavaScript can output when casting a float to a string, the + // following call sometimes does not produce a valid Amount. + // + // The correct way to solve this is probably to switch to a proper + // BigDecimal for our internal representation and then use that across + // the board instead of instantiating these dummy Amount objects. + var interestTempAmount = Amount.from_json(""+interest+"/1/1"); + + if (interestTempAmount.is_valid()) { + return this.multiply(interestTempAmount); + } + } else { + return this; + } +}; + /** * Format only value in a human-readable format. * @@ -885,21 +1031,8 @@ Amount.prototype.to_human = function (opts) { // Apply demurrage/interest var ref = this; - if (opts.reference_date && this._currency.has_interest()) { - var interest = this._currency.get_interest_at(opts.reference_date); - - // XXX Because the Amount parsing routines don't support some of the things - // that JavaScript can output when casting a float to a string, the - // following call sometimes does not produce a valid Amount. - // - // The correct way to solve this is probably to switch to a proper - // BigDecimal for our internal representation and then use that across - // the board instead of instantiating these dummy Amount objects. - var interestTempAmount = Amount.from_json(""+interest+"/1/1"); - - if (interestTempAmount.is_valid()) { - ref = this.multiply(interestTempAmount); - } + if (opts.reference_date) { + ref = this.applyInterest(opts.reference_date); } var order = ref._is_native ? consts.xns_precision : -ref._offset; diff --git a/src/js/ripple/keypair.js b/src/js/ripple/keypair.js index db82e79d..518ae70d 100644 --- a/src/js/ripple/keypair.js +++ b/src/js/ripple/keypair.js @@ -89,8 +89,10 @@ KeyPair.prototype.get_address = function () { }; KeyPair.prototype.sign = function (hash) { - var hash = UInt256.from_json(hash); - return this._secret.signDER(hash.to_bits(), 0); + hash = UInt256.from_json(hash); + var sig = this._secret.sign(hash.to_bits(), 0); + sig = this._secret.canonicalizeSignature(sig); + return this._secret.encodeDER(sig); }; exports.KeyPair = KeyPair; diff --git a/src/js/ripple/orderbook.js b/src/js/ripple/orderbook.js index d51f3e72..d667289b 100644 --- a/src/js/ripple/orderbook.js +++ b/src/js/ripple/orderbook.js @@ -225,11 +225,12 @@ OrderBook.prototype.notify = function (message) { break; case 'CreatedNode': - var price = Amount.from_json(an.fields.TakerPays).ratio_human(an.fields.TakerGets); + // XXX Should use Amount#from_quality + var price = Amount.from_json(an.fields.TakerPays).ratio_human(an.fields.TakerGets, {reference_date: new Date()}); for (i = 0, l = self._offers.length; i < l; i++) { offer = self._offers[i]; - var priceItem = Amount.from_json(offer.TakerPays).ratio_human(offer.TakerGets); + var priceItem = Amount.from_json(offer.TakerPays).ratio_human(offer.TakerGets, {reference_date: new Date()}); if (price.compareTo(priceItem) <= 0) { var obj = an.fields; diff --git a/src/js/ripple/server.js b/src/js/ripple/server.js index 1b393503..f7846e86 100644 --- a/src/js/ripple/server.js +++ b/src/js/ripple/server.js @@ -36,8 +36,17 @@ function Server(remote, opts) { throw new Error('Server host is malformed, use "host" and "port" server configuration'); } - if (typeof opts.port !== 'number') { - throw new TypeError('Server configuration "port" is not a Number'); + // We want to allow integer strings as valid port numbers for backward + // compatibility. + if (typeof opts.port === 'string') { + opts.port = parseFloat(opts.port); + } + + if (typeof opts.port !== 'number' || + opts.port >>> 0 !== parseFloat(opts.port) || // is integer? + opts.port < 1 || + opts.port > 65535) { + throw new TypeError('Server "port" must be an integer in range 1-65535'); } if (typeof opts.secure !== 'boolean') { diff --git a/src/js/ripple/transaction.js b/src/js/ripple/transaction.js index 58dcc131..f50b74ab 100644 --- a/src/js/ripple/transaction.js +++ b/src/js/ripple/transaction.js @@ -73,22 +73,19 @@ function Transaction(remote) { // Index at which transaction was submitted this.submitIndex = void(0); + this.canonical = true; + // We aren't clever enough to eschew preventative measures so we keep an array // of all submitted transactionIDs (which can change due to load_factor // effecting the Fee amount). This should be populated with a transactionID // any time it goes on the network - this.submittedIDs = [ ] + this.submittedIDs = [ ]; function finalize(message) { - if (self.result) { - self.result.ledger_index = message.ledger_index; - self.result.ledger_hash = message.ledger_hash; - } else { - self.result = message; - self.result.tx_json = self.tx_json; + if (!self.finalized) { + self.finalized = true; + self.emit('cleanup', message); } - - self.emit('cleanup', message); }; this.once('success', function(message) { @@ -120,6 +117,11 @@ Transaction.fee_units = { }; Transaction.flags = { + // Universal flags can apply to any transaction type + Universal: { + FullyCanonicalSig: 0x80000000 + }, + AccountSet: { RequireDestTag: 0x00010000, OptionalDestTag: 0x00020000, @@ -278,6 +280,15 @@ Transaction.prototype.complete = function() { this.tx_json.SigningPubKey = key.to_hex_pub(); } + // Set canonical flag - this enables canonicalized signature checking + if (this.canonical) { + this.tx_json.Flags |= Transaction.flags.Universal.FullyCanonicalSig; + + // JavaScript converts operands to 32-bit signed ints before doing bitwise + // operations. We need to convert it back to an unsigned int. + this.tx_json.Flags = this.tx_json.Flags >>> 0; + } + return this.tx_json; }; diff --git a/src/js/sjcl-custom/sjcl-ecdsa-canonical.js b/src/js/sjcl-custom/sjcl-ecdsa-canonical.js new file mode 100644 index 00000000..d56c5114 --- /dev/null +++ b/src/js/sjcl-custom/sjcl-ecdsa-canonical.js @@ -0,0 +1,17 @@ +sjcl.ecc.ecdsa.secretKey.prototype.canonicalizeSignature = function(rs) { + var w = sjcl.bitArray, + R = this._curve.r, + l = R.bitLength(); + + var r = sjcl.bn.fromBits(w.bitSlice(rs,0,l)), + s = sjcl.bn.fromBits(w.bitSlice(rs,l,2*l)); + + // For a canonical signature we want the lower of two possible values for s + // 0 < s <= n/2 + if (!R.copy().halveM().greaterEquals(s)) { + s = R.sub(s); + } + + return w.concat(r.toBits(l), s.toBits(l)); +}; + diff --git a/test/amount-test.js b/test/amount-test.js index 51c40282..0630470c 100644 --- a/test/amount-test.js +++ b/test/amount-test.js @@ -431,4 +431,130 @@ describe('Amount', function() { assert.strictEqual(a.not_equals_why(b), 'Native mismatch.'); }); }); + + describe('product_human', function() { + it('Multiply 0 XRP with 0 XRP', function () { + assert.strictEqual('0/XRP', Amount.from_json('0').product_human(Amount.from_json('0')).to_text_full()); + }); + it('Multiply 0 USD with 0 XRP', function () { + assert.strictEqual('0/USD/rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh', Amount.from_json('0/USD/rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh').product_human(Amount.from_json('0')).to_text_full()); + }); + it('Multiply 0 XRP with 0 USD', function () { + assert.strictEqual('0/XRP', Amount.from_json('0').product_human(Amount.from_json('0/USD/rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh')).to_text_full()); + }); + it('Multiply 1 XRP with 0 XRP', function () { + assert.strictEqual('0/XRP', Amount.from_json('1').product_human(Amount.from_json('0')).to_text_full()); + }); + it('Multiply 1 USD with 0 XRP', function () { + assert.strictEqual('0/USD/rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh', Amount.from_json('1/USD/rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh').product_human(Amount.from_json('0')).to_text_full()); + }); + it('Multiply 1 XRP with 0 USD', function () { + assert.strictEqual('0/XRP', Amount.from_json('1').product_human(Amount.from_json('0/USD/rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh')).to_text_full()); + }); + it('Multiply 0 XRP with 1 XRP', function () { + assert.strictEqual('0/XRP', Amount.from_json('0').product_human(Amount.from_json('1')).to_text_full()); + }); + it('Multiply 0 USD with 1 XRP', function () { + assert.strictEqual('0/USD/rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh', Amount.from_json('0/USD/rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh').product_human(Amount.from_json('1')).to_text_full()); + }); + it('Multiply 0 XRP with 1 USD', function () { + assert.strictEqual('0/XRP', Amount.from_json('0').product_human(Amount.from_json('1/USD/rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh')).to_text_full()); + }); + it('Multiply XRP with USD', function () { + assert.equal('0.002/XRP', Amount.from_json('200').product_human(Amount.from_json('10/USD/rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh')).to_text_full()); + }); + it('Multiply XRP with USD', function () { + assert.strictEqual('0.2/XRP', Amount.from_json('20000').product_human(Amount.from_json('10/USD/rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh')).to_text_full()); + }); + it('Multiply XRP with USD', function () { + assert.strictEqual('20/XRP', Amount.from_json('2000000').product_human(Amount.from_json('10/USD/rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh')).to_text_full()); + }); + it('Multiply XRP with USD, neg', function () { + assert.strictEqual('-0.002/XRP', Amount.from_json('200').product_human(Amount.from_json('-10/USD/rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh')).to_text_full()); + }); + it('Multiply XRP with USD, neg, frac', function () { + assert.strictEqual('-0.222/XRP', Amount.from_json('-6000').product_human(Amount.from_json('37/USD/rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh')).to_text_full()); + }); + it('Multiply USD with USD', function () { + assert.strictEqual('20000/USD/rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh', Amount.from_json('2000/USD/rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh').product_human(Amount.from_json('10/USD/rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh')).to_text_full()); + }); + it('Multiply USD with USD', function () { + assert.strictEqual('200000000000/USD/rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh', Amount.from_json('2000000/USD/rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh').product_human(Amount.from_json('100000/USD/rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh')).to_text_full()); + }); + it('Multiply EUR with USD, result < 1', function () { + assert.strictEqual('100000/EUR/rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh', Amount.from_json('100/EUR/rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh').product_human(Amount.from_json('1000/USD/rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh')).to_text_full()); + }); + it('Multiply EUR with USD, neg', function () { + assert.strictEqual(Amount.from_json('-24000/EUR/rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh').product_human(Amount.from_json('2000/USD/rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh')).to_text_full(), '-48000000/EUR/rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh'); + }); + it('Multiply EUR with USD, neg, <1', function () { + assert.strictEqual(Amount.from_json('0.1/EUR/rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh').product_human(Amount.from_json('-1000/USD/rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh')).to_text_full(), '-100/EUR/rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh'); + }); + it('Multiply EUR with XRP, factor < 1', function () { + assert.strictEqual(Amount.from_json('0.05/EUR/rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh').product_human(Amount.from_json('2000')).to_text_full(), '0.0001/EUR/rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh'); + }); + it('Multiply EUR with XRP, neg', function () { + assert.strictEqual(Amount.from_json('-100/EUR/rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh').product_human(Amount.from_json('5')).to_text_full(), '-0.0005/EUR/rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh'); + }); + it('Multiply EUR with XRP, neg, <1', function () { + assert.strictEqual(Amount.from_json('-0.05/EUR/rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh').product_human(Amount.from_json('2000')).to_text_full(), '-0.0001/EUR/rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh'); + }); + it('Multiply XRP with XRP', function () { + assert.strictEqual(Amount.from_json('10000000').product_human(Amount.from_json('10')).to_text_full(), '0.0001/XRP'); + }); + it('Multiply USD with XAU (dem)', function () { + assert.strictEqual(Amount.from_json('2000/USD/rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh').product_human(Amount.from_json('10/015841551A748AD2C1F76FF6ECB0CCCD00000000/rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh'), {reference_date: 443845330 + 31535000}).to_text_full(), '19900.00316303882/USD/rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh'); + }); + }); + + describe('ratio_human', function() { + it('Divide USD by XAU (dem)', function () { + assert.strictEqual(Amount.from_json('2000/USD/rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh').ratio_human(Amount.from_json('10/015841551A748AD2C1F76FF6ECB0CCCD00000000/rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh'), {reference_date: 443845330 + 31535000}).to_text_full(), '201.0049931765529/USD/rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh'); + }); + }); + + describe('_invert', function() { + it('Invert 1', function () { + assert.strictEqual(Amount.from_json('1/USD/rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh').invert().to_text_full(), '1/USD/rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh'); + }); + it('Invert 20', function () { + assert.strictEqual(Amount.from_json('20/USD/rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh').invert().to_text_full(), '0.05/USD/rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh'); + }); + it('Invert 0.02', function () { + assert.strictEqual(Amount.from_json('0.02/USD/rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh').invert().to_text_full(), '50/USD/rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh'); + }); + }); + + describe('from_quality', function() { + it('BTC/XRP', function () { + assert.strictEqual(Amount.from_quality('7B73A610A009249B0CC0D4311E8BA7927B5A34D86634581C5F0FF9FF678E1000', 'XRP', NaN, {base_currency: 'BTC'}).to_text_full(), '44,970/XRP'); + }); + it('BTC/XRP inverse', function () { + assert.strictEqual(Amount.from_quality('37AAC93D336021AE94310D0430FFA090F7137C97D473488C4A0918D0DEF8624E', 'XRP', NaN, {inverse: true, base_currency: 'BTC'}).to_text_full(), '39,053.954453/XRP'); + }); + it('XRP/USD', function () { + assert.strictEqual(Amount.from_quality('DFA3B6DDAB58C7E8E5D944E736DA4B7046C30E4F460FD9DE4D05DCAA8FE12000', 'USD', 'rvYAfWj5gh67oV6fW32ZzP3Aw4Eubs59B', {base_currency: 'XRP'}).to_text_full(), '0.0165/USD/rvYAfWj5gh67oV6fW32ZzP3Aw4Eubs59B'); + }); + it('XRP/USD inverse', function () { + assert.strictEqual(Amount.from_quality('4627DFFCFF8B5A265EDBD8AE8C14A52325DBFEDAF4F5C32E5C22A840E27DCA9B', 'USD', 'rvYAfWj5gh67oV6fW32ZzP3Aw4Eubs59B', {inverse: true, base_currency: 'XRP'}).to_text_full(), '0.010251/USD/rvYAfWj5gh67oV6fW32ZzP3Aw4Eubs59B'); + }); + it('BTC/USD', function () { + assert.strictEqual(Amount.from_quality('6EAB7C172DEFA430DBFAD120FDC373B5F5AF8B191649EC9858038D7EA4C68000', 'USD', 'rvYAfWj5gh67oV6fW32ZzP3Aw4Eubs59B', {base_currency: 'BTC'}).to_text_full(), '1000/USD/rvYAfWj5gh67oV6fW32ZzP3Aw4Eubs59B'); + }); + it('BTC/USD inverse', function () { + assert.strictEqual(Amount.from_quality('20294C923E80A51B487EB9547B3835FD483748B170D2D0A455071AFD498D0000', 'USD', 'rvYAfWj5gh67oV6fW32ZzP3Aw4Eubs59B', {inverse: true, base_currency: 'BTC'}).to_text_full(), '0.5/USD/rvYAfWj5gh67oV6fW32ZzP3Aw4Eubs59B'); + }); + it('XAU(dem)/XRP', function () { + assert.strictEqual(Amount.from_quality('587322CCBDE0ABD01704769A73A077C32FB39057D813D4165F1FF973CAF997EF', 'XRP', NaN, {base_currency: '015841551A748AD2C1F76FF6ECB0CCCD00000000', reference_date: 443845330 + 31535000}).to_text_full(), '90,452.246928/XRP'); + }); + it('XAU(dem)/XRP inverse', function () { + assert.strictEqual(Amount.from_quality('F72C7A9EAE4A45ED1FB547AD037D07B9B965C6E662BEBAFA4A03F2A976804235', 'XRP', NaN, {inverse: true, base_currency: '015841551A748AD2C1F76FF6ECB0CCCD00000000', reference_date: 443845330 + 31535000}).to_text_full(), '90,442.196677/XRP'); + }); + it('USD/XAU(dem)', function () { + assert.strictEqual(Amount.from_quality('4743E58E44974B325D42FD2BB683A6E36950F350EE46DD3A521B644B99782F5F', '015841551A748AD2C1F76FF6ECB0CCCD00000000', 'rUyPiNcSFFj6uMR2gEaD8jUerQ59G1qvwN', {base_currency: 'USD', reference_date: 443845330 + 31535000}).to_text_full(), '0.007710100231303007/015841551A748AD2C1F76FF6ECB0CCCD00000000/rUyPiNcSFFj6uMR2gEaD8jUerQ59G1qvwN'); + }); + it('USD/XAU(dem) inverse', function () { + assert.strictEqual(Amount.from_quality('CDFD3AFB2F8C5DBEF75B081F7C957FF5509563266F28F36C5704A0FB0BAD8800', '015841551A748AD2C1F76FF6ECB0CCCD00000000', 'rUyPiNcSFFj6uMR2gEaD8jUerQ59G1qvwN', {inverse: true, base_currency: 'USD', reference_date: 443845330 + 31535000}).to_text_full(), '0.007675186123263489/015841551A748AD2C1F76FF6ECB0CCCD00000000/rUyPiNcSFFj6uMR2gEaD8jUerQ59G1qvwN'); + }); + }); }); diff --git a/test/sjcl-ecdsa-canonical-test.js b/test/sjcl-ecdsa-canonical-test.js new file mode 100644 index 00000000..d0a270b7 --- /dev/null +++ b/test/sjcl-ecdsa-canonical-test.js @@ -0,0 +1,28 @@ +var assert = require('assert'); +var utils = require('./testutils'); +var sjcl = require('../build/sjcl'); +var Seed = require('../src/js/ripple/seed').Seed; + +describe('SJCL ECDSA Canonicalization', function() { + describe('canonicalizeSignature', function() { + it('should canonicalize non-canonical signatures', function () { + var seed = Seed.from_json('saESc82Vun7Ta5EJRzGJbrXb5HNYk'); + var key = seed.get_key('rBZ4j6MsoctipM6GEyHSjQKzXG3yambDnZ'); + + var rs = sjcl.codec.hex.toBits("27ce1b914045ba7e8c11a2f2882cb6e07a19d4017513f12e3e363d71dc3fff0fb0a0747ecc7b4ca46e45b3b32b6b2a066aa0249c027ef11e5bce93dab756549c"); + rs = sjcl.ecc.ecdsa.secretKey.prototype.canonicalizeSignature.call(key._secret, rs); + assert.strictEqual(sjcl.codec.hex.fromBits(rs), "27ce1b914045ba7e8c11a2f2882cb6e07a19d4017513f12e3e363d71dc3fff0f4f5f8b813384b35b91ba4c4cd494d5f8500eb84aacc9af1d6403cab218dfeca5"); + }); + + it('should not touch canonical signatures', function () { + var seed = Seed.from_json('saESc82Vun7Ta5EJRzGJbrXb5HNYk'); + var key = seed.get_key('rBZ4j6MsoctipM6GEyHSjQKzXG3yambDnZ'); + + var rs = sjcl.codec.hex.toBits("5c32bc2b4d34e27af9fb66eeea0f47f6afb3d433658af0f649ebae7b872471ab7d23860688aaf9d8131f84cfffa6c56bf9c32fd8b315b2ef9d6bcb243f7a686c"); + rs = sjcl.ecc.ecdsa.secretKey.prototype.canonicalizeSignature.call(key._secret, rs); + assert.strictEqual(sjcl.codec.hex.fromBits(rs), "5c32bc2b4d34e27af9fb66eeea0f47f6afb3d433658af0f649ebae7b872471ab7d23860688aaf9d8131f84cfffa6c56bf9c32fd8b315b2ef9d6bcb243f7a686c"); + }); + }); +}); + +// vim:sw=2:sts=2:ts=8:et