diff --git a/Builds/VisualStudio2015/RippleD.vcxproj b/Builds/VisualStudio2015/RippleD.vcxproj
index 47bb10603..9015de5d8 100644
--- a/Builds/VisualStudio2015/RippleD.vcxproj
+++ b/Builds/VisualStudio2015/RippleD.vcxproj
@@ -4364,6 +4364,10 @@
True
True
+
+ True
+ True
+
True
True
diff --git a/Builds/VisualStudio2015/RippleD.vcxproj.filters b/Builds/VisualStudio2015/RippleD.vcxproj.filters
index e872feaeb..e8ff774f5 100644
--- a/Builds/VisualStudio2015/RippleD.vcxproj.filters
+++ b/Builds/VisualStudio2015/RippleD.vcxproj.filters
@@ -5151,6 +5151,9 @@
test\app
+
+ test\app
+
test\app
diff --git a/src/test/app/Freeze_test.cpp b/src/test/app/Freeze_test.cpp
new file mode 100644
index 000000000..b80ad2852
--- /dev/null
+++ b/src/test/app/Freeze_test.cpp
@@ -0,0 +1,535 @@
+//------------------------------------------------------------------------------
+/*
+ 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
+#include
+
+namespace ripple {
+
+class Freeze_test : public beast::unit_test::suite
+{
+
+ static Json::Value
+ getAccountLines(test::jtx::Env& env, test::jtx::Account const& account)
+ {
+ Json::Value jq;
+ jq[jss::account] = account.human();
+ return env.rpc("json", "account_lines", to_string(jq))[jss::result];
+ }
+
+ static Json::Value
+ getAccountOffers(
+ test::jtx::Env& env,
+ test::jtx::Account const& account,
+ bool current = false)
+ {
+ Json::Value jq;
+ jq[jss::account] = account.human();
+ jq[jss::ledger_index] = current ? "current" : "validated";
+ return env.rpc("json", "account_offers", to_string(jq))[jss::result];
+ }
+
+ static bool checkArraySize(Json::Value const& val, unsigned int size)
+ {
+ return val.isArray() && val.size() == size;
+ }
+
+ void testRippleState()
+ {
+ testcase("RippleState Freeze");
+
+ using namespace test::jtx;
+ Env env(*this);
+
+ Account G1 {"G1"};
+ Account alice {"alice"};
+ Account bob {"bob"};
+
+ env.fund(XRP(1000), G1, alice, bob);
+ env.close();
+
+ env.trust(G1["USD"](100), bob);
+ env.trust(G1["USD"](100), alice);
+ env.close();
+
+ env(pay(G1, bob, G1["USD"](10)));
+ env(pay(G1, alice, G1["USD"](100)));
+ env.close();
+
+ env(offer(alice, XRP(500), G1["USD"](100)));
+ env.close();
+
+ {
+ auto lines = getAccountLines(env, bob);
+ if(! BEAST_EXPECT(checkArraySize(lines[jss::lines], 1u)))
+ return;
+ BEAST_EXPECT(lines[jss::lines][0u][jss::account] == G1.human());
+ BEAST_EXPECT(lines[jss::lines][0u][jss::limit] == "100");
+ BEAST_EXPECT(lines[jss::lines][0u][jss::balance] == "10");
+ }
+
+ {
+ auto lines = getAccountLines(env, alice);
+ if(! BEAST_EXPECT(checkArraySize(lines[jss::lines], 1u)))
+ return;
+ BEAST_EXPECT(lines[jss::lines][0u][jss::account] == G1.human());
+ BEAST_EXPECT(lines[jss::lines][0u][jss::limit] == "100");
+ BEAST_EXPECT(lines[jss::lines][0u][jss::balance] == "100");
+ }
+
+ {
+ // Account with line unfrozen (proving operations normally work)
+ // test: can make Payment on that line
+ env(pay(alice, bob, G1["USD"](1)));
+
+ // test: can receive Payment on that line
+ env(pay(bob, alice, G1["USD"](1)));
+ env.close();
+ }
+
+ {
+ // Is created via a TrustSet with SetFreeze flag
+ // test: sets LowFreeze | HighFreeze flags
+ env(trust(G1, bob["USD"](0), tfSetFreeze));
+ auto affected = env.meta()->getJson(0)[sfAffectedNodes.fieldName];
+ if(! BEAST_EXPECT(checkArraySize(affected, 2u)))
+ return;
+ auto ff =
+ affected[1u][sfModifiedNode.fieldName][sfFinalFields.fieldName];
+ BEAST_EXPECT(
+ ff[sfLowLimit.fieldName] == G1["USD"](0).value().getJson(0));
+ BEAST_EXPECT(ff[jss::Flags].asUInt() & lsfLowFreeze);
+ BEAST_EXPECT(! (ff[jss::Flags].asUInt() & lsfHighFreeze));
+ env.close();
+ }
+
+ {
+ // Account with line frozen by issuer
+ // test: can buy more assets on that line
+ env(offer(bob, G1["USD"](5), XRP(25)));
+ auto affected = env.meta()->getJson(0)[sfAffectedNodes.fieldName];
+ if(! BEAST_EXPECT(checkArraySize(affected, 5u)))
+ return;
+ auto ff =
+ affected[3u][sfModifiedNode.fieldName][sfFinalFields.fieldName];
+ BEAST_EXPECT(
+ ff[sfHighLimit.fieldName] == bob["USD"](100).value().getJson(0));
+ auto amt =
+ STAmount{Issue{to_currency("USD"), noAccount()}, -15}
+ .value().getJson(0);
+ BEAST_EXPECT(ff[sfBalance.fieldName] == amt);
+ env.close();
+ }
+
+ {
+ // test: can not sell assets from that line
+ env(offer(bob, XRP(1), G1["USD"](5)),
+ ter(tecUNFUNDED_OFFER));
+
+ // test: can receive Payment on that line
+ env(pay(alice, bob, G1["USD"](1)));
+
+ // test: can not make Payment from that line
+ env(pay(bob, alice, G1["USD"](1)), ter(tecPATH_DRY));
+ }
+
+ {
+ // check G1 account lines
+ // test: shows freeze
+ auto lines = getAccountLines(env, G1);
+ Json::Value bobLine;
+ for (auto const& it : lines[jss::lines])
+ {
+ if(it[jss::account] == bob.human())
+ {
+ bobLine = it;
+ break;
+ }
+ }
+ if(! BEAST_EXPECT(bobLine))
+ return;
+ BEAST_EXPECT(bobLine[jss::freeze] == true);
+ BEAST_EXPECT(bobLine[jss::balance] == "-16");
+ }
+
+ {
+ // test: shows freeze peer
+ auto lines = getAccountLines(env, bob);
+ Json::Value g1Line;
+ for (auto const& it : lines[jss::lines])
+ {
+ if(it[jss::account] == G1.human())
+ {
+ g1Line = it;
+ break;
+ }
+ }
+ if(! BEAST_EXPECT(g1Line))
+ return;
+ BEAST_EXPECT(g1Line[jss::freeze_peer] == true);
+ BEAST_EXPECT(g1Line[jss::balance] == "16");
+ }
+
+ {
+ //Is cleared via a TrustSet with ClearFreeze flag
+ // test: sets LowFreeze | HighFreeze flags
+ env(trust(G1, bob["USD"](0), tfClearFreeze));
+ auto affected = env.meta()->getJson(0)[sfAffectedNodes.fieldName];
+ if(! BEAST_EXPECT(checkArraySize(affected, 2u)))
+ return;
+ auto ff =
+ affected[1u][sfModifiedNode.fieldName][sfFinalFields.fieldName];
+ BEAST_EXPECT(
+ ff[sfLowLimit.fieldName] == G1["USD"](0).value().getJson(0));
+ BEAST_EXPECT(! (ff[jss::Flags].asUInt() & lsfLowFreeze));
+ BEAST_EXPECT(! (ff[jss::Flags].asUInt() & lsfHighFreeze));
+ env.close();
+ }
+ }
+
+ void
+ testGlobalFreeze()
+ {
+ testcase("Global Freeze");
+
+ using namespace test::jtx;
+ Env env(*this);
+
+ Account G1 {"G1"};
+ Account A1 {"A1"};
+ Account A2 {"A2"};
+ Account A3 {"A3"};
+ Account A4 {"A4"};
+
+ env.fund(XRP(12000), G1);
+ env.fund(XRP(1000), A1);
+ env.fund(XRP(20000), A2, A3, A4);
+ env.close();
+
+ env.trust(G1["USD"](1200), A1);
+ env.trust(G1["USD"](200), A2);
+ env.trust(G1["BTC"](100), A3);
+ env.trust(G1["BTC"](100), A4);
+ env.close();
+
+ env(pay(G1, A1, G1["USD"](1000)));
+ env(pay(G1, A2, G1["USD"](100)));
+ env(pay(G1, A3, G1["BTC"](100)));
+ env(pay(G1, A4, G1["BTC"](100)));
+ env.close();
+
+ env(offer(G1, XRP(10000), G1["USD"](100)), txflags(tfPassive));
+ env(offer(G1, G1["USD"](100), XRP(10000)), txflags(tfPassive));
+ env(offer(A1, XRP(10000), G1["USD"](100)), txflags(tfPassive));
+ env(offer(A2, G1["USD"](100), XRP(10000)), txflags(tfPassive));
+ env.close();
+
+ {
+ //Is toggled via AccountSet using SetFlag and ClearFlag
+ // test: SetFlag GlobalFreeze
+ env.require(nflags(G1, asfGlobalFreeze));
+ env(fset(G1, asfGlobalFreeze));
+ env.require(flags(G1, asfGlobalFreeze));
+ env.require(nflags(G1, asfNoFreeze));
+
+ // test: ClearFlag GlobalFreeze
+ env(fclear(G1, asfGlobalFreeze));
+ env.require(nflags(G1, asfGlobalFreeze));
+ env.require(nflags(G1, asfNoFreeze));
+ }
+
+ {
+ //Account without GlobalFreeze (proving operations normally work)
+ // test: visible offers where taker_pays is unfrozen issuer
+ auto offers =
+ env.rpc("book_offers",
+ std::string("USD/")+G1.human(), "XRP")
+ [jss::result][jss::offers];
+ if(! BEAST_EXPECT(checkArraySize(offers, 2u)))
+ return;
+ std::set accounts;
+ for (auto const& offer : offers)
+ {
+ accounts.insert(offer[jss::Account].asString());
+ }
+ BEAST_EXPECT(accounts.find(A2.human()) != std::end(accounts));
+ BEAST_EXPECT(accounts.find(G1.human()) != std::end(accounts));
+
+ // test: visible offers where taker_gets is unfrozen issuer
+ offers =
+ env.rpc("book_offers",
+ "XRP", std::string("USD/")+G1.human())
+ [jss::result][jss::offers];
+ if(! BEAST_EXPECT(checkArraySize(offers, 2u)))
+ return;
+ accounts.clear();
+ for (auto const& offer : offers)
+ {
+ accounts.insert(offer[jss::Account].asString());
+ }
+ BEAST_EXPECT(accounts.find(A1.human()) != std::end(accounts));
+ BEAST_EXPECT(accounts.find(G1.human()) != std::end(accounts));
+ }
+
+ {
+ // Offers/Payments
+ // test: assets can be bought on the market
+ env(offer(A3, G1["BTC"](1), XRP(1)));
+
+ // test: assets can be sold on the market
+ env(offer(A4, XRP(1), G1["BTC"](1)));
+
+ // test: direct issues can be sent
+ env(pay(G1, A2, G1["USD"](1)));
+
+ // test: direct redemptions can be sent
+ env(pay(A2, G1, G1["USD"](1)));
+
+ // test: via rippling can be sent
+ env(pay(A2, A1, G1["USD"](1)));
+
+ // test: via rippling can be sent back
+ env(pay(A1, A2, G1["USD"](1)));
+ }
+
+ {
+ // Account with GlobalFreeze
+ // set GlobalFreeze first
+ // test: SetFlag GlobalFreeze will toggle back to freeze
+ env.require(nflags(G1, asfGlobalFreeze));
+ env(fset(G1, asfGlobalFreeze));
+ env.require(flags(G1, asfGlobalFreeze));
+ env.require(nflags(G1, asfNoFreeze));
+
+ // test: assets can't be bought on the market
+ env(offer(A3, G1["BTC"](1), XRP(1)), ter(tecFROZEN));
+
+ // test: assets can't be sold on the market
+ env(offer(A4, XRP(1), G1["BTC"](1)), ter(tecFROZEN));
+ }
+
+ {
+ // offers are filtered (seems to be broken?)
+ // test: account_offers always shows own offers
+ auto offers = getAccountOffers(env, G1)[jss::offers];
+ if(! BEAST_EXPECT(checkArraySize(offers, 2u)))
+ return;
+
+ // test: book_offers shows offers
+ // (should these actually be filtered?)
+ offers =
+ env.rpc("book_offers",
+ "XRP", std::string("USD/")+G1.human())
+ [jss::result][jss::offers];
+ if(! BEAST_EXPECT(checkArraySize(offers, 2u)))
+ return;
+
+ offers =
+ env.rpc("book_offers",
+ std::string("USD/")+G1.human(), "XRP")
+ [jss::result][jss::offers];
+ if(! BEAST_EXPECT(checkArraySize(offers, 2u)))
+ return;
+ }
+
+ {
+ // Payments
+ // test: direct issues can be sent
+ env(pay(G1, A2, G1["USD"](1)));
+
+ // test: direct redemptions can be sent
+ env(pay(A2, G1, G1["USD"](1)));
+
+ // test: via rippling cant be sent
+ env(pay(A2, A1, G1["USD"](1)), ter(tecPATH_DRY));
+ }
+ }
+
+ void
+ testNoFreeze()
+ {
+ testcase("No Freeze");
+
+ using namespace test::jtx;
+ Env env(*this);
+
+ Account G1 {"G1"};
+ Account A1 {"A1"};
+
+ env.fund(XRP(12000), G1);
+ env.fund(XRP(1000), A1);
+ env.close();
+
+ env.trust(G1["USD"](1000), A1);
+ env.close();
+
+ env(pay(G1, A1, G1["USD"](1000)));
+ env.close();
+
+ //TrustSet NoFreeze
+ // test: should set NoFreeze in Flags
+ env.require(nflags(G1, asfNoFreeze));
+ env(fset(G1, asfNoFreeze));
+ env.require(flags(G1, asfNoFreeze));
+ env.require(nflags(G1, asfGlobalFreeze));
+
+ // test: cannot be cleared
+ env(fclear(G1, asfNoFreeze));
+ env.require(flags(G1, asfNoFreeze));
+ env.require(nflags(G1, asfGlobalFreeze));
+
+ // test: can set GlobalFreeze
+ env(fset(G1, asfGlobalFreeze));
+ env.require(flags(G1, asfNoFreeze));
+ env.require(flags(G1, asfGlobalFreeze));
+
+ // test: cannot unset GlobalFreeze
+ env(fclear(G1, asfGlobalFreeze));
+ env.require(flags(G1, asfNoFreeze));
+ env.require(flags(G1, asfGlobalFreeze));
+
+ // test: trustlines can't be frozen
+ env(trust(G1, A1["USD"](0), tfSetFreeze));
+ auto affected = env.meta()->getJson(0)[sfAffectedNodes.fieldName];
+ if(! BEAST_EXPECT(checkArraySize(affected, 1u)))
+ return;
+
+ auto let =
+ affected[0u][sfModifiedNode.fieldName][sfLedgerEntryType.fieldName];
+ BEAST_EXPECT(let == "AccountRoot");
+ }
+
+ void
+ testOffersWhenFrozen()
+ {
+ testcase("Offers for Frozen Trust Lines");
+
+ using namespace test::jtx;
+ Env env(*this);
+
+ Account G1 {"G1"};
+ Account A2 {"A2"};
+ Account A3 {"A3"};
+ Account A4 {"A4"};
+
+ env.fund(XRP(1000), G1, A3, A4);
+ env.fund(XRP(2000), A2);
+ env.close();
+
+ env.trust(G1["USD"](1000), A2);
+ env.trust(G1["USD"](2000), A3);
+ env.trust(G1["USD"](2000), A4);
+ env.close();
+
+ env(pay(G1, A3, G1["USD"](2000)));
+ env(pay(G1, A4, G1["USD"](2000)));
+ env.close();
+
+ env(offer(A3, XRP(1000), G1["USD"](1000)), txflags(tfPassive));
+ env.close();
+
+ // removal after successful payment
+ // test: make a payment with partially consuming offer
+ env(pay(A2, G1, G1["USD"](1)), paths(G1["USD"]), sendmax(XRP(1)));
+ env.close();
+
+ // test: offer was only partially consumed
+ auto offers = getAccountOffers(env, A3)[jss::offers];
+ if(! BEAST_EXPECT(checkArraySize(offers, 1u)))
+ return;
+ BEAST_EXPECT(
+ offers[0u][jss::taker_gets] == G1["USD"](999).value().getJson(0));
+
+ // test: someone else creates an offer providing liquidity
+ env(offer(A4, XRP(999), G1["USD"](999)));
+ env.close();
+
+ // test: owner of partially consumed offers line is frozen
+ env(trust(G1, A3["USD"](0), tfSetFreeze));
+ auto affected = env.meta()->getJson(0)[sfAffectedNodes.fieldName];
+ if(! BEAST_EXPECT(checkArraySize(affected, 2u)))
+ return;
+ auto ff =
+ affected[1u][sfModifiedNode.fieldName][sfFinalFields.fieldName];
+ BEAST_EXPECT(
+ ff[sfHighLimit.fieldName] == G1["USD"](0).value().getJson(0));
+ BEAST_EXPECT(! (ff[jss::Flags].asUInt() & lsfLowFreeze));
+ BEAST_EXPECT(ff[jss::Flags].asUInt() & lsfHighFreeze);
+ env.close();
+
+ // verify offer on the books
+ offers = getAccountOffers(env, A3)[jss::offers];
+ if(! BEAST_EXPECT(checkArraySize(offers, 1u)))
+ return;
+
+ // test: Can make a payment via the new offer
+ env(pay(A2, G1, G1["USD"](1)), paths(G1["USD"]), sendmax(XRP(1)));
+ env.close();
+
+ // test: Partially consumed offer was removed by tes* payment
+ offers = getAccountOffers(env, A3)[jss::offers];
+ if(! BEAST_EXPECT(checkArraySize(offers, 0u)))
+ return;
+
+ // removal buy successful OfferCreate
+ // test: freeze the new offer
+ env(trust(G1, A4["USD"](0), tfSetFreeze));
+ affected = env.meta()->getJson(0)[sfAffectedNodes.fieldName];
+ if(! BEAST_EXPECT(checkArraySize(affected, 2u)))
+ return;
+ ff =
+ affected[0u][sfModifiedNode.fieldName][sfFinalFields.fieldName];
+ BEAST_EXPECT(
+ ff[sfLowLimit.fieldName] == G1["USD"](0).value().getJson(0));
+ BEAST_EXPECT(ff[jss::Flags].asUInt() & lsfLowFreeze);
+ BEAST_EXPECT(! (ff[jss::Flags].asUInt() & lsfHighFreeze));
+ env.close();
+
+ // test: can no longer create a crossing offer
+ env(offer(A2, G1["USD"](999), XRP(999)));
+ affected = env.meta()->getJson(0)[sfAffectedNodes.fieldName];
+ if(! BEAST_EXPECT(checkArraySize(affected, 8u)))
+ return;
+ auto created = affected[5u][sfCreatedNode.fieldName];
+ BEAST_EXPECT(created[sfNewFields.fieldName][jss::Account] == A2.human());
+ env.close();
+
+ // test: offer was removed by offer_create
+ offers = getAccountOffers(env, A4)[jss::offers];
+ if(! BEAST_EXPECT(checkArraySize(offers, 0u)))
+ return;
+ }
+
+public:
+
+ void run()
+ {
+ testRippleState();
+ testGlobalFreeze();
+ testNoFreeze();
+ testOffersWhenFrozen();
+ }
+};
+
+BEAST_DEFINE_TESTSUITE(Freeze, app, ripple);
+} // ripple
+
+
diff --git a/src/unity/app_test_unity.cpp b/src/unity/app_test_unity.cpp
index 2ca1a1166..050479e70 100644
--- a/src/unity/app_test_unity.cpp
+++ b/src/unity/app_test_unity.cpp
@@ -24,6 +24,7 @@
#include
#include
#include
+#include
#include
#include
#include