diff --git a/Builds/VisualStudio2015/RippleD.vcxproj b/Builds/VisualStudio2015/RippleD.vcxproj
index a060d9f4d8..40d06f1df9 100644
--- a/Builds/VisualStudio2015/RippleD.vcxproj
+++ b/Builds/VisualStudio2015/RippleD.vcxproj
@@ -4402,6 +4402,10 @@
True
True
+
+ True
+ True
+
True
True
diff --git a/Builds/VisualStudio2015/RippleD.vcxproj.filters b/Builds/VisualStudio2015/RippleD.vcxproj.filters
index c274f87049..8a1925f805 100644
--- a/Builds/VisualStudio2015/RippleD.vcxproj.filters
+++ b/Builds/VisualStudio2015/RippleD.vcxproj.filters
@@ -5205,6 +5205,9 @@
test\app
+
+ test\app
+
test\app
diff --git a/src/ripple/test/jtx/impl/ticket.cpp b/src/ripple/test/jtx/impl/ticket.cpp
index 075dd8d9c7..d080bd6951 100644
--- a/src/ripple/test/jtx/impl/ticket.cpp
+++ b/src/ripple/test/jtx/impl/ticket.cpp
@@ -46,6 +46,16 @@ create (Account const& account,
} // detail
+Json::Value
+cancel(Account const& account, std::string const & ticketId)
+{
+ Json::Value jv;
+ jv[jss::TransactionType] = "TicketCancel";
+ jv[jss::Account] = account.human();
+ jv["TicketID"] = ticketId;
+ return jv;
+}
+
} // ticket
} // jtx
diff --git a/src/ripple/test/jtx/ticket.h b/src/ripple/test/jtx/ticket.h
index 813e35b566..1aea80dd6d 100644
--- a/src/ripple/test/jtx/ticket.h
+++ b/src/ripple/test/jtx/ticket.h
@@ -97,8 +97,8 @@ create (Account const& account,
}
/** Cancel a ticket */
-//Json::Value
-//cancel (Account const& account,
+Json::Value
+cancel(Account const& account, std::string const & ticketId);
} // ticket
diff --git a/src/test/app/Ticket_test.cpp b/src/test/app/Ticket_test.cpp
new file mode 100644
index 0000000000..fcacba35a6
--- /dev/null
+++ b/src/test/app/Ticket_test.cpp
@@ -0,0 +1,403 @@
+//------------------------------------------------------------------------------
+/*
+ This file is part of rippled: https://github.com/ripple/rippled
+ Copyright (c) 2012-2016 Ripple Labs Inc.
+
+ Permission to use, copy, modify, and/or distribute this software for any
+ purpose with or without fee is hereby granted, provided that the above
+ copyright notice and this permission notice appear in all copies.
+
+ THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+*/
+//==============================================================================
+
+#include
+#include
+#include
+#include
+
+namespace ripple {
+
+class Ticket_test : public beast::unit_test::suite
+{
+ static auto constexpr idOne =
+ "00000000000000000000000000000000"
+ "00000000000000000000000000000001";
+
+ /// @brief validate metadata for a create/cancel ticket transaction and
+ /// return the 3 or 4 nodes that make-up the metadata (AffectedNodes)
+ ///
+ /// @param env current jtx env (meta will be extracted from it)
+ ///
+ /// @param other_target flag to indicate whether a Target different
+ /// from the Account was specified for the ticket (when created)
+ ///
+ /// @param expiration flag to indicate a cancellation with expiration which
+ /// causes two of the affected nodes to be swapped (in order).
+ ///
+ /// @retval std::array size 4 of json object values representing
+ /// each meta node entry. When the transaction was a cancel with differing
+ /// target and account, there will be 4 complete items, otherwise the last
+ /// entry will be an empty object
+ auto
+ checkTicketMeta(
+ test::jtx::Env& env,
+ bool other_target = false,
+ bool expiration = false)
+ {
+ using namespace std::string_literals;
+ auto const& tx = env.tx ()->getJson (0);
+ bool is_cancel = tx[jss::TransactionType] == "TicketCancel";
+
+ auto const& jvm = env.meta ()->getJson (0);
+ std::array retval;
+
+ // these are the affected nodes that we expect for
+ // a few different scenarios.
+ // tuple is index, field name, and label (LedgerEntryType)
+ std::vector<
+ std::tuple
+ > expected_nodes;
+
+ if (is_cancel && other_target)
+ {
+ expected_nodes = {
+ std::make_tuple(0, sfModifiedNode.fieldName, "AccountRoot"s),
+ std::make_tuple(
+ expiration ? 2: 1, sfModifiedNode.fieldName, "AccountRoot"s),
+ std::make_tuple(
+ expiration ? 1: 2, sfDeletedNode.fieldName, "Ticket"s),
+ std::make_tuple(3, sfDeletedNode.fieldName, "DirectoryNode"s)
+ };
+ }
+ else
+ {
+ expected_nodes = {
+ std::make_tuple(0, sfModifiedNode.fieldName, "AccountRoot"s),
+ std::make_tuple(1,
+ is_cancel ?
+ sfDeletedNode.fieldName : sfCreatedNode.fieldName,
+ "Ticket"s),
+ std::make_tuple(2,
+ is_cancel ?
+ sfDeletedNode.fieldName : sfCreatedNode.fieldName,
+ "DirectoryNode"s)
+ };
+ }
+
+ BEAST_EXPECT(jvm.isMember (sfAffectedNodes.fieldName));
+ BEAST_EXPECT(jvm[sfAffectedNodes.fieldName].isArray());
+ BEAST_EXPECT(
+ jvm[sfAffectedNodes.fieldName].size() == expected_nodes.size());
+
+ // verify the actual metadata against the expected
+ for (auto const& it : expected_nodes)
+ {
+ auto const& idx = std::get<0>(it);
+ auto const& field = std::get<1>(it);
+ auto const& type = std::get<2>(it);
+ BEAST_EXPECT(jvm[sfAffectedNodes.fieldName][idx].isMember(field));
+ retval[idx] = jvm[sfAffectedNodes.fieldName][idx][field];
+ BEAST_EXPECT(retval[idx][sfLedgerEntryType.fieldName] == type);
+ }
+
+ return retval;
+ }
+
+ void testTicketNotEnabled ()
+ {
+ testcase ("Feature Not Enabled");
+
+ using namespace test::jtx;
+ Env env {*this};
+
+ env (ticket::create (env.master), ter(temDISABLED));
+ env (ticket::cancel (env.master, idOne), ter (temDISABLED));
+ }
+
+ void testTicketCancelNonexistent ()
+ {
+ testcase ("Cancel Nonexistent");
+
+ using namespace test::jtx;
+ Env env {*this, features (featureTickets)};
+ env (ticket::cancel (env.master, idOne), ter (tecNO_ENTRY));
+ }
+
+ void testTicketCreatePreflightFail ()
+ {
+ testcase ("Create/Cancel Ticket with Bad Fee, Fail Preflight");
+
+ using namespace test::jtx;
+ Env env {*this, features (featureTickets)};
+
+ env (ticket::create (env.master), fee (XRP (-1)), ter (temBAD_FEE));
+ env (ticket::cancel (env.master, idOne), fee (XRP (-1)), ter (temBAD_FEE));
+ }
+
+ void testTicketCreateNonexistent ()
+ {
+ testcase ("Create Tickets with Nonexistent Accounts");
+
+ using namespace test::jtx;
+ Env env {*this, features (featureTickets)};
+ Account alice {"alice"};
+ env.memoize (alice);
+
+ env (ticket::create (env.master, alice), ter(tecNO_TARGET));
+
+ env (ticket::create (alice, env.master),
+ json (jss::Sequence, 1),
+ ter (terNO_ACCOUNT));
+ }
+
+ void testTicketToSelf ()
+ {
+ testcase ("Create Tickets with Same Account and Target");
+
+ using namespace test::jtx;
+ Env env {*this, features (featureTickets)};
+
+ env (ticket::create (env.master, env.master));
+ auto cr = checkTicketMeta (env);
+ auto const& jticket = cr[1];
+
+ BEAST_EXPECT(jticket[sfLedgerIndex.fieldName] ==
+ "7F58A0AE17775BA3404D55D406DD1C2E91EADD7AF3F03A26877BCE764CCB75E3");
+ BEAST_EXPECT(jticket[sfNewFields.fieldName][jss::Account] ==
+ env.master.human());
+ BEAST_EXPECT(jticket[sfNewFields.fieldName][jss::Sequence] == 1);
+ //verify that there's no `Target` saved in the ticket
+ BEAST_EXPECT(! jticket[sfNewFields.fieldName].
+ isMember(sfTarget.fieldName));
+ }
+
+ void testTicketCancelByCreator ()
+ {
+ testcase ("Create Ticket and Then Cancel by Creator");
+
+ using namespace test::jtx;
+ Env env {*this, features (featureTickets)};
+
+ // create and verify
+ env (ticket::create (env.master));
+ auto cr = checkTicketMeta (env);
+ auto const& jacct = cr[0];
+ auto const& jticket = cr[1];
+ BEAST_EXPECT(
+ jacct[sfPreviousFields.fieldName][sfOwnerCount.fieldName] == 0);
+ BEAST_EXPECT(
+ jacct[sfFinalFields.fieldName][sfOwnerCount.fieldName] == 1);
+ BEAST_EXPECT(jticket[sfNewFields.fieldName][jss::Sequence] ==
+ jacct[sfPreviousFields.fieldName][jss::Sequence]);
+ BEAST_EXPECT(jticket[sfLedgerIndex.fieldName] ==
+ "7F58A0AE17775BA3404D55D406DD1C2E91EADD7AF3F03A26877BCE764CCB75E3");
+ BEAST_EXPECT(jticket[sfNewFields.fieldName][jss::Account] ==
+ env.master.human());
+
+ // cancel
+ env (ticket::cancel(env.master, jticket[sfLedgerIndex.fieldName].asString()));
+ auto crd = checkTicketMeta (env);
+ auto const& jacctd = crd[0];
+ BEAST_EXPECT(jacctd[sfFinalFields.fieldName][jss::Sequence] == 3);
+ BEAST_EXPECT(
+ jacctd[sfFinalFields.fieldName][sfOwnerCount.fieldName] == 0);
+ }
+
+ void testTicketInsufficientReserve ()
+ {
+ testcase ("Create Ticket Insufficient Reserve");
+
+ using namespace test::jtx;
+ Env env {*this, features (featureTickets)};
+ Account alice {"alice"};
+
+ env.fund (env.current ()->fees ().accountReserve (0), alice);
+ env.close ();
+
+ env (ticket::create (alice), ter (tecINSUFFICIENT_RESERVE));
+ }
+
+ void testTicketCancelByTarget ()
+ {
+ testcase ("Create Ticket and Then Cancel by Target");
+
+ using namespace test::jtx;
+ Env env {*this, features (featureTickets)};
+ Account alice {"alice"};
+
+ env.fund (XRP (10000), alice);
+ env.close ();
+
+ // create and verify
+ env (ticket::create (env.master, alice));
+ auto cr = checkTicketMeta (env, true);
+ auto const& jacct = cr[0];
+ auto const& jticket = cr[1];
+ BEAST_EXPECT(
+ jacct[sfFinalFields.fieldName][sfOwnerCount.fieldName] == 1);
+ BEAST_EXPECT(jticket[sfLedgerEntryType.fieldName] == "Ticket");
+ BEAST_EXPECT(jticket[sfLedgerIndex.fieldName] ==
+ "C231BA31A0E13A4D524A75F990CE0D6890B800FF1AE75E51A2D33559547AC1A2");
+ BEAST_EXPECT(jticket[sfNewFields.fieldName][jss::Account] ==
+ env.master.human());
+ BEAST_EXPECT(jticket[sfNewFields.fieldName][sfTarget.fieldName] ==
+ alice.human());
+ BEAST_EXPECT(jticket[sfNewFields.fieldName][jss::Sequence] == 2);
+
+ // cancel using the target account
+ env (ticket::cancel(alice, jticket[sfLedgerIndex.fieldName].asString()));
+ auto crd = checkTicketMeta (env, true);
+ auto const& jacctd = crd[0];
+ auto const& jdir = crd[2];
+ BEAST_EXPECT(
+ jacctd[sfFinalFields.fieldName][sfOwnerCount.fieldName] == 0);
+ BEAST_EXPECT(jdir[sfLedgerIndex.fieldName] ==
+ jticket[sfLedgerIndex.fieldName]);
+ BEAST_EXPECT(jdir[sfFinalFields.fieldName][jss::Account] ==
+ env.master.human());
+ BEAST_EXPECT(jdir[sfFinalFields.fieldName][sfTarget.fieldName] ==
+ alice.human());
+ BEAST_EXPECT(jdir[sfFinalFields.fieldName][jss::Flags] == 0);
+ BEAST_EXPECT(jdir[sfFinalFields.fieldName][sfOwnerNode.fieldName] ==
+ "0000000000000000");
+ BEAST_EXPECT(jdir[sfFinalFields.fieldName][jss::Sequence] == 2);
+ }
+
+ void testTicketWithExpiration ()
+ {
+ testcase ("Create Ticket with Future Expiration");
+
+ using namespace test::jtx;
+ Env env {*this, features (featureTickets)};
+
+ // create and verify
+ uint32_t expire =
+ (env.timeKeeper ().closeTime () + 60s)
+ .time_since_epoch ().count ();
+ env (ticket::create (env.master, expire));
+ auto cr = checkTicketMeta (env);
+ auto const& jacct = cr[0];
+ auto const& jticket = cr[1];
+ BEAST_EXPECT(
+ jacct[sfPreviousFields.fieldName][sfOwnerCount.fieldName] == 0);
+ BEAST_EXPECT(
+ jacct[sfFinalFields.fieldName][sfOwnerCount.fieldName] == 1);
+ BEAST_EXPECT(jticket[sfNewFields.fieldName][jss::Sequence] ==
+ jacct[sfPreviousFields.fieldName][jss::Sequence]);
+ BEAST_EXPECT(
+ jticket[sfNewFields.fieldName][sfExpiration.fieldName] == expire);
+ }
+
+ void testTicketZeroExpiration ()
+ {
+ testcase ("Create Ticket with Zero Expiration");
+
+ using namespace test::jtx;
+ Env env {*this, features (featureTickets)};
+
+ // create and verify
+ env (ticket::create (env.master, 0u), ter (temBAD_EXPIRATION));
+ }
+
+ void testTicketWithPastExpiration ()
+ {
+ testcase ("Create Ticket with Past Expiration");
+
+ using namespace test::jtx;
+ Env env {*this, features (featureTickets)};
+
+ env.timeKeeper ().adjustCloseTime (days {2});
+ env.close ();
+
+ // create and verify
+ uint32_t expire = 60;
+ env (ticket::create (env.master, expire));
+ // in the case of past expiration, we only get
+ // one meta node entry returned
+ auto const& jvm = env.meta ()->getJson (0);
+ BEAST_EXPECT(jvm.isMember(sfAffectedNodes.fieldName));
+ BEAST_EXPECT(jvm[sfAffectedNodes.fieldName].isArray());
+ BEAST_EXPECT(jvm[sfAffectedNodes.fieldName].size() == 1);
+ BEAST_EXPECT(jvm[sfAffectedNodes.fieldName][0u].
+ isMember(sfModifiedNode.fieldName));
+ auto const& jacct =
+ jvm[sfAffectedNodes.fieldName][0u][sfModifiedNode.fieldName];
+ BEAST_EXPECT(
+ jacct[sfLedgerEntryType.fieldName] == "AccountRoot");
+ BEAST_EXPECT(jacct[sfFinalFields.fieldName][jss::Account] ==
+ env.master.human());
+ }
+
+ void testTicketAllowExpiration ()
+ {
+ testcase ("Create Ticket and Allow to Expire");
+
+ using namespace test::jtx;
+ Env env {*this, features (featureTickets)};
+
+ // create and verify
+ uint32_t expire =
+ (env.timeKeeper ().closeTime () + std::chrono::hours {3})
+ .time_since_epoch().count();
+ env (ticket::create (env.master, expire));
+ auto cr = checkTicketMeta (env);
+ auto const& jacct = cr[0];
+ auto const& jticket = cr[1];
+ BEAST_EXPECT(
+ jacct[sfPreviousFields.fieldName][sfOwnerCount.fieldName] == 0);
+ BEAST_EXPECT(
+ jacct[sfFinalFields.fieldName][sfOwnerCount.fieldName] == 1);
+ BEAST_EXPECT(
+ jticket[sfNewFields.fieldName][sfExpiration.fieldName] == expire);
+ BEAST_EXPECT(jticket[sfLedgerIndex.fieldName] ==
+ "7F58A0AE17775BA3404D55D406DD1C2E91EADD7AF3F03A26877BCE764CCB75E3");
+
+ Account alice {"alice"};
+ env.fund (XRP (10000), alice);
+ env.close ();
+
+ // now try to cancel with alice account, which should not work
+ auto jv = ticket::cancel(alice, jticket[sfLedgerIndex.fieldName].asString());
+ env (jv, ter (tecNO_PERMISSION));
+
+ // advance the ledger time to as to trigger expiration
+ env.timeKeeper ().adjustCloseTime (days {3});
+ env.close ();
+
+ // now try again - the cancel succeeds because ticket has expired
+ env (jv);
+ auto crd = checkTicketMeta (env, true, true);
+ auto const& jticketd = crd[1];
+ BEAST_EXPECT(
+ jticketd[sfFinalFields.fieldName][sfExpiration.fieldName] == expire);
+ }
+
+public:
+ void run ()
+ {
+ testTicketNotEnabled ();
+ testTicketCancelNonexistent ();
+ testTicketCreatePreflightFail ();
+ testTicketCreateNonexistent ();
+ testTicketToSelf ();
+ testTicketCancelByCreator ();
+ testTicketInsufficientReserve ();
+ testTicketCancelByTarget ();
+ testTicketWithExpiration ();
+ testTicketZeroExpiration ();
+ testTicketWithPastExpiration ();
+ testTicketAllowExpiration ();
+ }
+};
+
+BEAST_DEFINE_TESTSUITE (Ticket, tx, ripple);
+
+} // ripple
+
diff --git a/src/unity/app_test_unity.cpp b/src/unity/app_test_unity.cpp
index fc8f417e0e..e736107a0c 100644
--- a/src/unity/app_test_unity.cpp
+++ b/src/unity/app_test_unity.cpp
@@ -41,3 +41,4 @@
#include
#include
#include
+#include