From 78c867d714853ec58abe9f94fbadc22d58755f27 Mon Sep 17 00:00:00 2001 From: Arthur Britto Date: Thu, 25 Apr 2013 15:23:53 -0700 Subject: [PATCH] Add tfSell support for OfferCreate. --- src/cpp/ripple/Amount.cpp | 40 ++--- src/cpp/ripple/OfferCreateTransactor.cpp | 33 ++-- src/cpp/ripple/OfferCreateTransactor.h | 1 + src/cpp/ripple/SerializedTypes.h | 1 + src/cpp/ripple/TransactionFormats.h | 3 +- test/offer-test.js | 191 +++++++++++++++++++++++ 6 files changed, 238 insertions(+), 31 deletions(-) diff --git a/src/cpp/ripple/Amount.cpp b/src/cpp/ripple/Amount.cpp index c3d2c7a571..88488cfdc2 100644 --- a/src/cpp/ripple/Amount.cpp +++ b/src/cpp/ripple/Amount.cpp @@ -986,18 +986,19 @@ STAmount STAmount::setRate(uint64 rate) return STAmount(CURRENCY_ONE, ACCOUNT_ONE, mantissa, exponent); } +// Existing offer is on the books. +// Price is offer owner's, which might be better for taker. +// Taker pays what they can. // Taker gets all taker can pay for with saTakerFunds/uTakerPaysRate, limited by saOfferPays and saOfferFunds/uOfferPaysRate. -// -// Existing offer is on the books. Offer owner gets their rate. -// -// Taker pays what they can. If taker is an offer, doesn't matter what rate taker is. Taker is spending at same or better rate -// than they wanted. Taker should consider themselves as wanting to buy X amount. Taker is willing to pay at most the rate of Y/X -// each. Therefore, after having some part of their offer fulfilled at a better rate their offer should be reduced accordingly. -// -// YYY Could have a flag for spend up to behaviour vs current limit spend rate. +// If taker is an offer, taker is spending at same or better rate than they wanted. +// Taker should consider themselves as wanting to buy X amount. +// Taker is willing to pay at most the rate of Y/X each. +// Buy semantics: +// - After having some part of their offer fulfilled at a better rate their offer should be reduced accordingly. // // There are no quality costs for offer vs offer taking. // +// --> bSell: True for sell semantics. // --> uTakerPaysRate: >= QUALITY_ONE | TransferRate for third party IOUs paid by taker. // --> uOfferPaysRate: >= QUALITY_ONE | TransferRate for third party IOUs paid by offer owner. // --> saOfferRate: Original saOfferGets/saOfferPays, when offer was made. @@ -1012,6 +1013,7 @@ STAmount STAmount::setRate(uint64 rate) // <-- saTakerIssuerFee: Actual // <-- saOfferIssuerFee: Actual bool STAmount::applyOffer( + const bool bSell, const uint32 uTakerPaysRate, const uint32 uOfferPaysRate, const STAmount& saOfferRate, const STAmount& saOfferFunds, const STAmount& saTakerFunds, @@ -1022,21 +1024,21 @@ bool STAmount::applyOffer( { saOfferGets.throwComparable(saTakerFunds); - assert(!saOfferFunds.isZero() && !saTakerFunds.isZero()); // Must have funds. - assert(!saOfferGets.isZero() && !saOfferPays.isZero()); // Must not be a null offer. + assert(!saOfferFunds.isZero() && !saTakerFunds.isZero()); // Both must have funds. + assert(saOfferGets.isPositive() && saOfferPays.isPositive()); // Must not be a null offer. // Limit offerer funds available, by transfer fees. STAmount saOfferFundsAvailable = QUALITY_ONE == uOfferPaysRate - ? saOfferFunds - : STAmount::divide(saOfferFunds, STAmount(CURRENCY_ONE, ACCOUNT_ONE, uOfferPaysRate, -9)); + ? saOfferFunds // As is. + : STAmount::divide(saOfferFunds, STAmount(CURRENCY_ONE, ACCOUNT_ONE, uOfferPaysRate, -9)); // Reduce by offer fees. cLog(lsINFO) << "applyOffer: uOfferPaysRate=" << uOfferPaysRate; cLog(lsINFO) << "applyOffer: saOfferFundsAvailable=" << saOfferFundsAvailable.getFullText(); // Limit taker funds available, by transfer fees. STAmount saTakerFundsAvailable = QUALITY_ONE == uTakerPaysRate - ? saTakerFunds - : STAmount::divide(saTakerFunds, STAmount(CURRENCY_ONE, ACCOUNT_ONE, uTakerPaysRate, -9)); + ? saTakerFunds // As is. + : STAmount::divide(saTakerFunds, STAmount(CURRENCY_ONE, ACCOUNT_ONE, uTakerPaysRate, -9)); // Reduce by taker fees. cLog(lsINFO) << "applyOffer: TAKER_FEES=" << STAmount(CURRENCY_ONE, ACCOUNT_ONE, uTakerPaysRate, -9).getFullText(); cLog(lsINFO) << "applyOffer: uTakerPaysRate=" << uTakerPaysRate; @@ -1069,12 +1071,14 @@ bool STAmount::applyOffer( cLog(lsINFO) << "applyOffer: saTakerPaysMax=" << saTakerPaysMax.getFullText(); STAmount saTakerGetsMax = saTakerPaysMax >= saOfferGetsAvailable ? saOfferPaysAvailable // Potentially take entire offer. Avoid math shenanigans. - : std::min(saOfferPaysAvailable, divRound(saTakerPaysMax, saOfferRate, saTakerGets, !saTakerGets.isNative())); // Taker a portion of offer. + : std::min(saOfferPaysAvailable, divRound(saTakerPaysMax, saOfferRate, saTakerGets, true)); // Taker a portion of offer. cLog(lsINFO) << "applyOffer: saOfferRate=" << saOfferRate.getFullText(); cLog(lsINFO) << "applyOffer: saTakerGetsMax=" << saTakerGetsMax.getFullText(); - saTakerGot = std::min(saTakerGets, saTakerGetsMax); // Limit by wanted. - saTakerPaid = saTakerGot == saOfferPaysAvailable + saTakerGot = bSell + ? saTakerGetsMax // Get all available that are paid for. + : std::min(saTakerGets, saTakerGetsMax); // Limit by wanted. + saTakerPaid = saTakerGot >= saOfferPaysAvailable ? saOfferGetsAvailable : std::min(saOfferGetsAvailable, mulRound(saTakerGot, saOfferRate, saTakerFunds, true)); @@ -1120,7 +1124,7 @@ bool STAmount::applyOffer( cLog(lsINFO) << "applyOffer: saTakerGot=" << saTakerGot.getFullText(); - return saTakerGot >= saOfferPaysAvailable; + return saTakerGot >= saOfferPaysAvailable; // True, if consumed offer. } STAmount STAmount::getPay(const STAmount& offerOut, const STAmount& offerIn, const STAmount& needed) diff --git a/src/cpp/ripple/OfferCreateTransactor.cpp b/src/cpp/ripple/OfferCreateTransactor.cpp index b6f5e9fd92..2bcdd60129 100644 --- a/src/cpp/ripple/OfferCreateTransactor.cpp +++ b/src/cpp/ripple/OfferCreateTransactor.cpp @@ -92,6 +92,7 @@ bool OfferCreateTransactor::bValidOffer( TER OfferCreateTransactor::takeOffers( const bool bOpenLedger, const bool bPassive, + const bool bSell, const uint256& uBookBase, const uint160& uTakerAccountID, SLE::ref sleTakerAccount, @@ -110,7 +111,7 @@ TER OfferCreateTransactor::takeOffers( assert(saTakerPays && saTakerGets); - cLog(lsINFO) << "takeOffers: against book: " << uBookBase.ToString(); + cLog(lsINFO) << "takeOffers: bSell: " << bSell << ": against book: " << uBookBase.ToString(); LedgerEntrySet& lesActive = mEngine->getNodes(); uint256 uTipIndex = uBookBase; @@ -244,6 +245,7 @@ TER OfferCreateTransactor::takeOffers( cLog(lsINFO) << "takeOffers: applyOffer: saTakerGets: " << saTakerGets.getFullText(); bool bOfferDelete = STAmount::applyOffer( + bSell, lesActive.rippleTransferRate(uTakerAccountID, uOfferOwnerID, uTakerPaysAccountID), lesActive.rippleTransferRate(uOfferOwnerID, uTakerAccountID, uTakerGetsAccountID), saOfferRate, @@ -313,19 +315,24 @@ TER OfferCreateTransactor::takeOffers( if (tesSUCCESS == terResult) terResult = lesActive.accountSend(uTakerAccountID, uOfferOwnerID, saSubTakerPaid); // Taker pays offer owner. - // Reduce amount considered paid by taker's rate (not actual cost). - STAmount saTakerCould = saTakerPays - saTakerPaid; // Taker could pay. - if (saTakerFunds < saTakerCould) - saTakerCould = saTakerFunds; + if (!bSell) + { + // Buy semantics: Reduce amount considered paid by taker's rate. Not by actual cost which is lower. + // That is, take less as to just satify our buy requirement. + STAmount saTakerCould = saTakerPays - saTakerPaid; // Taker could pay. + if (saTakerFunds < saTakerCould) + saTakerCould = saTakerFunds; - STAmount saTakerUsed = STAmount::multiply(saSubTakerGot, saTakerRate, saTakerPays); + STAmount saTakerUsed = STAmount::multiply(saSubTakerGot, saTakerRate, saTakerPays); - cLog(lsINFO) << "takeOffers: applyOffer: saTakerCould: " << saTakerCould.getFullText(); - cLog(lsINFO) << "takeOffers: applyOffer: saSubTakerGot: " << saSubTakerGot.getFullText(); - cLog(lsINFO) << "takeOffers: applyOffer: saTakerRate: " << saTakerRate.getFullText(); - cLog(lsINFO) << "takeOffers: applyOffer: saTakerUsed: " << saTakerUsed.getFullText(); + cLog(lsINFO) << "takeOffers: applyOffer: saTakerCould: " << saTakerCould.getFullText(); + cLog(lsINFO) << "takeOffers: applyOffer: saSubTakerGot: " << saSubTakerGot.getFullText(); + cLog(lsINFO) << "takeOffers: applyOffer: saTakerRate: " << saTakerRate.getFullText(); + cLog(lsINFO) << "takeOffers: applyOffer: saTakerUsed: " << saTakerUsed.getFullText(); - saTakerPaid += std::min(saTakerCould, saTakerUsed); + saSubTakerPaid = std::min(saTakerCould, saTakerUsed); + } + saTakerPaid += saSubTakerPaid; saTakerGot += saSubTakerGot; if (tesSUCCESS == terResult) @@ -362,6 +369,7 @@ TER OfferCreateTransactor::doApply() const bool bPassive = isSetBit(uTxFlags, tfPassive); const bool bImmediateOrCancel = isSetBit(uTxFlags, tfImmediateOrCancel); const bool bFillOrKill = isSetBit(uTxFlags, tfFillOrKill); + const bool bSell = isSetBit(uTxFlags, tfSell); STAmount saTakerPays = mTxn.getFieldAmount(sfTakerPays); STAmount saTakerGets = mTxn.getFieldAmount(sfTakerGets); @@ -498,12 +506,13 @@ TER OfferCreateTransactor::doApply() terResult = takeOffers( bOpenLedger, bPassive, + bSell, uTakeBookBase, mTxnAccountID, sleCreator, saTakerGets, // Reverse as we are the taker for taking. saTakerPays, - saPaid, // How much would have spent at full price. + saPaid, // Buy semantics: how much would have sold at full price. Sell semantics: how much was sold. saGot, // How much was got. bUnfunded); diff --git a/src/cpp/ripple/OfferCreateTransactor.h b/src/cpp/ripple/OfferCreateTransactor.h index dfe8696bca..795db826e6 100644 --- a/src/cpp/ripple/OfferCreateTransactor.h +++ b/src/cpp/ripple/OfferCreateTransactor.h @@ -21,6 +21,7 @@ protected: TER takeOffers( const bool bOpenLedger, const bool bPassive, + const bool bSell, const uint256& uBookBase, const uint160& uTakerAccountID, SLE::ref sleTakerAccount, diff --git a/src/cpp/ripple/SerializedTypes.h b/src/cpp/ripple/SerializedTypes.h index 5b0df2fedf..e3921ecaa2 100644 --- a/src/cpp/ripple/SerializedTypes.h +++ b/src/cpp/ripple/SerializedTypes.h @@ -451,6 +451,7 @@ public: // Someone is offering X for Y, I try to pay Z, how much do I get? // And what's left of the offer? And how much do I actually pay? static bool applyOffer( + const bool bSell, const uint32 uTakerPaysRate, const uint32 uOfferPaysRate, const STAmount& saOfferRate, const STAmount& saOfferFunds, const STAmount& saTakerFunds, diff --git a/src/cpp/ripple/TransactionFormats.h b/src/cpp/ripple/TransactionFormats.h index b7a5d079b6..6e3d6a6dab 100644 --- a/src/cpp/ripple/TransactionFormats.h +++ b/src/cpp/ripple/TransactionFormats.h @@ -74,7 +74,8 @@ const uint32 tfAccountSetMask = ~(tfRequireDestTag|tfOptionalDestTag const uint32 tfPassive = 0x00010000; const uint32 tfImmediateOrCancel = 0x00020000; const uint32 tfFillOrKill = 0x00040000; -const uint32 tfOfferCreateMask = ~(tfPassive|tfImmediateOrCancel|tfFillOrKill); +const uint32 tfSell = 0x00080000; +const uint32 tfOfferCreateMask = ~(tfPassive|tfImmediateOrCancel|tfFillOrKill|tfSell); // Payment flags: const uint32 tfNoRippleDirect = 0x00010000; diff --git a/test/offer-test.js b/test/offer-test.js index 13724f6f5f..113d6b0000 100644 --- a/test/offer-test.js +++ b/test/offer-test.js @@ -1547,4 +1547,195 @@ buster.testCase("Offer tests 3", { }); }, }); + +buster.testCase("Offer tfSell", { + 'setUp' : testutils.build_setup(), + // 'setUp' : testutils.build_setup({ verbose: true }), + // 'setUp' : testutils.build_setup({ verbose: true, standalone: true }), + 'tearDown' : testutils.build_teardown(), + + "basic sell" : + function (done) { + var self = this; + var final_create; + + async.waterfall([ + function (callback) { + // Provide micro amounts to compensate for fees to make results round nice. + self.what = "Create accounts."; + + testutils.create_accounts(self.remote, "root", "350.000020", ["alice", "bob", "mtgox"], callback); + }, + function (callback) { + self.what = "Set limits."; + + testutils.credit_limits(self.remote, + { + "alice" : "1000/USD/mtgox", + "bob" : "1000/USD/mtgox", + }, + callback); + }, + function (callback) { + self.what = "Distribute funds."; + + testutils.payments(self.remote, + { + "mtgox" : [ "500/USD/bob" ], + }, + callback); + }, + function (callback) { + self.what = "Create offer bob."; + + self.remote.transaction() + .offer_create("bob", "200.0", "200/USD/mtgox") + .set_flags('Sell') // Should not matter at all. + .on('proposed', function (m) { + // console.log("proposed: offer_create: %s", json.stringify(m)); + callback(m.result !== 'tesSUCCESS'); + + seq_carol = m.tx_json.sequence; + }) + .submit(); + }, + function (callback) { + // Alice has 350 fees - a reserve of 50 = 250 reserve = 100 available. + // Ask for more than available to prove reserve works. + self.what = "Create offer alice."; + + self.remote.transaction() + .offer_create("alice", "100/USD/mtgox", "100.0") + .set_flags('Sell') // Should not matter at all. + .on('proposed', function (m) { + // console.log("proposed: offer_create: %s", json.stringify(m)); + callback(m.result !== 'tesSUCCESS'); + + seq_carol = m.tx_json.sequence; + }) + .submit(); + }, +// function (callback) { +// self.what = "Display ledger"; +// +// self.remote.request_ledger('current', true) +// .on('success', function (m) { +// console.log("Ledger: %s", JSON.stringify(m, undefined, 2)); +// +// callback(); +// }) +// .request(); +// }, + function (callback) { + self.what = "Verify balances."; + + testutils.verify_balances(self.remote, + { + "alice" : [ "100/USD/mtgox", "250.0" ], + "bob" : "400/USD/mtgox", + }, + callback); + }, + ], function (error) { + // console.log("result: error=%s", error); + buster.refute(error); + + done(); + }); + }, + + "2x sell exceed limit" : + function (done) { + var self = this; + var final_create; + + async.waterfall([ + function (callback) { + // Provide micro amounts to compensate for fees to make results round nice. + self.what = "Create accounts."; + + testutils.create_accounts(self.remote, "root", "350.000020", ["alice", "bob", "mtgox"], callback); + }, + function (callback) { + self.what = "Set limits."; + + testutils.credit_limits(self.remote, + { + "alice" : "150/USD/mtgox", + "bob" : "1000/USD/mtgox", + }, + callback); + }, + function (callback) { + self.what = "Distribute funds."; + + testutils.payments(self.remote, + { + "mtgox" : [ "500/USD/bob" ], + }, + callback); + }, + function (callback) { + self.what = "Create offer bob."; + + // Taker pays 200 XRP for 100 USD. + // Selling USD. + self.remote.transaction() + .offer_create("bob", "100.0", "200/USD/mtgox") + .on('proposed', function (m) { + // console.log("proposed: offer_create: %s", json.stringify(m)); + callback(m.result !== 'tesSUCCESS'); + + seq_carol = m.tx_json.sequence; + }) + .submit(); + }, + function (callback) { + // Alice has 350 fees - a reserve of 50 = 250 reserve = 100 available. + // Ask for more than available to prove reserve works. + self.what = "Create offer alice."; + + // Taker pays 100 USD for 100 XRP. + // Selling XRP. + // Will sell all 100 XRP and get more USD than asked for. + self.remote.transaction() + .offer_create("alice", "100/USD/mtgox", "100.0") + .set_flags('Sell') + .on('proposed', function (m) { + // console.log("proposed: offer_create: %s", json.stringify(m)); + callback(m.result !== 'tesSUCCESS'); + + seq_carol = m.tx_json.sequence; + }) + .submit(); + }, +// function (callback) { +// self.what = "Display ledger"; +// +// self.remote.request_ledger('current', true) +// .on('success', function (m) { +// console.log("Ledger: %s", JSON.stringify(m, undefined, 2)); +// +// callback(); +// }) +// .request(); +// }, + function (callback) { + self.what = "Verify balances."; + + testutils.verify_balances(self.remote, + { + "alice" : [ "200/USD/mtgox", "250.0" ], + "bob" : "300/USD/mtgox", + }, + callback); + }, + ], function (error) { + // console.log("result: error=%s", error); + buster.refute(error); + + done(); + }); + }, +}); // vim:sw=2:sts=2:ts=8:et