diff --git a/modules/ripple_data/protocol/ripple_TxFormat.cpp b/modules/ripple_data/protocol/ripple_TxFormat.cpp index b8a45ae6e8..f9542030ad 100644 --- a/modules/ripple_data/protocol/ripple_TxFormat.cpp +++ b/modules/ripple_data/protocol/ripple_TxFormat.cpp @@ -43,6 +43,7 @@ void TFInit () << SOElement (sfTakerPays, SOE_REQUIRED) << SOElement (sfTakerGets, SOE_REQUIRED) << SOElement (sfExpiration, SOE_OPTIONAL) + << SOElement (sfOfferSequence, SOE_OPTIONAL) ; DECLARE_TF (OfferCancel, ttOFFER_CANCEL) diff --git a/src/cpp/ripple/OfferCreateTransactor.cpp b/src/cpp/ripple/OfferCreateTransactor.cpp index 45ff33883b..6c547da4e6 100644 --- a/src/cpp/ripple/OfferCreateTransactor.cpp +++ b/src/cpp/ripple/OfferCreateTransactor.cpp @@ -372,45 +372,48 @@ TER OfferCreateTransactor::takeOffers ( TER OfferCreateTransactor::doApply () { WriteLog (lsTRACE, OfferCreateTransactor) << "OfferCreate> " << mTxn.getJson (0); - const uint32 uTxFlags = mTxn.getFlags (); - 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); + const uint32 uTxFlags = mTxn.getFlags (); + 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); - if (!saTakerPays.isLegalNet () || !saTakerGets.isLegalNet ()) - return temBAD_AMOUNT; + if (!saTakerPays.isLegalNet () || !saTakerGets.isLegalNet ()) + return temBAD_AMOUNT; - WriteLog (lsTRACE, OfferCreateTransactor) << boost::str (boost::format ("OfferCreate: saTakerPays=%s saTakerGets=%s") - % saTakerPays.getFullText () - % saTakerGets.getFullText ()); + WriteLog (lsTRACE, OfferCreateTransactor) << boost::str (boost::format ("OfferCreate: saTakerPays=%s saTakerGets=%s") + % saTakerPays.getFullText () + % saTakerGets.getFullText ()); - const uint160 uPaysIssuerID = saTakerPays.getIssuer (); - const uint160 uGetsIssuerID = saTakerGets.getIssuer (); - const uint32 uExpiration = mTxn.getFieldU32 (sfExpiration); - const bool bHaveExpiration = mTxn.isFieldPresent (sfExpiration); - const uint32 uSequence = mTxn.getSequence (); + const uint160 uPaysIssuerID = saTakerPays.getIssuer (); + const uint160 uGetsIssuerID = saTakerGets.getIssuer (); + const uint32 uExpiration = mTxn.getFieldU32 (sfExpiration); + const bool bHaveExpiration = mTxn.isFieldPresent (sfExpiration); + const bool bHaveCancel = mTxn.isFieldPresent (sfOfferSequence); + const uint32 uCancelSequence = mTxn.getFieldU32 (sfOfferSequence); + const uint32 uAccountSequenceNext = mTxnAccount->getFieldU32 (sfSequence); + const uint32 uSequence = mTxn.getSequence (); - const uint256 uLedgerIndex = Ledger::getOfferIndex (mTxnAccountID, uSequence); + const uint256 uLedgerIndex = Ledger::getOfferIndex (mTxnAccountID, uSequence); - WriteLog (lsTRACE, OfferCreateTransactor) << "OfferCreate: Creating offer node: " << uLedgerIndex.ToString () << " uSequence=" << uSequence; + WriteLog (lsTRACE, OfferCreateTransactor) << "OfferCreate: Creating offer node: " << uLedgerIndex.ToString () << " uSequence=" << uSequence; - const uint160 uPaysCurrency = saTakerPays.getCurrency (); - const uint160 uGetsCurrency = saTakerGets.getCurrency (); - const uint64 uRate = STAmount::getRate (saTakerGets, saTakerPays); + const uint160 uPaysCurrency = saTakerPays.getCurrency (); + const uint160 uGetsCurrency = saTakerGets.getCurrency (); + const uint64 uRate = STAmount::getRate (saTakerGets, saTakerPays); - TER terResult = tesSUCCESS; - uint256 uDirectory; // Delete hints. - uint64 uOwnerNode; - uint64 uBookNode; + TER terResult = tesSUCCESS; + uint256 uDirectory; // Delete hints. + uint64 uOwnerNode; + uint64 uBookNode; - LedgerEntrySet& lesActive = mEngine->getNodes (); - LedgerEntrySet lesCheckpoint = lesActive; // Checkpoint with just fees paid. - lesActive.bumpSeq (); // Begin ledger variance. + LedgerEntrySet& lesActive = mEngine->getNodes (); + LedgerEntrySet lesCheckpoint = lesActive; // Checkpoint with just fees paid. + lesActive.bumpSeq (); // Begin ledger variance. - SLE::pointer sleCreator = mEngine->entryCache (ltACCOUNT_ROOT, Ledger::getAccountRootIndex (mTxnAccountID)); + SLE::pointer sleCreator = mEngine->entryCache (ltACCOUNT_ROOT, Ledger::getAccountRootIndex (mTxnAccountID)); if (uTxFlags & tfOfferCreateMask) { @@ -472,7 +475,35 @@ TER OfferCreateTransactor::doApply () terResult = tecUNFUNDED_OFFER; } + else if (bHaveCancel && (!uCancelSequence || uAccountSequenceNext - 1 <= uCancelSequence)) + { + WriteLog (lsINFO, OfferCreateTransactor) << "OfferCreate: uAccountSequenceNext=" << uAccountSequenceNext << " uOfferSequence=" << uCancelSequence; + terResult = temBAD_SEQUENCE; + } + + // Cancel offer. + if (tesSUCCESS == terResult && bHaveCancel) + { + const uint256 uCancelIndex = Ledger::getOfferIndex (mTxnAccountID, uCancelSequence); + SLE::pointer sleCancel = mEngine->entryCache (ltOFFER, uCancelIndex); + + if (sleCancel) + { + WriteLog (lsWARNING, OfferCancelTransactor) << "OfferCreate: uCancelSequence=" << uCancelSequence; + + terResult = mEngine->getNodes ().offerDelete (sleCancel, uCancelIndex, mTxnAccountID); + } + else + { + WriteLog (lsWARNING, OfferCancelTransactor) << "OfferCreate: offer not found: " + << RippleAddress::createHumanAccountID (mTxnAccountID) + << " : " << uCancelSequence + << " : " << uCancelIndex.ToString (); + } + } + + // Make sure authorized to hold what taker will pay. if (tesSUCCESS == terResult && !saTakerPays.isNative ()) { SLE::pointer sleTakerPays = mEngine->entryCache (ltACCOUNT_ROOT, Ledger::getAccountRootIndex (uPaysIssuerID)); diff --git a/test/offer-test.js b/test/offer-test.js index bf7a2cdadd..6d4af9584f 100644 --- a/test/offer-test.js +++ b/test/offer-test.js @@ -73,6 +73,100 @@ buster.testCase("Offer tests", { }); }, + "offer create then offer create with cancel in one ledger" : + function (done) { + var self = this; + var final_create; + var sequence_first; + var sequence_second; + var dones = 0; + + async.waterfall([ + function (callback) { + self.remote.transaction() + .offer_create("root", "500", "100/USD/root") + .on('proposed', function (m) { + // console.log("PROPOSED: offer_create: %s", JSON.stringify(m)); + callback(m.result !== 'tesSUCCESS', m); + }) + .on('final', function (m) { + // console.log("FINAL: offer_create: %s", JSON.stringify(m)); + + buster.assert.equals('tesSUCCESS', m.metadata.TransactionResult); + buster.assert(final_create); + + if (3 === ++dones) + done(); + }) + .submit(); + }, + function (m, callback) { + sequence_first = m.tx_json.Sequence; + + // Test canceling existant offer. + self.remote.transaction() + .offer_create("root", "300", "100/USD/root", undefined, sequence_first) + .on('proposed', function (m) { + // console.log("PROPOSED: offer_create: %s", JSON.stringify(m)); + callback(m.result !== 'tesSUCCESS', m); + }) + .on('final', function (m) { + // console.log("FINAL: offer_create: %s", JSON.stringify(m)); + + buster.assert.equals('tesSUCCESS', m.metadata.TransactionResult); + buster.assert(final_create); + + if (3 === ++dones) + done(); + }) + .submit(); + }, + function (m, callback) { + sequence_second = m.tx_json.Sequence; + self.what = "Verify offer canceled."; + + testutils.verify_offer_not_found(self.remote, "root", sequence_first, callback); + }, + function (callback) { + self.what = "Verify offer replaced."; + + testutils.verify_offer(self.remote, "root", sequence_second, "300", "100/USD/root", callback); + }, + function (callback) { + // Test canceling non-existant offer. + self.remote.transaction() + .offer_create("root", "400", "200/USD/root", undefined, sequence_first) + .on('proposed', function (m) { + // console.log("PROPOSED: offer_create: %s", JSON.stringify(m)); + callback(m.result !== 'tesSUCCESS', m); + }) + .on('final', function (m) { + // console.log("FINAL: offer_create: %s", JSON.stringify(m)); + + buster.assert.equals('tesSUCCESS', m.metadata.TransactionResult); + buster.assert(final_create); + + if (3 === ++dones) + done(); + }) + .submit(); + }, + function (callback) { + self.remote + .once('ledger_closed', function (message) { + // console.log("LEDGER_CLOSED: %d: %s", ledger_index, ledger_hash); + final_create = message; + }) + .ledger_accept(); + } + ], function (error) { + // console.log("result: error=%s", error); + buster.refute(error); + + done(); + }); + }, + "Offer create then self crossing offer, no trust lines with self" : function (done) { var self = this;