From 68dbd66aea90f02f6b860cc4a62b416a6ea7dfdd Mon Sep 17 00:00:00 2001 From: JoelKatz Date: Fri, 2 Nov 2012 16:08:24 -0700 Subject: [PATCH 01/14] Tiny cleanup. --- src/SHAMapSync.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/SHAMapSync.cpp b/src/SHAMapSync.cpp index e0ced0d8d..a1e661cd0 100644 --- a/src/SHAMapSync.cpp +++ b/src/SHAMapSync.cpp @@ -65,7 +65,7 @@ void SHAMap::getMissingNodes(std::vector& nodeIDs, std::vectorgetNodeHash()) { cLog(lsERROR) << "Wrong hash from cached object"; - d = SHAMapTreeNode::pointer(); + d.reset(); } else { From a17e02e35fa8101eeb7875ab58e78073dbbc01fe Mon Sep 17 00:00:00 2001 From: JoelKatz Date: Fri, 2 Nov 2012 16:53:46 -0700 Subject: [PATCH 02/14] Ack! I accidentally made some data undecodable. This will fix broken tx metadata. --- src/SerializeProto.h | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/SerializeProto.h b/src/SerializeProto.h index 4cb17d5dc..776e7b109 100644 --- a/src/SerializeProto.h +++ b/src/SerializeProto.h @@ -125,22 +125,22 @@ // inner object // OBJECT/1 is reserved for end of object - FIELD(TemplateEntry, OBJECT, 1) FIELD(TransactionMetaData, OBJECT, 2) FIELD(CreatedNode, OBJECT, 3) FIELD(DeletedNode, OBJECT, 4) FIELD(ModifiedNode, OBJECT, 5) FIELD(PreviousFields, OBJECT, 6) FIELD(FinalFields, OBJECT, 7) + FIELD(TemplateEntry, OBJECT, 8) // array of objects // ARRAY/1 is reserved for end of array - FIELD(AffectedNodes, ARRAY, 1) FIELD(SigningAccounts, ARRAY, 2) FIELD(TxnSignatures, ARRAY, 3) FIELD(Signatures, ARRAY, 4) FIELD(Template, ARRAY, 5) FIELD(Necessary, ARRAY, 6) FIELD(Sufficient, ARRAY, 7) + FIELD(AffectedNodes, ARRAY, 8) // vim:ts=4 From 1af46fbe133c80bb5c1d4b6527929a8ecb3e77a1 Mon Sep 17 00:00:00 2001 From: JoelKatz Date: Fri, 2 Nov 2012 17:05:19 -0700 Subject: [PATCH 03/14] Track fields not serialized for signing hashes in a more sensible way. --- src/FieldNames.cpp | 3 +++ src/FieldNames.h | 11 +++++++++-- src/SerializedObject.cpp | 7 +------ 3 files changed, 13 insertions(+), 8 deletions(-) diff --git a/src/FieldNames.cpp b/src/FieldNames.cpp index 9b76ffe08..2789f33ab 100644 --- a/src/FieldNames.cpp +++ b/src/FieldNames.cpp @@ -28,6 +28,9 @@ SField sfIndex(STI_HASH256, 258, "index"); static int initFields() { + sfTxnSignature.notSigningField(); sfTxnSignatures.notSigningField(); + sfSignature.notSigningField(); + sfHighQualityIn.setMeta(SFM_CHANGE); sfHighQualityOut.setMeta(SFM_CHANGE); sfLowQualityIn.setMeta(SFM_CHANGE); sfLowQualityOut.setMeta(SFM_CHANGE); diff --git a/src/FieldNames.h b/src/FieldNames.h index cdb11ad6b..3a0ae9a3a 100644 --- a/src/FieldNames.h +++ b/src/FieldNames.h @@ -60,16 +60,18 @@ public: const int fieldValue; // Code number for protocol std::string fieldName; SF_Meta fieldMeta; + bool signingField; SField(int fc, SerializedTypeID tid, int fv, const char* fn) : - fieldCode(fc), fieldType(tid), fieldValue(fv), fieldName(fn), fieldMeta(SFM_NEVER) + fieldCode(fc), fieldType(tid), fieldValue(fv), fieldName(fn), fieldMeta(SFM_NEVER), signingField(true) { boost::mutex::scoped_lock sl(mapMutex); codeToField[fieldCode] = this; } SField(SerializedTypeID tid, int fv, const char *fn) : - fieldCode(FIELD_CODE(tid, fv)), fieldType(tid), fieldValue(fv), fieldName(fn), fieldMeta(SFM_NEVER) + fieldCode(FIELD_CODE(tid, fv)), fieldType(tid), fieldValue(fv), fieldName(fn), + fieldMeta(SFM_NEVER), signingField(true) { boost::mutex::scoped_lock sl(mapMutex); codeToField[fieldCode] = this; @@ -97,6 +99,11 @@ public: bool shouldMetaDel() const { return (fieldMeta == SFM_DELETE) || (fieldMeta == SFM_ALWAYS); } bool shouldMetaMod() const { return (fieldMeta == SFM_CHANGE) || (fieldMeta == SFM_ALWAYS); } void setMeta(SF_Meta m) { fieldMeta = m; } + bool isSigningField() const { return signingField; } + void notSigningField() { signingField = false; } + + bool shouldInclude(bool withSigningField) const + { return (fieldValue < 256) && (withSigningField || signingField); } bool operator==(const SField& f) const { return fieldCode == f.fieldCode; } bool operator!=(const SField& f) const { return fieldCode != f.fieldCode; } diff --git a/src/SerializedObject.cpp b/src/SerializedObject.cpp index 2f90c75cb..2ef6a84f8 100644 --- a/src/SerializedObject.cpp +++ b/src/SerializedObject.cpp @@ -283,13 +283,8 @@ void STObject::add(Serializer& s, bool withSigningFields) const BOOST_FOREACH(const SerializedType& it, mData) { // pick out the fields and sort them - if ((it.getSType() != STI_NOTPRESENT) && it.getFName().isBinary()) - { - SField::ref fName = it.getFName(); - if (withSigningFields || - ((fName != sfTxnSignature) && (fName != sfTxnSignatures) && (fName != sfSignature))) + if ((it.getSType() != STI_NOTPRESENT) && it.getFName().shouldInclude(withSigningFields)) fields.insert(std::make_pair(it.getFName().fieldCode, &it)); - } } From 8b8b8ce61207df0116b3ce50abe009b4531c5adb Mon Sep 17 00:00:00 2001 From: Arthur Britto Date: Sat, 3 Nov 2012 04:13:37 -0700 Subject: [PATCH 04/14] UT: Add utility to verify multiple balances. --- test/testutils.js | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/test/testutils.js b/test/testutils.js index 8ab9af69e..49209ff98 100644 --- a/test/testutils.js +++ b/test/testutils.js @@ -159,15 +159,43 @@ var verify_balance = function (remote, src, amount_json, callback) { // console.log("issuer_balance: %s", m.issuer_balance.to_text_full()); // console.log("issuer_limit: %s", m.issuer_limit.to_text_full()); + if (!m.account_balance.equals(amount)) { + console.log("verify_balance: failed: %s vs %s is %s", src, amount_json, amount.to_text_full()); + } + callback(!m.account_balance.equals(amount)); }) .request(); }; + +var verify_balances = function (remote, balances, callback) { + var tests = []; + + for (var src in balances) { + var values_src = balances[src]; + var values = 'string' === typeof values_src ? [ values_src ] : values_src; + + for (var index in values) { + tests.push( { "source" : src, "amount" : values[index] } ); + } + } + + async.every(tests, + function (check, callback) { + verify_balance(remote, check.source, check.amount, + function (mismatch) { callback(!mismatch); }); + }, + function (every) { + callback(!every); + }); +}; + exports.build_setup = build_setup; exports.create_accounts = create_accounts; exports.credit_limit = credit_limit; exports.payment = payment; exports.build_teardown = build_teardown; exports.verify_balance = verify_balance; +exports.verify_balances = verify_balances; // vim:sw=2:sts=2:ts=8 From 4adb0e07a2f1f75996c185d985da647170d4c7ba Mon Sep 17 00:00:00 2001 From: Arthur Britto Date: Sat, 3 Nov 2012 04:14:09 -0700 Subject: [PATCH 05/14] JS: Add support for setting transfer fee. --- js/remote.js | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/js/remote.js b/js/remote.js index 9e0b4fedf..0d6459d03 100644 --- a/js/remote.js +++ b/js/remote.js @@ -1042,6 +1042,13 @@ Transaction.prototype.send_max = function (send_max) { return this; } +// --> rate: In billionths. +Transaction.prototype.transfer_rate = function (rate) { + this.transaction.TransferRate = Number(rate); + + return this; +} + // Add flags to a transaction. // --> flags: undefined, _flag_, or [ _flags_ ] Transaction.prototype.set_flags = function (flags) { @@ -1081,14 +1088,15 @@ Transaction.prototype._account_secret = function (account) { return this.remote.config.accounts[account] ? this.remote.config.accounts[account].secret : undefined; }; -// .wallet_locator() -// .message_key() -// .domain() -// .transfer_rate() -// .publish() +// Options: +// .domain() NYI +// .message_key() NYI +// .transfer_rate() +// .wallet_locator() NYI Transaction.prototype.account_set = function (src) { this.secret = this._account_secret(src); this.transaction.TransactionType = 'AccountSet'; + this.transaction.Account = UInt160.json_rewrite(src); return this; }; From 7c595bf23bc26100237248fdfa636ec18a47f307 Mon Sep 17 00:00:00 2001 From: Arthur Britto Date: Sat, 3 Nov 2012 04:16:48 -0700 Subject: [PATCH 06/14] Fix ripple bugs. - Default send max inherits source account as issuer. - Add end point implications for node expansion. - Fix rippling through accounts. --- src/RippleCalc.cpp | 146 +++++++++++++++++++++++++------------- src/SerializedTypes.h | 2 +- src/TransactionAction.cpp | 6 +- 3 files changed, 101 insertions(+), 53 deletions(-) diff --git a/src/RippleCalc.cpp b/src/RippleCalc.cpp index a4f377417..2e78779a3 100644 --- a/src/RippleCalc.cpp +++ b/src/RippleCalc.cpp @@ -799,7 +799,11 @@ void RippleCalc::calcNodeRipple( } // Calculate saPrvRedeemReq, saPrvIssueReq, saPrvDeliver from saCur... -// No account adjustments in reverse as we don't know how much is going to actually be pushed through yet. +// Based on required deliverable, propagate redeem, issue, and deliver requests to the previous node. +// Inflate amount requested by required fees. +// Reedems are limited based on IOUs previous has on hand. +// Issues are limited based on credit limits and amount owed. +// No account balance adjustments as we don't know how much is going to actually be pushed through yet. // <-- tesSUCCESS or tepPATH_DRY TER RippleCalc::calcNodeAccountRev(const unsigned int uIndex, PathState::ref pspCur, const bool bMultiQuality) { @@ -1135,7 +1139,9 @@ TER RippleCalc::calcNodeAccountRev(const unsigned int uIndex, PathState::ref psp return terResult; } -// When moving forward, we know the actual amount to push through so adjust balances. +// The reverse pass has been narrowing by credit available and inflating by fees as it worked backwards. +// Now, push through the actual amount to each node and adjust balances. +// // Perform balance adjustments between previous and current node. // - The previous node: specifies what to push through to current. // - All of previous output is consumed. @@ -1204,46 +1210,39 @@ TER RippleCalc::calcNodeAccountFwd( if (bPrvAccount && bNxtAccount) { + // Next is an account, must be rippling. + if (!uIndex) { // ^ --> ACCOUNT --> account - // First node, calculate amount to send. - // XXX Limit by stamp/ripple balance + // First node, calculate amount to ripple based on what is available. - const STAmount& saCurSendMaxReq = pspCur->saInReq.isNegative() - ? pspCur->saInReq // Negative for no limit, doing a calculation. + // Limit by sendmax. + const STAmount saCurSendMaxReq = pspCur->saInReq.isNegative() + ? pspCur->saInReq // Negative for no limit, doing a calculation. : pspCur->saInReq-pspCur->saInAct; // request - done. STAmount& saCurSendMaxPass = pspCur->saInPass; // Report how much pass sends. - if (saCurRedeemReq) - { - // Redeem requested. - saCurRedeemAct = saCurRedeemReq.isNegative() - ? saCurRedeemReq - : std::min(saCurRedeemReq, saCurSendMaxReq); - } - else - { - // No redeeming. - - saCurRedeemAct = saCurRedeemReq; - } + saCurRedeemAct = saCurRedeemReq + // Redeem requested. + ? saCurRedeemReq.isNegative() + ? saCurRedeemReq + : std::min(saCurRedeemReq, saCurSendMaxReq) + // No redeeming. + : saCurRedeemReq; saCurSendMaxPass = saCurRedeemAct; - if (saCurIssueReq && (saCurSendMaxReq.isNegative() || saCurSendMaxPass != saCurSendMaxReq)) - { - // Issue requested and pass does not meet max. - saCurIssueAct = saCurSendMaxReq.isNegative() - ? saCurIssueReq - : std::min(saCurSendMaxReq-saCurRedeemAct, saCurIssueReq); - } - else - { - // No issuing. + saCurIssueAct = (saCurIssueReq // Issue wanted. + && (saCurSendMaxReq.isNegative() // No limit. + || saCurSendMaxPass != saCurSendMaxReq)) // Not yet satisfied. + // Issue requested and pass does not meet max. + ? saCurSendMaxReq.isNegative() + ? saCurIssueReq + : std::min(saCurSendMaxReq-saCurRedeemAct, saCurIssueReq) + // No issuing. + : STAmount(saCurIssueReq); - saCurIssueAct = STAmount(saCurIssueReq); - } saCurSendMaxPass += saCurIssueAct; cLog(lsINFO) << boost::str(boost::format("calcNodeAccountFwd: ^ --> ACCOUNT --> account : saInReq=%s saInAct=%s saCurSendMaxReq=%s saCurRedeemAct=%s saCurIssueReq=%s saCurIssueAct=%s saCurSendMaxPass=%s") @@ -1287,7 +1286,7 @@ TER RippleCalc::calcNodeAccountFwd( saCurIssueAct.zero(saCurIssueReq); // Previous redeem part 1: redeem -> redeem - if (saPrvRedeemReq != saPrvRedeemAct) // Previous wants to redeem. To next must be ok. + if (saPrvRedeemReq && saCurRedeemReq) // Previous wants to redeem. { // Rate : 1.0 : quality out calcNodeRipple(QUALITY_ONE, uQualityOut, saPrvRedeemReq, saCurRedeemReq, saPrvRedeemAct, saCurRedeemAct, uRateMax); @@ -1302,25 +1301,23 @@ TER RippleCalc::calcNodeAccountFwd( } // Previous redeem part 2: redeem -> issue. - // wants to redeem and current would and can issue. - // If redeeming cur to next is done, this implies can issue. if (saPrvRedeemReq != saPrvRedeemAct // Previous still wants to redeem. - && saCurRedeemReq == saCurRedeemAct // Current has no more to redeem to next. - && saCurIssueReq) + && saCurRedeemReq == saCurRedeemAct // Current redeeming is done can issue. + && saCurIssueReq) // Current wants to issue. { // Rate : 1.0 : transfer_rate calcNodeRipple(QUALITY_ONE, lesActive.rippleTransferRate(uCurAccountID), saPrvRedeemReq, saCurIssueReq, saPrvRedeemAct, saCurIssueAct, uRateMax); } // Previous issue part 2 : issue -> issue - if (saPrvIssueReq != saPrvIssueAct) // Previous wants to issue. To next must be ok. + if (saPrvIssueReq != saPrvIssueAct // Previous wants to issue. + && saCurRedeemReq == saCurRedeemAct) // Current redeeming is done can issue. { // Rate: quality in : 1.0 calcNodeRipple(uQualityIn, QUALITY_ONE, saPrvIssueReq, saCurIssueReq, saPrvIssueAct, saCurIssueAct, uRateMax); } // Adjust prv --> cur balance : take all inbound - // XXX Currency must be in amount. lesActive.rippleCredit(uPrvAccountID, uCurAccountID, saPrvRedeemReq + saPrvIssueReq, false); } } @@ -1429,7 +1426,7 @@ bool PathState::lessPriority(PathState::ref lhs, PathState::ref rhs) return lhs->mIndex > rhs->mIndex; // Bigger is worse. } -// Make sure the path delivers to uAccountID: uCurrencyID from uIssuerID. +// Make sure last path node 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. // @@ -1651,7 +1648,40 @@ PathState::PathState( | STPathElement::typeIssuer, uSenderID, uInCurrencyID, - uInIssuerID); + uSenderID); + + if (tesSUCCESS == terStatus + && !!uInCurrencyID // First was not XRC + && uInIssuerID != uSenderID) { // Issuer was not same as sender + // May have an implied node. + + // Figure out next node properties for implied node. + const uint160 uNxtCurrencyID = spSourcePath.getElementCount() + ? spSourcePath.getElement(0).getCurrency() + : uOutCurrencyID; + const uint160 uNxtAccountID = spSourcePath.getElementCount() + ? spSourcePath.getElement(0).getAccountID() + : !!uOutCurrencyID + ? uOutIssuerID == uReceiverID + ? uReceiverID + : uOutIssuerID + : ACCOUNT_XNS; + + // Can't just use push implied, because it can't compensate for next account. + if (!uNxtCurrencyID // Next is XRC - will have offer next + || uInCurrencyID != uNxtCurrencyID // Next is different current - will have offer next + || uInIssuerID != uNxtAccountID) // Next is not implied issuer + { + // Add implied account. + terStatus = pushNode( + STPathElement::typeAccount + | STPathElement::typeCurrency + | STPathElement::typeIssuer, + uInIssuerID, + uInCurrencyID, + uInIssuerID); + } + } BOOST_FOREACH(const STPathElement& speElement, spSourcePath) { @@ -1659,21 +1689,35 @@ PathState::PathState( terStatus = pushNode(speElement.getNodeType(), speElement.getAccountID(), speElement.getCurrency(), speElement.getIssuerID()); } + const PaymentNode& pnPrv = vpnNodes.back(); + + if (tesSUCCESS == terStatus + && !!uOutCurrencyID // Next is not XRC + && uOutIssuerID != uReceiverID // Out issuer is not reciever + && (pnPrv.uCurrencyID != uOutCurrencyID // Previous will be an offer. + || pnPrv.uAccountID != uOutIssuerID)) // Need the implied issuer. + { + // Add implied account. + terStatus = pushNode( + STPathElement::typeAccount + | STPathElement::typeCurrency + | STPathElement::typeIssuer, + uOutIssuerID, + uInCurrencyID, + uOutIssuerID); + } + if (tesSUCCESS == terStatus) { // Create receiver node. - terStatus = pushImply(uReceiverID, uOutCurrencyID, uOutIssuerID); - if (tesSUCCESS == terStatus) - { - terStatus = pushNode( - STPathElement::typeAccount // Last node is always an account. - | STPathElement::typeCurrency - | STPathElement::typeIssuer, - uReceiverID, // Receive to output - uOutCurrencyID, // Desired currency - uOutIssuerID); - } + terStatus = pushNode( + STPathElement::typeAccount // Last node is always an account. + | STPathElement::typeCurrency + | STPathElement::typeIssuer, + uReceiverID, // Receive to output + uOutCurrencyID, // Desired currency + !!uOutCurrencyID ? uReceiverID : ACCOUNT_XNS); } if (tesSUCCESS == terStatus) diff --git a/src/SerializedTypes.h b/src/SerializedTypes.h index 4832b4621..b86df1ddb 100644 --- a/src/SerializedTypes.h +++ b/src/SerializedTypes.h @@ -603,7 +603,7 @@ public: int getElementCount() const { return mPath.size(); } bool isEmpty() const { return mPath.empty(); } const STPathElement& getElement(int offset) const { return mPath[offset]; } - const STPathElement& getElemet(int offset) { return mPath[offset]; } + const STPathElement& getElement(int offset) { return mPath[offset]; } void addElement(const STPathElement &e) { mPath.push_back(e); } void clear() { mPath.clear(); } bool hasSeen(const uint160 &acct); diff --git a/src/TransactionAction.cpp b/src/TransactionAction.cpp index a793957f2..8ff5c1f34 100644 --- a/src/TransactionAction.cpp +++ b/src/TransactionAction.cpp @@ -462,7 +462,11 @@ TER TransactionEngine::doPayment(const SerializedTransaction& txn, const Transac const bool bMax = txn.isFieldPresent(sfSendMax); const uint160 uDstAccountID = txn.getFieldAccount160(sfDestination); const STAmount saDstAmount = txn.getFieldAmount(sfAmount); - const STAmount saMaxAmount = bMax ? txn.getFieldAmount(sfSendMax) : saDstAmount; + const STAmount saMaxAmount = bMax + ? txn.getFieldAmount(sfSendMax) + : saDstAmount.isNative() + ? saDstAmount + : STAmount(saDstAmount.getCurrency(), mTxnAccountID, saDstAmount.getMantissa(), saDstAmount.getExponent(), saDstAmount.isNegative()); const uint160 uSrcCurrency = saMaxAmount.getCurrency(); const uint160 uDstCurrency = saDstAmount.getCurrency(); From dfaf33aa0a0b536b17768c31d2afee5c38e9dd5a Mon Sep 17 00:00:00 2001 From: Arthur Britto Date: Sat, 3 Nov 2012 04:18:55 -0700 Subject: [PATCH 07/14] UT: Add test of transfer fees. --- test/send-test.js | 124 ++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 102 insertions(+), 22 deletions(-) diff --git a/test/send-test.js b/test/send-test.js index 5cb895700..08f655a19 100644 --- a/test/send-test.js +++ b/test/send-test.js @@ -642,39 +642,119 @@ buster.testCase("Indirect ripple", { .submit(); }, function (callback) { - self.what = "Verify amazon balance with mtgox."; + self.what = "Verify balances."; - testutils.verify_balance(self.remote, "amazon", "150/USD/mtgox", callback); - }, - function (callback) { - self.what = "Verify alice balance with bob."; - - testutils.verify_balance(self.remote, "alice", "-50/USD/bob", callback); - }, - function (callback) { - self.what = "Verify alice balance with carol."; - - testutils.verify_balance(self.remote, "alice", "-100/USD/carol", callback); - }, - function (callback) { - self.what = "Verify bob balance with mtgox."; - - testutils.verify_balance(self.remote, "bob", "50/USD/mtgox", callback); - }, - function (callback) { - self.what = "Verify carol balance with mtgox."; - - testutils.verify_balance(self.remote, "carol", "0/USD/mtgox", callback); + testutils.verify_balances(self.remote, + { + "alice" : [ "-50/USD/bob", "-100/USD/carol" ], + "amazon" : "150/USD/mtgox", + "bob" : "50/USD/mtgox", + "carol" : "0/USD/mtgox", + }, + callback); }, ], function (error) { buster.refute(error, self.what); done(); }); }, + + "indirect ripple with path and transfer fee" : + function (done) { + var self = this; + + async.waterfall([ + function (callback) { + self.what = "Create accounts."; + + testutils.create_accounts(self.remote, "root", "10000", ["alice", "bob", "carol", "amazon", "mtgox"], callback); + }, + function (callback) { + self.what = "Set mtgox transfer rate."; + + self.remote.transaction() + .account_set("mtgox") + .transfer_rate(1.1e9) + .on('proposed', function (m) { + // console.log("proposed: %s", JSON.stringify(m)); + + callback(m.result != 'tesSUCCESS'); + }) + .submit(); + }, + function (callback) { + self.what = "Set alice's limit with bob."; + + testutils.credit_limit(self.remote, "bob", "600/USD/alice", callback); + }, + function (callback) { + self.what = "Set alice's limit with carol."; + + testutils.credit_limit(self.remote, "carol", "700/USD/alice", callback); + }, + function (callback) { + self.what = "Set bob's mtgox limit."; + + testutils.credit_limit(self.remote, "bob", "1000/USD/mtgox", callback); + }, + function (callback) { + self.what = "Set carol's mtgox limit."; + + testutils.credit_limit(self.remote, "carol", "1000/USD/mtgox", callback); + }, + function (callback) { + self.what = "Set amazon's mtgox limit."; + + testutils.credit_limit(self.remote, "amazon", "2000/USD/mtgox", callback); + }, + function (callback) { + self.what = "Give bob some mtgox."; + + testutils.payment(self.remote, "mtgox", "bob", "100/USD/mtgox", callback); + }, + function (callback) { + self.what = "Give carol some mtgox."; + + testutils.payment(self.remote, "mtgox", "carol", "100/USD/mtgox", callback); + }, + function (callback) { + self.what = "Alice pays amazon via multiple paths"; + + self.remote.transaction() + .payment("alice", "amazon", "150/USD/mtgox") + .send_max("200/USD/alice") + .path_add( [ { account: "bob" } ]) + .path_add( [ { account: "carol" } ]) + .on('proposed', function (m) { + // console.log("proposed: %s", JSON.stringify(m)); + + callback(m.result != 'tesSUCCESS'); + }) + .submit(); + }, + function (callback) { + self.what = "Verify balances."; + + testutils.verify_balances(self.remote, + { + "alice" : [ "-65.00000000000001/USD/bob", "-100/USD/carol" ], + "amazon" : "150/USD/mtgox", + "bob" : "35/USD/mtgox", + "carol" : "0/USD/mtgox", + }, + callback); + }, + ], function (error) { + buster.refute(error, self.what); + done(); + }); + }, + // Max send of currency sender doesn't have. // Direct ripple without no liqudity. // Ripple without credit path. // Ripple with one-way credit path. // Transfer Fees // Use multiple paths. + // Test with XRC at start and end. }); // vim:sw=2:sts=2:ts=8 From f4d4b3920dd91a23dcd02b7b3b6e8f21793885bb Mon Sep 17 00:00:00 2001 From: Arthur Britto Date: Sat, 3 Nov 2012 04:32:46 -0700 Subject: [PATCH 08/14] Fix ripple best path determination. --- src/RippleCalc.cpp | 4 ++-- test/send-test.js | 12 ++++++------ 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/RippleCalc.cpp b/src/RippleCalc.cpp index 2e78779a3..d4ba49a39 100644 --- a/src/RippleCalc.cpp +++ b/src/RippleCalc.cpp @@ -2109,8 +2109,8 @@ int iPass = 0; assert(!!pspCur->saInPass && !!pspCur->saOutPass); if ((!bLimitQuality || pspCur->uQuality <= uQualityLimit) // Quality is not limted or increment has allowed quality. - || !pspBest // Best is not yet set. - || PathState::lessPriority(pspBest, pspCur)) // Current is better than set. + && (!pspBest // Best is not yet set. + || PathState::lessPriority(pspBest, pspCur))) // Current is better than set. { cLog(lsDEBUG) << boost::str(boost::format("rippleCalc: better: uQuality=%s saInPass=%s saOutPass=%s") % STAmount::saFromRate(pspCur->uQuality) diff --git a/test/send-test.js b/test/send-test.js index 08f655a19..2de3fd7f8 100644 --- a/test/send-test.js +++ b/test/send-test.js @@ -646,10 +646,10 @@ buster.testCase("Indirect ripple", { testutils.verify_balances(self.remote, { - "alice" : [ "-50/USD/bob", "-100/USD/carol" ], + "alice" : [ "-100/USD/bob", "-50/USD/carol" ], "amazon" : "150/USD/mtgox", - "bob" : "50/USD/mtgox", - "carol" : "0/USD/mtgox", + "bob" : "0/USD/mtgox", + "carol" : "50/USD/mtgox", }, callback); }, @@ -737,10 +737,10 @@ buster.testCase("Indirect ripple", { testutils.verify_balances(self.remote, { - "alice" : [ "-65.00000000000001/USD/bob", "-100/USD/carol" ], + "alice" : [ "-100/USD/bob", "-65.00000000000001/USD/carol" ], "amazon" : "150/USD/mtgox", - "bob" : "35/USD/mtgox", - "carol" : "0/USD/mtgox", + "bob" : "0/USD/mtgox", + "carol" : "35/USD/mtgox", }, callback); }, From 7b268647a0b13b9c2bdc71639959136a1a75c13f Mon Sep 17 00:00:00 2001 From: Arthur Britto Date: Sat, 3 Nov 2012 13:06:55 -0700 Subject: [PATCH 09/14] UT: Add helper for setting transfer rate. --- js/remote.js | 3 +++ test/send-test.js | 15 +-------------- test/testutils.js | 20 ++++++++++++++++++++ 3 files changed, 24 insertions(+), 14 deletions(-) diff --git a/js/remote.js b/js/remote.js index 0d6459d03..e43bb2e77 100644 --- a/js/remote.js +++ b/js/remote.js @@ -1046,6 +1046,9 @@ Transaction.prototype.send_max = function (send_max) { Transaction.prototype.transfer_rate = function (rate) { this.transaction.TransferRate = Number(rate); + if (this.transaction.TransferRate < 1e9) + throw 'invalidTransferRate'; + return this; } diff --git a/test/send-test.js b/test/send-test.js index 2de3fd7f8..990afe212 100644 --- a/test/send-test.js +++ b/test/send-test.js @@ -672,15 +672,7 @@ buster.testCase("Indirect ripple", { function (callback) { self.what = "Set mtgox transfer rate."; - self.remote.transaction() - .account_set("mtgox") - .transfer_rate(1.1e9) - .on('proposed', function (m) { - // console.log("proposed: %s", JSON.stringify(m)); - - callback(m.result != 'tesSUCCESS'); - }) - .submit(); + testutils.transfer_rate(self.remote, "mtgox", 1.1e9, callback); }, function (callback) { self.what = "Set alice's limit with bob."; @@ -749,12 +741,7 @@ buster.testCase("Indirect ripple", { done(); }); }, - // Max send of currency sender doesn't have. // Direct ripple without no liqudity. - // Ripple without credit path. - // Ripple with one-way credit path. - // Transfer Fees - // Use multiple paths. // Test with XRC at start and end. }); // vim:sw=2:sts=2:ts=8 diff --git a/test/testutils.js b/test/testutils.js index 49209ff98..4d1d9ec2d 100644 --- a/test/testutils.js +++ b/test/testutils.js @@ -147,6 +147,25 @@ var payment = function (remote, src, dst, amount, callback) { .submit(); }; +var transfer_rate = function (remote, src, billionths, callback) { + assert(4 === arguments.length); + + remote.transaction() + .account_set(src) + .transfer_rate(billionths) + .on('proposed', function (m) { + // console.log("proposed: %s", JSON.stringify(m)); + + callback(m.result != 'tesSUCCESS'); + }) + .on('error', function (m) { + // console.log("error: %s", JSON.stringify(m)); + + callback(m); + }) + .submit(); +}; + var verify_balance = function (remote, src, amount_json, callback) { assert(4 === arguments.length); var amount = Amount.from_json(amount_json); @@ -195,6 +214,7 @@ exports.create_accounts = create_accounts; exports.credit_limit = credit_limit; exports.payment = payment; exports.build_teardown = build_teardown; +exports.transfer_rate = transfer_rate; exports.verify_balance = verify_balance; exports.verify_balances = verify_balances; From 41f2f5c0c076b5a9823bfba1d741d11a0f6b34a9 Mon Sep 17 00:00:00 2001 From: Arthur Britto Date: Sat, 3 Nov 2012 13:14:01 -0700 Subject: [PATCH 10/14] UT: remove mapOr. --- js/nodeutils.js | 5 ++++- js/utils.js | 19 ------------------- 2 files changed, 4 insertions(+), 20 deletions(-) diff --git a/js/nodeutils.js b/js/nodeutils.js index befdb4abc..1959ef87a 100644 --- a/js/nodeutils.js +++ b/js/nodeutils.js @@ -1,16 +1,18 @@ +var async = require("async"); var fs = require("fs"); var path = require("path"); var utils = require("./utils.js"); // Empty a directory. +// done(err) : err = true if an error occured. var emptyPath = function(dirPath, done) { fs.readdir(dirPath, function(err, files) { if (err) { done(err); } else { - utils.mapOr(rmPath, files.map(function(f) { return path.join(dirPath, f); }), done); + async.some(files.map(function(f) { return path.join(dirPath, f); }), rmPath, done); } }); }; @@ -53,6 +55,7 @@ var resetPath = function(dirPath, mode, done) { }; // Remove path recursively. +// done(err) var rmPath = function(dirPath, done) { // console.log("rmPath: %s", dirPath); diff --git a/js/utils.js b/js/utils.js index 288445993..e51ca76ed 100644 --- a/js/utils.js +++ b/js/utils.js @@ -19,24 +19,6 @@ var throwErr = function(done) { }; }; -// apply function to elements of array. Return first true value to done or undefined. -var mapOr = function(func, array, done) { - if (array.length) { - func(array[array.length-1], function(v) { - if (v) { - done(v); - } - else { - array.length -= 1; - mapOr(func, array, done); - } - }); - } - else { - done(); - } -}; - var trace = function(comment, func) { return function() { console.log("%s: %s", trace, arguments.toString); @@ -88,7 +70,6 @@ var stringToArray = function (s) { return a; }; -exports.mapOr = mapOr; exports.trace = trace; exports.arraySet = arraySet; exports.hexToString = hexToString; From 025193efd126d1d39a24c1efbc02bfa3f4f491f5 Mon Sep 17 00:00:00 2001 From: Arthur Britto Date: Sat, 3 Nov 2012 14:28:31 -0700 Subject: [PATCH 11/14] JS: Fix account sequence caching. --- js/remote.js | 65 +++++++++++++++++++++++++++++++++++++--------------- 1 file changed, 47 insertions(+), 18 deletions(-) diff --git a/js/remote.js b/js/remote.js index e43bb2e77..84a86d309 100644 --- a/js/remote.js +++ b/js/remote.js @@ -36,11 +36,12 @@ var config = require('../test/config.js'); var Request = function (remote, command) { var self = this; - this.message = { + this.message = { 'command' : command, 'id' : undefined, }; - this.remote = remote; + this.remote = remote; + this.requested = false; this.on('request', function () { self.request_default(); @@ -68,7 +69,10 @@ Request.prototype.request = function (remote) { }; Request.prototype.request_default = function () { - this.remote.request(this); + if (!this.requested) { + this.requested = true; + this.remote.request(this); + } }; Request.prototype.ledger_choose = function (current) { @@ -572,6 +576,7 @@ Remote.prototype.submit = function (transaction) { else { if (!transaction.transaction.Sequence) { transaction.transaction.Sequence = this.account_seq(transaction.transaction.Account, 'ADVANCE'); + // console.log("Sequence: %s", transaction.transaction.Sequence); } if (!transaction.transaction.Sequence) { @@ -581,7 +586,7 @@ Remote.prototype.submit = function (transaction) { // Try again. self.submit(transaction); }) - .on('error', function (message) { + .on('error_account_seq_cache', function (message) { // XXX Maybe be smarter about this. Don't want to trust an untrusted server for this seq number. // Look in the current ledger. @@ -590,7 +595,7 @@ Remote.prototype.submit = function (transaction) { // Try again. self.submit(transaction); }) - .on('error', function (message) { + .on('error_account_seq_cache', function (message) { // Forward errors. transaction.emit('error', message); }) @@ -685,6 +690,11 @@ Remote.prototype.account_seq = function (account, advance) { seq = account_info.seq; if (advance) account_info.seq += 1; + + // console.log("cached: %s current=%d next=%d", account, seq, account_info.seq); + } + else { + // console.log("uncached: %s", account); } return seq; @@ -701,21 +711,40 @@ Remote.prototype.set_account_seq = function (account, seq) { // Return a request to refresh accounts[account].seq. Remote.prototype.account_seq_cache = function (account, current) { var self = this; - var request = this.request_ledger_entry('account_root'); + var request; + + if (!self.accounts[account]) self.accounts[account] = {}; + + var account_info = self.accounts[account]; + + request = account_info.caching_seq_request; + if (!request) { + // console.log("starting: %s", account); + request = self.request_ledger_entry('account_root') + .account_root(account) + .ledger_choose(current) + .on('success', function (message) { + delete account_info.caching_seq_request; + + var seq = message.node.Sequence; + + account_info.seq = seq; + + // console.log("caching: %s %d", account, seq); + // If the caller also waits for 'success', they might run before this. + request.emit('success_account_seq_cache', message); + }) + .on('error', function (message) { + // console.log("error: %s", account); + delete account_info.caching_seq_request; + + request.emit('error_account_seq_cache', message); + }); + + account_info.caching_seq_request = request; + } return request - .account_root(account) - .ledger_choose(current) - .on('success', function (message) { - var seq = message.node.Sequence; - - if (!self.accounts[account]) self.accounts[account] = {}; - - self.accounts[account].seq = seq; - - // If the caller also waits for 'success', they might run before this. - request.emit('success_account_seq_cache'); - }); }; // Mark an account's root node as dirty. From f5231f353f8095c2b17176127a950a18f0074785 Mon Sep 17 00:00:00 2001 From: Arthur Britto Date: Sat, 3 Nov 2012 14:28:59 -0700 Subject: [PATCH 12/14] UT: Add credit_limits helper. --- test/send-test.js | 88 ++++++++++++++++------------------------------- test/testutils.js | 25 ++++++++++++++ 2 files changed, 55 insertions(+), 58 deletions(-) diff --git a/test/send-test.js b/test/send-test.js index 990afe212..cbc9c1013 100644 --- a/test/send-test.js +++ b/test/send-test.js @@ -464,14 +464,14 @@ buster.testCase("Indirect ripple", { testutils.create_accounts(self.remote, "root", "10000", ["alice", "bob", "mtgox"], callback); }, function (callback) { - self.what = "Set alice's limit."; + self.what = "Set credit limits."; - 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); + testutils.credit_limits(self.remote, + { + "alice" : "600/USD/mtgox", + "bob" : "700/USD/mtgox", + }, + callback); }, function (callback) { self.what = "Give alice some mtgox."; @@ -534,14 +534,14 @@ buster.testCase("Indirect ripple", { testutils.create_accounts(self.remote, "root", "10000", ["alice", "bob", "mtgox"], callback); }, function (callback) { - self.what = "Set alice's limit."; + self.what = "Set credit limits."; - 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); + testutils.credit_limits(self.remote, + { + "alice" : "600/USD/mtgox", + "bob" : "700/USD/mtgox", + }, + callback); }, function (callback) { self.what = "Give alice some mtgox."; @@ -593,29 +593,15 @@ buster.testCase("Indirect ripple", { testutils.create_accounts(self.remote, "root", "10000", ["alice", "bob", "carol", "amazon", "mtgox"], callback); }, function (callback) { - self.what = "Set alice's limit with bob."; + self.what = "Set credit limits."; - testutils.credit_limit(self.remote, "bob", "600/USD/alice", callback); - }, - function (callback) { - self.what = "Set alice's limit with carol."; - - testutils.credit_limit(self.remote, "carol", "700/USD/alice", callback); - }, - function (callback) { - self.what = "Set bob's mtgox limit."; - - testutils.credit_limit(self.remote, "bob", "1000/USD/mtgox", callback); - }, - function (callback) { - self.what = "Set carol's mtgox limit."; - - testutils.credit_limit(self.remote, "carol", "1000/USD/mtgox", callback); - }, - function (callback) { - self.what = "Set amazon's mtgox limit."; - - testutils.credit_limit(self.remote, "amazon", "2000/USD/mtgox", callback); + testutils.credit_limits(self.remote, + { + "amazon" : "2000/USD/mtgox", + "bob" : [ "600/USD/alice", "1000/USD/mtgox" ], + "carol" : [ "700/USD/alice", "1000/USD/mtgox" ], + }, + callback); }, function (callback) { self.what = "Give bob some mtgox."; @@ -675,29 +661,15 @@ buster.testCase("Indirect ripple", { testutils.transfer_rate(self.remote, "mtgox", 1.1e9, callback); }, function (callback) { - self.what = "Set alice's limit with bob."; + self.what = "Set credit limits."; - testutils.credit_limit(self.remote, "bob", "600/USD/alice", callback); - }, - function (callback) { - self.what = "Set alice's limit with carol."; - - testutils.credit_limit(self.remote, "carol", "700/USD/alice", callback); - }, - function (callback) { - self.what = "Set bob's mtgox limit."; - - testutils.credit_limit(self.remote, "bob", "1000/USD/mtgox", callback); - }, - function (callback) { - self.what = "Set carol's mtgox limit."; - - testutils.credit_limit(self.remote, "carol", "1000/USD/mtgox", callback); - }, - function (callback) { - self.what = "Set amazon's mtgox limit."; - - testutils.credit_limit(self.remote, "amazon", "2000/USD/mtgox", callback); + testutils.credit_limits(self.remote, + { + "amazon" : "2000/USD/mtgox", + "bob" : [ "600/USD/alice", "1000/USD/mtgox" ], + "carol" : [ "700/USD/alice", "1000/USD/mtgox" ], + }, + callback); }, function (callback) { self.what = "Give bob some mtgox."; diff --git a/test/testutils.js b/test/testutils.js index 4d1d9ec2d..097032876 100644 --- a/test/testutils.js +++ b/test/testutils.js @@ -129,6 +129,30 @@ var credit_limit = function (remote, src, amount, callback) { .submit(); }; +var credit_limits = function (remote, balances, callback) { + assert(3 === arguments.length); + + var limits = []; + + for (var src in balances) { + var values_src = balances[src]; + var values = 'string' === typeof values_src ? [ values_src ] : values_src; + + for (var index in values) { + limits.push( { "source" : src, "amount" : values[index] } ); + } + } + + async.every(limits, + function (limit, callback) { + credit_limit(remote, limit.source, limit.amount, + function (mismatch) { callback(!mismatch); }); + }, + function (every) { + callback(!every); + }); +}; + var payment = function (remote, src, dst, amount, callback) { assert(5 === arguments.length); @@ -212,6 +236,7 @@ var verify_balances = function (remote, balances, callback) { exports.build_setup = build_setup; exports.create_accounts = create_accounts; exports.credit_limit = credit_limit; +exports.credit_limits = credit_limits; exports.payment = payment; exports.build_teardown = build_teardown; exports.transfer_rate = transfer_rate; From 0f0cc86ca111ba120bc2d1befe9ce80f6e810b14 Mon Sep 17 00:00:00 2001 From: Arthur Britto Date: Sat, 3 Nov 2012 14:52:33 -0700 Subject: [PATCH 13/14] UT: Add payments helper. --- test/send-test.js | 52 ++++++++++++++++++++++------------------------- test/testutils.js | 28 +++++++++++++++++++++++++ 2 files changed, 52 insertions(+), 28 deletions(-) diff --git a/test/send-test.js b/test/send-test.js index cbc9c1013..d5f4cb969 100644 --- a/test/send-test.js +++ b/test/send-test.js @@ -474,14 +474,13 @@ buster.testCase("Indirect ripple", { callback); }, function (callback) { - self.what = "Give alice some mtgox."; + self.what = "Distribute funds."; - 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); + testutils.payments(self.remote, + { + "mtgox" : [ "70/USD/alice", "50/USD/bob" ], + }, + callback); }, function (callback) { self.what = "Verify alice balance with mtgox."; @@ -544,14 +543,13 @@ buster.testCase("Indirect ripple", { callback); }, function (callback) { - self.what = "Give alice some mtgox."; + self.what = "Distribute funds."; - 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); + testutils.payments(self.remote, + { + "mtgox" : [ "70/USD/alice", "50/USD/bob" ], + }, + callback); }, function (callback) { self.what = "Alice sends via a path"; @@ -604,14 +602,13 @@ buster.testCase("Indirect ripple", { callback); }, function (callback) { - self.what = "Give bob some mtgox."; + self.what = "Distribute funds."; - testutils.payment(self.remote, "mtgox", "bob", "100/USD/mtgox", callback); - }, - function (callback) { - self.what = "Give carol some mtgox."; - - testutils.payment(self.remote, "mtgox", "carol", "100/USD/mtgox", callback); + testutils.payments(self.remote, + { + "mtgox" : [ "100/USD/bob", "100/USD/carol" ], + }, + callback); }, function (callback) { self.what = "Alice pays amazon via multiple paths"; @@ -672,14 +669,13 @@ buster.testCase("Indirect ripple", { callback); }, function (callback) { - self.what = "Give bob some mtgox."; + self.what = "Distribute funds."; - testutils.payment(self.remote, "mtgox", "bob", "100/USD/mtgox", callback); - }, - function (callback) { - self.what = "Give carol some mtgox."; - - testutils.payment(self.remote, "mtgox", "carol", "100/USD/mtgox", callback); + testutils.payments(self.remote, + { + "mtgox" : [ "100/USD/bob", "100/USD/carol" ], + }, + callback); }, function (callback) { self.what = "Alice pays amazon via multiple paths"; diff --git a/test/testutils.js b/test/testutils.js index 097032876..a3a03466c 100644 --- a/test/testutils.js +++ b/test/testutils.js @@ -171,6 +171,33 @@ var payment = function (remote, src, dst, amount, callback) { .submit(); }; +var payments = function (remote, balances, callback) { + assert(3 === arguments.length); + + var sends = []; + + for (var src in balances) { + var values_src = balances[src]; + var values = 'string' === typeof values_src ? [ values_src ] : values_src; + + for (var index in values) { + var amount_json = values[index]; + var amount = Amount.from_json(amount_json); + + sends.push( { "source" : src, "destination" : amount.issuer.to_json(), "amount" : amount_json } ); + } + } + + async.every(sends, + function (send, callback) { + payment(remote, send.source, send.destination, send.amount, + function (mismatch) { callback(!mismatch); }); + }, + function (every) { + callback(!every); + }); +}; + var transfer_rate = function (remote, src, billionths, callback) { assert(4 === arguments.length); @@ -238,6 +265,7 @@ exports.create_accounts = create_accounts; exports.credit_limit = credit_limit; exports.credit_limits = credit_limits; exports.payment = payment; +exports.payments = payments; exports.build_teardown = build_teardown; exports.transfer_rate = transfer_rate; exports.verify_balance = verify_balance; From 80450598940312718aa9d2a8a601bee09316d035 Mon Sep 17 00:00:00 2001 From: Arthur Britto Date: Sat, 3 Nov 2012 15:01:22 -0700 Subject: [PATCH 14/14] JS: Add is_native() to Amount. --- test/testutils.js | 32 +++++++++++++++++++------------- 1 file changed, 19 insertions(+), 13 deletions(-) diff --git a/test/testutils.js b/test/testutils.js index a3a03466c..c11fdaf04 100644 --- a/test/testutils.js +++ b/test/testutils.js @@ -221,21 +221,27 @@ var verify_balance = function (remote, src, amount_json, callback) { assert(4 === arguments.length); var amount = Amount.from_json(amount_json); - remote.request_ripple_balance(src, amount.issuer.to_json(), amount.currency.to_json(), 'CURRENT') - .once('ripple_state', function (m) { -// console.log("BALANCE: %s", JSON.stringify(m)); -// console.log("account_balance: %s", m.account_balance.to_text_full()); -// console.log("account_limit: %s", m.account_limit.to_text_full()); -// console.log("issuer_balance: %s", m.issuer_balance.to_text_full()); -// console.log("issuer_limit: %s", m.issuer_limit.to_text_full()); + if (amount.is_native()) { + // XXX Not implemented. + callback(false); + } + else { + remote.request_ripple_balance(src, amount.issuer.to_json(), amount.currency.to_json(), 'CURRENT') + .once('ripple_state', function (m) { + // console.log("BALANCE: %s", JSON.stringify(m)); + // console.log("account_balance: %s", m.account_balance.to_text_full()); + // console.log("account_limit: %s", m.account_limit.to_text_full()); + // console.log("issuer_balance: %s", m.issuer_balance.to_text_full()); + // console.log("issuer_limit: %s", m.issuer_limit.to_text_full()); - if (!m.account_balance.equals(amount)) { - console.log("verify_balance: failed: %s vs %s is %s", src, amount_json, amount.to_text_full()); - } + if (!m.account_balance.equals(amount)) { + console.log("verify_balance: failed: %s vs %s is %s", src, amount_json, amount.to_text_full()); + } - callback(!m.account_balance.equals(amount)); - }) - .request(); + callback(!m.account_balance.equals(amount)); + }) + .request(); + } }; var verify_balances = function (remote, balances, callback) {