diff --git a/js/amount.js b/js/amount.js index 49a60c0739..d450d0fe35 100644 --- a/js/amount.js +++ b/js/amount.js @@ -13,9 +13,7 @@ var UInt160 = function () { }; UInt160.from_json = function (j) { - var u = new UInt160(); - - return u.parse_json(j); + return (new UInt160()).parse_json(j); }; // value === NaN on error. @@ -71,7 +69,7 @@ UInt160.prototype.to_json = function () { { return exports.consts.hex_one; } - else if (20 === this.value.length) { + else if ('string' === typeof this.value && 20 === this.value.length) { return utils.stringToHex(this.value); } else @@ -90,7 +88,11 @@ var Currency = function () { // XXX Should support hex, C++ doesn't currently allow it. } -// Returns NaN on error. +Currency.from_json = function (j) { + return (new Currency()).parse_json(j); +}; + +// this.value === NaN on error. Currency.prototype.parse_json = function (j) { if ("" === j || "0" === j || "XNS" === j) { this.value = 0; @@ -102,15 +104,15 @@ Currency.prototype.parse_json = function (j) { this.value = j; } - return this.value; + return this; }; Currency.prototype.to_json = function () { - return this.value ? this.value : 'XNS'; + return this.value ? this.value : "XNS"; }; Currency.prototype.to_human = function() { - return this.value ? this.value : 'XNS'; + return this.value ? this.value : "XNS"; }; var Amount = function () { @@ -127,6 +129,10 @@ var Amount = function () { this.issuer = new UInt160(); }; +Amount.from_json = function (j) { + return (new Amount()).parse_json(j); +}; + // YYY Might also check range. Amount.prototype.is_valid = function() { return NaN !== this.value; @@ -153,7 +159,7 @@ Amount.prototype.to_text = function(allow_nan) { { return "0"; } - else if (this.offset < -25 || mOffset > -5) + else if (this.offset < -25 || this.offset > -5) { // Use e notation. // XXX Clamp output. @@ -165,14 +171,12 @@ Amount.prototype.to_text = function(allow_nan) { var val = "000000000000000000000000000" + this.value.toString() + "00000000000000000000000"; var pre = val.substring(0, this.offset + 43); var post = val.substring(this.offset + 43); - var s_pre = val.match(/[1-9].*$/); // Everything but leading zeros. - var s_post = val.match(/0+$/); // Trailing zeros. - + var s_pre = pre.match(/[1-9].*$/); // Everything but leading zeros. + var s_post = post.match(/[1-9]0*$/); // Last non-zero plus trailing zeros. return (this.is_negative ? "-" : "") - + (null == s_pre ? "0" : s_pre[0]) - + "." - + post.substring(post.length - s_post.length); + + (s_pre ? s_pre[0] : "0") + + (s_post ? "." + post.substring(0, 1+post.length-s_post[0].length) : ""); } }; @@ -186,13 +190,13 @@ Amount.prototype.canonicalize = function() { } else { - while (this.value.compareTo(exports.consts.bi_man_min_value)) { - this.value.multiply(exports.consts.bi_10); + while (this.value.compareTo(exports.consts.bi_man_min_value) < 0) { + this.value = this.value.multiply(exports.consts.bi_10); this.offset -= 1; } - while (this.value.compareTo(exports.consts.bi_man_max_value)) { - this.value.divide(exports.consts.bi_10); + while (this.value.compareTo(exports.consts.bi_man_max_value) > 0) { + this.value = this.value.divide(exports.consts.bi_10); this.offset += 1; } } @@ -212,7 +216,17 @@ Amount.prototype.to_json = function() { } }; +Amount.prototype.to_text_full = function() { + return this.value === NaN + ? NaN + : this.is_native + ? this.to_text() + "/XNS" + : this.to_text() + "/" + this.currency.to_json() + "/" + this.issuer.to_json(); +}; + // Parse a XNS value from untrusted input. +// - integer = raw units +// - float = with precision 6 // XXX Improvements: disallow leading zeros. Amount.prototype.parse_native = function(j) { var m; @@ -220,18 +234,21 @@ Amount.prototype.parse_native = function(j) { if ('string' === typeof j) m = j.match(/^(\d+)(\.\d{1,6})?$/); - if ('integer' === typeof j || null !== m) { - if ('integer' === typeof j || ("" === e[2])) { - this.value = new BigInteger(j); - } - else - { - // Decimal notation - var int_part = (new BigInteger(e[1])).multiply(exports.consts.xns_unit); - var fraction_part = (new BigInteger(e[2])).multiply(new BigInteger(Math.pow(10, exports.consts.xns_unit-e[2].length))); + if (null !== m) { + if (undefined === m[2]) { + // Integer notation - this.value = int_part.add(fraction_part); + this.value = new BigInteger(m[1]); } + else { + // Decimal notation + + var int_part = (new BigInteger(m[1])).multiply(exports.consts.xns_unit); + var fraction_part = (new BigInteger(m[2])).multiply(new BigInteger(String(Math.pow(10, 1+exports.consts.xns_precision-m[2].length)))); + + this.value = int_part.add(fraction_part); + } + this.is_native = true; this.offset = undefined; this.is_negative = undefined; @@ -244,11 +261,13 @@ Amount.prototype.parse_native = function(j) { else { this.value = NaN; } + + return this; }; // Parse a non-native value. Amount.prototype.parse_value = function(j) { - if ('integer' === typeof j) { + if ('number' === typeof j) { this.value = new BigInteger(j); this.offset = 0; this.is_native = false; @@ -260,17 +279,19 @@ Amount.prototype.parse_value = function(j) { var e = j.match(/^(-?\d+)e(\d+)/); var d = j.match(/^(-?\d+)\.(\d+)/); - if (null !== e) { + if (e) { // e notation this.value = new BigInteger(e[1]); this.offset = parseInt(e[2]); } - else if (null !== d) { + else if (d) { // float notation - this.value = (new BigInteger(e[1])).multiply((new BigInteger(exports.consts.bi_10)).pow(e[2].length)).add(new BigInteger(e[2])); - this.offset = -e[2].length; + var integer = new BigInteger(d[1]); + var fraction = new BigInteger(d[2]); + this.value = integer.multiply(exports.consts.bi_10.clone().pow(d[2].length)).add(fraction); + this.offset = -d[2].length; } else { @@ -288,21 +309,39 @@ Amount.prototype.parse_value = function(j) { else { this.value = NaN; } + + return this; }; // <-> j Amount.prototype.parse_json = function(j) { - if ('object' === typeof j && j.currency) { + if ('string' === typeof j) { + // .../.../... notation is not a wire format. But allowed for easier testing. + var m = j.match(/^(.+)\/(...)\/(.+)$/); - this.parse_value(j); + if (m) { + this.parse_value(m[1]); + this.currency = Currency.from_json(m[2]); + this.issuer = UInt160.from_json(m[3]); + } + else { + this.parse_native(j); + this.currency = new Currency(); + this.issuer = new UInt160(); + } + } + else if ('object' === typeof j && j.currency) { + // Never XNS. + + this.parse_value(j.value); this.currency.parse_json(j.currency); this.issuer.parse_json(j.issuer); } else { - this.parse_native(j); - this.currency = 0; - this.issuer = 0; + this.value = NaN; } + + return this; }; exports.Amount = Amount; @@ -321,6 +360,7 @@ exports.consts = { 'xns_max' : new BigInteger("9000000000000000000"), // Json wire limit. 'xns_min' : new BigInteger("-9000000000000000000"), // Json wire limit. 'xns_unit' : new BigInteger('1000000'), + 'xns_precision' : 6, 'bi_man_min_value' : new BigInteger('1000000000000000'), 'bi_man_max_value' : new BigInteger('9999999999999999'), 'bi_10' : new BigInteger('10'), diff --git a/js/remote.js b/js/remote.js index 3bf421d697..719bc73470 100644 --- a/js/remote.js +++ b/js/remote.js @@ -7,10 +7,15 @@ // YYY A better model might be to allow requesting a target state: keep connected or not. // -var util = require('util'); +// Node +var util = require('util'); +// npm var WebSocket = require('ws'); +var amount = require('./amount.js'); +var Amount = amount.Amount; + // --> trusted: truthy, if remote is trusted var Remote = function (trusted, websocket_ip, websocket_port, config, trace) { this.trusted = trusted; @@ -57,10 +62,10 @@ var flags = { // XXX This needs to be determined from the network. var fees = { - 'default' : 100, - 'account_create' : 1000, - 'nickname_create' : 1000, - 'offer' : 100, + 'default' : Amount.from_json("100"), + 'account_create' : Amount.from_json("1000"), + 'nickname_create' : Amount.from_json("1000"), + 'offer' : Amount.from_json("100"), }; Remote.prototype.connect_helper = function () { @@ -383,40 +388,79 @@ Remote.prototype.dirty_account_root = function (account) { // Transactions // -Remote.prototype.ripple_line_set = function (secret, src, dst, amount, onDone) { +Remote.prototype.offer_create = function (secret, src, taker_pays, taker_gets, expiration, onDone) { var secret = this.config.accounts[src] ? this.config.accounts[src].secret : secret; var src_account = this.config.accounts[src] ? this.config.accounts[src].account : src; - var dst_account = this.config.accounts[dst] ? this.config.accounts[dst].account : dst; + + var transaction = { + 'TransactionType' : 'OfferCreate', + 'Account' : src_account, + 'Fee' : fees.offer.to_json(), + 'TakerPays' : taker_pays.to_json(), + 'TakerGets' : taker_gets.to_json(), + }; + + if (expiration) + transaction.Expiration = expiration; this.submit_seq( { - 'transaction' : { - 'TransactionType' : 'CreditSet', - 'Account' : src_account, - 'Destination' : dst_account, - 'Fee' : create ? fees.account_create : fees['default'], - 'Amount' : amount, - }, + 'transaction' : transaction, 'secret' : secret, }, function () { }, onDone); }; -Remote.prototype.send_xns = function (secret, src, dst, amount, create, onDone) { +Remote.prototype.ripple_line_set = function (secret, src, limit, quaility_in, quality_out, onDone) { + var secret = this.config.accounts[src] ? this.config.accounts[src].secret : secret; + var src_account = this.config.accounts[src] ? this.config.accounts[src].account : src; + + var transaction = { + 'TransactionType' : 'CreditSet', + 'Account' : src_account, + 'Fee' : fees['default'].to_json(), + }; + + if (limit) + transaction.LimitAmount = limit.to_json(); + + if (quaility_in) + transaction.QualityIn = quaility_in; + + if (quaility_out) + transaction.QualityOut = quaility_out; + + this.submit_seq( + { + 'transaction' : transaction, + 'secret' : secret, + }, function () { + }, onDone); +}; + +// --> create: is only valid if destination gets XNS. +Remote.prototype.send = function (secret, src, dst, deliver_amount, send_max, create, onDone) { var secret = this.config.accounts[src] ? this.config.accounts[src].secret : secret; var src_account = this.config.accounts[src] ? this.config.accounts[src].account : src; var dst_account = this.config.accounts[dst] ? this.config.accounts[dst].account : dst; + var transaction = { + 'TransactionType' : 'Payment', + 'Account' : src_account, + 'Fee' : (create ? fees.account_create : fees['default']).to_json(), + 'Destination' : dst_account, + 'Amount' : deliver_amount.to_json(), + }; + + if (create) + transaction.Flags = flags.tfCreateAccount; + + if (send_max) + transaction.SendMax = send_max.to_json(); + this.submit_seq( { - 'transaction' : { - 'TransactionType' : 'Payment', - 'Account' : src_account, - 'Destination' : dst_account, - 'Fee' : create ? fees.account_create : fees['default'], - 'Flags' : create ? flags.tfCreateAccount : 0, - 'Amount' : amount, - }, + 'transaction' : transaction, 'secret' : secret, }, function () { }, onDone); diff --git a/src/Amount.cpp b/src/Amount.cpp index 78cde08d5f..672d67505f 100644 --- a/src/Amount.cpp +++ b/src/Amount.cpp @@ -1131,15 +1131,13 @@ Json::Value STAmount::getJson(int) const { Json::Value elem(Json::objectValue); - // This is a hack, many places don't specify a currency. STAmount is used just as a value. if (!mIsNative) { + // It is an error for currency or issuer not to be specified for valid json. + elem["value"] = getText(); elem["currency"] = getHumanCurrency(); - - if (!mIssuer.isZero()) - elem["issuer"] = NewcoinAddress::createHumanAccountID(mIssuer); - + elem["issuer"] = NewcoinAddress::createHumanAccountID(mIssuer); } else { diff --git a/test/amount-test.js b/test/amount-test.js index 3a7e656a74..b9addaac10 100644 --- a/test/amount-test.js +++ b/test/amount-test.js @@ -7,6 +7,27 @@ buster.testCase("Amount", { "Parse 0" : function () { buster.assert.equals(0, amount.UInt160.from_json("0").value); }, + "Parse 0 export" : function () { + buster.assert.equals(amount.consts.hex_xns, amount.UInt160.from_json("0").to_json()); + }, + "Parse native 123" : function () { + buster.assert.equals("123/XNS", amount.Amount.from_json("123").to_text_full()); + }, + "Parse native 12.3" : function () { + buster.assert.equals("12300000/XNS", amount.Amount.from_json("12.3").to_text_full()); + }, + "Parse 123./USD/iHb9CJAWyB4ij91VRWn96DkukG4bwdtyTh" : function () { + buster.assert.equals("123/USD/iHb9CJAWyB4ij91VRWn96DkukG4bwdtyTh", amount.Amount.from_json("123./USD/iHb9CJAWyB4ij91VRWn96DkukG4bwdtyTh").to_text_full()); + }, + "Parse 12300/USD/iHb9CJAWyB4ij91VRWn96DkukG4bwdtyTh" : function () { + buster.assert.equals("12300/USD/iHb9CJAWyB4ij91VRWn96DkukG4bwdtyTh", amount.Amount.from_json("12300/USD/iHb9CJAWyB4ij91VRWn96DkukG4bwdtyTh").to_text_full()); + }, + "Parse 12.3/USD/iHb9CJAWyB4ij91VRWn96DkukG4bwdtyTh" : function () { + buster.assert.equals("12.3/USD/iHb9CJAWyB4ij91VRWn96DkukG4bwdtyTh", amount.Amount.from_json("12.3/USD/iHb9CJAWyB4ij91VRWn96DkukG4bwdtyTh").to_text_full()); + }, + "Parse 1.2300/USD/iHb9CJAWyB4ij91VRWn96DkukG4bwdtyTh" : function () { + buster.assert.equals("1.23/USD/iHb9CJAWyB4ij91VRWn96DkukG4bwdtyTh", amount.Amount.from_json("1.2300/USD/iHb9CJAWyB4ij91VRWn96DkukG4bwdtyTh").to_text_full()); + }, } }); diff --git a/test/standalone-test.js b/test/standalone-test.js index f8b8985956..1cf0d2a6da 100644 --- a/test/standalone-test.js +++ b/test/standalone-test.js @@ -2,8 +2,11 @@ var buster = require("buster"); var config = require("./config.js"); var server = require("./server.js"); +var amount = require("../js/amount.js"); var remote = require("../js/remote.js"); +var Amount = amount.Amount; + // How long to wait for server to start. var serverDelay = 1500; @@ -113,8 +116,8 @@ buster.testCase("Websocket commands", { alpha.request_ledger_entry({ 'ledger_closed' : r.ledger_closed, - 'type' : 'account_root', - 'account_root' : 'iHb9CJAWyB4ij91VRWn96DkukG4bwdtyTh' + 'type' : 'account_root', + 'account_root' : 'iHb9CJAWyB4ij91VRWn96DkukG4bwdtyTh' } , function (r) { // console.log("account_root: %s", JSON.stringify(r)); @@ -133,8 +136,8 @@ buster.testCase("Websocket commands", { alpha.request_ledger_entry({ 'ledger_closed' : r.ledger_closed, - 'type' : 'account_root', - 'account_root' : 'foobar' + 'type' : 'account_root', + 'account_root' : 'foobar' } , function (r) { // console.log("account_root: %s", JSON.stringify(r)); @@ -153,8 +156,8 @@ buster.testCase("Websocket commands", { alpha.request_ledger_entry({ 'ledger_closed' : r.ledger_closed, - 'type' : 'account_root', - 'account_root' : 'iG1QQv2nh2gi7RCZ1P8YYcBUKCCN633jCn' + 'type' : 'account_root', + 'account_root' : 'iG1QQv2nh2gi7RCZ1P8YYcBUKCCN633jCn' }, function (r) { console.log("account_root: %s", JSON.stringify(r)); @@ -173,8 +176,8 @@ buster.testCase("Websocket commands", { alpha.request_ledger_entry({ 'ledger_closed' : r.ledger_closed, - 'type' : 'account_root', - 'index' : "2B6AC232AA4C4BE41BF49D2459FA4A0347E1B543A4C92FCEE0821C0201E2E9A8", + 'type' : 'account_root', + 'index' : "2B6AC232AA4C4BE41BF49D2459FA4A0347E1B543A4C92FCEE0821C0201E2E9A8", } , function (r) { console.log("node: %s", JSON.stringify(r)); @@ -186,7 +189,7 @@ buster.testCase("Websocket commands", { 'create account' : function (done) { - alpha.send_xns(undefined, 'root', 'alice', 10000, true, function (r) { + alpha.send(undefined, 'root', 'alice', Amount.from_json("10000"), undefined, 'CREATE', function (r) { console.log(r); buster.refute(r.error);