diff --git a/js/amount.js b/js/amount.js index a9e6e839d8..b5c542288e 100644 --- a/js/amount.js +++ b/js/amount.js @@ -232,6 +232,11 @@ var Currency = function () { this.value = NaN; } +// Given "USD" return the json. +Currency.json_rewrite = function(j) { + return Currency.from_json(j).to_json(); +}; + Currency.from_json = function (j) { return (new Currency()).parse_json(j); }; diff --git a/js/remote.js b/js/remote.js index 8313088f0a..1b0fe5a2d4 100644 --- a/js/remote.js +++ b/js/remote.js @@ -980,6 +980,43 @@ Transaction.prototype.submit = function () { // Set options for Transactions // +Transaction._path_rewrite = function (path) { + var path_new = []; + + for (var index in path) { + var node = path[index]; + var node_new = {}; + + if ('account' in node) + node_new.account = UInt160.json_rewrite(node.account); + if ('issuer' in node) + node_new.issuer = UInt160.json_rewrite(node.issuer); + if ('currency' in node) + node_new.currency = Currency.json_rewrite(node.currency); + + path_new.push(node_new); + } + + return path_new; +} + +Transaction.prototype.path_add = function (path) { + this.transaction.Paths = this.transaction.Paths || [] + this.transaction.Paths.push(Transaction._path_rewrite(path)); + + return this; +} + +// --> paths: undefined or array of path +// A path is an array of objects containing some combination of: account, currency, issuer +Transaction.prototype.paths = function (paths) { + for (var index in paths) { + this.path_add(paths[index]); + } + + return this; +} + // If the secret is in the config object, it does not need to be provided. Transaction.prototype.secret = function (secret) { this.secret = secret; @@ -987,7 +1024,7 @@ Transaction.prototype.secret = function (secret) { Transaction.prototype.send_max = function (send_max) { if (send_max) - this.transaction.SendMax = send_max.to_json(); + this.transaction.SendMax = Amount.json_rewrite(send_max); return this; } @@ -1105,6 +1142,13 @@ Transaction.prototype.password_set = function (src, authorized_key, generator, p // --> src : UInt160 or String // --> dst : UInt160 or String // --> deliver_amount : Amount or String. +// +// Options: +// .paths() +// .path_add() +// .secret() +// .send_max() +// .set_flags() Transaction.prototype.payment = function (src, dst, deliver_amount) { this.secret = this._account_secret(src); this.transaction.TransactionType = 'Payment'; diff --git a/src/RippleCalc.cpp b/src/RippleCalc.cpp index 6cbbf46d83..3d14717d89 100644 --- a/src/RippleCalc.cpp +++ b/src/RippleCalc.cpp @@ -1421,6 +1421,8 @@ bool PathState::lessPriority(PathState::ref lhs, PathState::ref rhs) // Make sure the path delivers to uAccountID: uCurrencyID from uIssuerID. // +// If the unadded next node as specified by arguments would not work as is, then add the necessary nodes so it would work. +// // Rules: // - Currencies must be converted via an offer. // - A node names it's output. @@ -1449,13 +1451,14 @@ TER PathState::pushImply( ACCOUNT_ONE, // Placeholder for offers. uCurrencyID, // The offer's output is what is now wanted. uIssuerID); - } + const PaymentNode& pnBck = vpnNodes.back(); + // For ripple, non-stamps, ensure the issuer is on at least one side of the transaction. if (tesSUCCESS == terResult && !!uCurrencyID // Not stamps. - && (pnPrv.uAccountID != uIssuerID // Previous is not issuing own IOUs. + && (pnBck.uAccountID != uIssuerID // Previous is not issuing own IOUs. && uAccountID != uIssuerID)) // Current is not receiving own IOUs. { // Need to ripple through uIssuerID's account. @@ -1514,13 +1517,19 @@ TER PathState::pushNode( pnCur.saRevRedeem = STAmount(uCurrencyID, uAccountID); pnCur.saRevIssue = STAmount(uCurrencyID, uAccountID); - if (!bFirst) + if (bFirst) + { + // The first node is always correct as is. + + nothing(); + } + else { // Add required intermediate nodes to deliver to current account. terResult = pushImply( pnCur.uAccountID, // Current account. pnCur.uCurrencyID, // Wanted currency. - !!pnCur.uCurrencyID ? uAccountID : ACCOUNT_XNS); // Account as issuer. + !!pnCur.uCurrencyID ? uAccountID : ACCOUNT_XNS); // Account as wanted issuer. // Note: pnPrv may no longer be the immediately previous node. } @@ -1532,7 +1541,7 @@ TER PathState::pushNode( if (bBckAccount) { - SLE::pointer sleRippleState = mLedger->getSLE(Ledger::getRippleStateIndex(pnBck.uAccountID, pnCur.uAccountID, pnPrv.uCurrencyID)); + SLE::pointer sleRippleState = lesEntries.entryCache(ltRIPPLE_STATE, Ledger::getRippleStateIndex(pnBck.uAccountID, pnCur.uAccountID, pnPrv.uCurrencyID)); if (!sleRippleState) { @@ -1541,7 +1550,7 @@ TER PathState::pushNode( << " and " << RippleAddress::createHumanAccountID(pnCur.uAccountID) << " for " - << STAmount::createHumanCurrency(pnPrv.uCurrencyID) + << STAmount::createHumanCurrency(pnCur.uCurrencyID) << "." ; cLog(lsINFO) << getJson(); @@ -1555,12 +1564,12 @@ TER PathState::pushNode( << " and " << RippleAddress::createHumanAccountID(pnCur.uAccountID) << " for " - << STAmount::createHumanCurrency(pnPrv.uCurrencyID) + << STAmount::createHumanCurrency(pnCur.uCurrencyID) << "." ; - STAmount saOwed = lesEntries.rippleOwed(pnCur.uAccountID, pnBck.uAccountID, uCurrencyID); + STAmount saOwed = lesEntries.rippleOwed(pnCur.uAccountID, pnBck.uAccountID, pnCur.uCurrencyID); - if (!saOwed.isPositive() && *saOwed.negate() >= lesEntries.rippleLimit(pnCur.uAccountID, pnBck.uAccountID, uCurrencyID)) + if (!saOwed.isPositive() && *saOwed.negate() >= lesEntries.rippleLimit(pnCur.uAccountID, pnBck.uAccountID, pnCur.uCurrencyID)) { terResult = tepPATH_DRY; } diff --git a/src/SerializedObject.cpp b/src/SerializedObject.cpp index 7b6a51f946..2f90c75cba 100644 --- a/src/SerializedObject.cpp +++ b/src/SerializedObject.cpp @@ -1112,23 +1112,24 @@ std::auto_ptr STObject::parseJson(const Json::Value& object, SField::r data.push_back(new STPathSet(field)); STPathSet* tail = dynamic_cast(&data.back()); assert(tail); - for (Json::UInt i = 0; !object.isValidIndex(i); ++i) + for (Json::UInt i = 0; value.isValidIndex(i); ++i) { STPath p; - if (!object[i].isArray()) + if (!value[i].isArray()) throw std::runtime_error("Path must be array"); - for (Json::UInt j = 0; !object[i].isValidIndex(j); ++j) + for (Json::UInt j = 0; value[i].isValidIndex(j); ++j) { // each element in this path has some combination of account, currency, or issuer - Json::Value pathEl = object[i][j]; + Json::Value pathEl = value[i][j]; if (!pathEl.isObject()) throw std::runtime_error("Path elements must be objects"); - const Json::Value& account = pathEl["account"]; - const Json::Value& currency = pathEl["currency"]; - const Json::Value& issuer = pathEl["issuer"]; + const Json::Value& account = pathEl["account"]; + const Json::Value& currency = pathEl["currency"]; + const Json::Value& issuer = pathEl["issuer"]; + bool hasCurrency = false; uint160 uAccount, uCurrency, uIssuer; - bool hasCurrency; + if (!account.isNull()) { // human account id if (!account.isString()) @@ -1138,7 +1139,7 @@ std::auto_ptr STObject::parseJson(const Json::Value& object, SField::r uAccount.SetHex(strValue); { RippleAddress a; - if (!a.setAccountPublic(strValue)) + if (!a.setAccountID(strValue)) throw std::runtime_error("Account in path element invalid"); uAccount = a.getAccountID(); } @@ -1162,7 +1163,7 @@ std::auto_ptr STObject::parseJson(const Json::Value& object, SField::r else { RippleAddress a; - if (!a.setAccountPublic(issuer.asString())) + if (!a.setAccountID(issuer.asString())) throw std::runtime_error("path element issuer invalid"); uIssuer = a.getAccountID(); } diff --git a/test/send-test.js b/test/send-test.js index 13ca513765..bb42fbe2e5 100644 --- a/test/send-test.js +++ b/test/send-test.js @@ -523,6 +523,65 @@ buster.testCase("Indirect ripple", { }); }, + "indirect ripple with path" : + function (done) { + var self = this; + + async.waterfall([ + function (callback) { + self.what = "Create accounts."; + + testutils.create_accounts(self.remote, "root", "10000", ["alice", "bob", "mtgox"], callback); + }, + function (callback) { + self.what = "Set alice's limit."; + + testutils.credit_limit(self.remote, "alice", "600/USD/mtgox", callback); + }, + function (callback) { + self.what = "Set bob's limit."; + + testutils.credit_limit(self.remote, "bob", "700/USD/mtgox", callback); + }, + function (callback) { + self.what = "Give alice some mtgox."; + + testutils.payment(self.remote, "mtgox", "alice", "70/USD/mtgox", callback); + }, + function (callback) { + self.what = "Give bob some mtgox."; + + testutils.payment(self.remote, "mtgox", "bob", "50/USD/mtgox", callback); + }, + function (callback) { + self.what = "Alice sends via a path"; + + self.remote.transaction() + .payment("alice", "bob", "5/USD/mtgox") + .path_add( [ { account: "mtgox" } ]) + .on('proposed', function (m) { + // console.log("proposed: %s", JSON.stringify(m)); + + callback(m.result != 'tesSUCCESS'); + }) + .submit(); + }, + function (callback) { + self.what = "Verify alice balance with mtgox."; + + testutils.verify_balance(self.remote, "alice", "65/USD/mtgox", callback); + }, + function (callback) { + self.what = "Verify bob balance with mtgox."; + + testutils.verify_balance(self.remote, "bob", "55/USD/mtgox", callback); + }, + ], function (error) { + buster.refute(error, self.what); + done(); + }); + }, + // Direct ripple without no liqudity. // Ripple without credit path. // Ripple with one-way credit path.