diff --git a/Builds/CMake/RippledCore.cmake b/Builds/CMake/RippledCore.cmake index 78843991f..17969c28f 100644 --- a/Builds/CMake/RippledCore.cmake +++ b/Builds/CMake/RippledCore.cmake @@ -755,6 +755,7 @@ if (tests) src/test/app/Taker_test.cpp src/test/app/TheoreticalQuality_test.cpp src/test/app/Ticket_test.cpp + src/test/app/Touch_test.cpp src/test/app/Transaction_ordering_test.cpp src/test/app/TrustAndBalance_test.cpp src/test/app/TxQ_test.cpp diff --git a/src/ripple/app/tx/impl/Transactor.cpp b/src/ripple/app/tx/impl/Transactor.cpp index 180bf64a8..621667896 100644 --- a/src/ripple/app/tx/impl/Transactor.cpp +++ b/src/ripple/app/tx/impl/Transactor.cpp @@ -1079,6 +1079,24 @@ Transactor::checkMultiSign(PreclaimContext const& ctx) //------------------------------------------------------------------------------ +// increment the touch counter on an account +static void +touchAccount(ApplyView& view, AccountID const& id) +{ + if (!view.rules().enabled(featureTouch)) + return; + + std::shared_ptr sle = view.peek(keylet::account(id)); + if (!sle) + return; + + uint64_t tc = + sle->isFieldPresent(sfTouchCount) ? sle->getFieldU64(sfTouchCount) : 0; + + sle->setFieldU64(sfTouchCount, tc + 1); + view.update(sle); +} + static void removeUnfundedOffers( ApplyView& view, @@ -1519,6 +1537,8 @@ Transactor::doTSH( if ((!canRollback && strong) || (canRollback && !strong)) continue; + touchAccount(view, tshAccountID); + auto klTshHook = keylet::hook(tshAccountID); auto tshHook = view.read(klTshHook); diff --git a/src/ripple/protocol/Feature.h b/src/ripple/protocol/Feature.h index b242b2f7f..3c6e625a9 100644 --- a/src/ripple/protocol/Feature.h +++ b/src/ripple/protocol/Feature.h @@ -74,7 +74,7 @@ namespace detail { // Feature.cpp. Because it's only used to reserve storage, and determine how // large to make the FeatureBitset, it MAY be larger. It MUST NOT be less than // the actual number of amendments. A LogicError on startup will verify this. -static constexpr std::size_t numFeatures = 77; +static constexpr std::size_t numFeatures = 78; /** Amendments that this server supports and the default voting behavior. Whether they are enabled depends on the Rules defined in the validated @@ -362,6 +362,7 @@ extern uint256 const fix240819; extern uint256 const fixPageCap; extern uint256 const fix240911; extern uint256 const fixFloatDivide; +extern uint256 const featureTouch; extern uint256 const fixReduceImport; extern uint256 const fixXahauV3; extern uint256 const fix20250131; diff --git a/src/ripple/protocol/SField.h b/src/ripple/protocol/SField.h index 1f9d15368..0fc34ce09 100644 --- a/src/ripple/protocol/SField.h +++ b/src/ripple/protocol/SField.h @@ -433,6 +433,7 @@ extern SF_UINT64 const sfReferenceCount; extern SF_UINT64 const sfRewardAccumulator; extern SF_UINT64 const sfAccountCount; extern SF_UINT64 const sfAccountIndex; +extern SF_UINT64 const sfTouchCount; // 128-bit extern SF_UINT128 const sfEmailHash; diff --git a/src/ripple/protocol/impl/Feature.cpp b/src/ripple/protocol/impl/Feature.cpp index 73db671ed..d32950946 100644 --- a/src/ripple/protocol/impl/Feature.cpp +++ b/src/ripple/protocol/impl/Feature.cpp @@ -468,6 +468,7 @@ REGISTER_FIX (fix240819, Supported::yes, VoteBehavior::De REGISTER_FIX (fixPageCap, Supported::yes, VoteBehavior::DefaultYes); REGISTER_FIX (fix240911, Supported::yes, VoteBehavior::DefaultYes); REGISTER_FIX (fixFloatDivide, Supported::yes, VoteBehavior::DefaultYes); +REGISTER_FEATURE(Touch, Supported::yes, VoteBehavior::DefaultNo); REGISTER_FIX (fixReduceImport, Supported::yes, VoteBehavior::DefaultYes); REGISTER_FIX (fixXahauV3, Supported::yes, VoteBehavior::DefaultYes); REGISTER_FIX (fix20250131, Supported::yes, VoteBehavior::DefaultYes); diff --git a/src/ripple/protocol/impl/LedgerFormats.cpp b/src/ripple/protocol/impl/LedgerFormats.cpp index acb07c489..4fbcb961a 100644 --- a/src/ripple/protocol/impl/LedgerFormats.cpp +++ b/src/ripple/protocol/impl/LedgerFormats.cpp @@ -66,6 +66,7 @@ LedgerFormats::LedgerFormats() {sfGovernanceFlags, soeOPTIONAL}, {sfGovernanceMarks, soeOPTIONAL}, {sfAccountIndex, soeOPTIONAL}, + {sfTouchCount, soeOPTIONAL}, }, commonFields); diff --git a/src/ripple/protocol/impl/SField.cpp b/src/ripple/protocol/impl/SField.cpp index a72208607..3dee6b3a4 100644 --- a/src/ripple/protocol/impl/SField.cpp +++ b/src/ripple/protocol/impl/SField.cpp @@ -183,6 +183,7 @@ CONSTRUCT_TYPED_SFIELD(sfEmitBurden, "EmitBurden", UINT64, CONSTRUCT_TYPED_SFIELD(sfHookInstructionCount, "HookInstructionCount", UINT64, 17); CONSTRUCT_TYPED_SFIELD(sfHookReturnCode, "HookReturnCode", UINT64, 18); CONSTRUCT_TYPED_SFIELD(sfReferenceCount, "ReferenceCount", UINT64, 19); +CONSTRUCT_TYPED_SFIELD(sfTouchCount, "TouchCount", UINT64, 97); CONSTRUCT_TYPED_SFIELD(sfAccountIndex, "AccountIndex", UINT64, 98); CONSTRUCT_TYPED_SFIELD(sfAccountCount, "AccountCount", UINT64, 99); CONSTRUCT_TYPED_SFIELD(sfRewardAccumulator, "RewardAccumulator", UINT64, 100); diff --git a/src/test/app/Discrepancy_test.cpp b/src/test/app/Discrepancy_test.cpp index c89432f91..ba08f4a0c 100644 --- a/src/test/app/Discrepancy_test.cpp +++ b/src/test/app/Discrepancy_test.cpp @@ -42,6 +42,8 @@ class Discrepancy_test : public beast::unit_test::suite using namespace test::jtx; Env env{*this, features}; + bool const withTouch = env.current()->rules().enabled(featureTouch); + Account A1{"A1"}; Account A2{"A2"}; Account A3{"A3"}; @@ -107,7 +109,8 @@ class Discrepancy_test : public beast::unit_test::suite auto meta = jrr[jss::meta]; uint64_t sumPrev{0}; uint64_t sumFinal{0}; - BEAST_EXPECT(meta[sfAffectedNodes.fieldName].size() == 9); + BEAST_EXPECT( + meta[sfAffectedNodes.fieldName].size() == withTouch ? 11 : 10); for (auto const& an : meta[sfAffectedNodes.fieldName]) { Json::Value node; @@ -127,12 +130,17 @@ class Discrepancy_test : public beast::unit_test::suite Json::Value finalFields = node.isMember(sfFinalFields.fieldName) ? node[sfFinalFields.fieldName] : node[sfNewFields.fieldName]; - if (prevFields) - sumPrev += beast::lexicalCastThrow( - prevFields[sfBalance.fieldName].asString()); - if (finalFields) - sumFinal += beast::lexicalCastThrow( - finalFields[sfBalance.fieldName].asString()); + + // withTouch: "Touched" account does not update Balance + if (prevFields.isMember(sfBalance.fieldName)) + { + if (prevFields) + sumPrev += beast::lexicalCastThrow( + prevFields[sfBalance.fieldName].asString()); + if (finalFields) + sumFinal += beast::lexicalCastThrow( + finalFields[sfBalance.fieldName].asString()); + } } } // the difference in balances (final and prev) should be the @@ -147,6 +155,7 @@ public: using namespace test::jtx; auto const sa = supported_amendments(); testXRPDiscrepancy(sa - featureFlowCross); + testXRPDiscrepancy(sa - featureTouch); testXRPDiscrepancy(sa); } }; diff --git a/src/test/app/Freeze_test.cpp b/src/test/app/Freeze_test.cpp index 7be1f2569..6402f84c5 100644 --- a/src/test/app/Freeze_test.cpp +++ b/src/test/app/Freeze_test.cpp @@ -60,6 +60,7 @@ class Freeze_test : public beast::unit_test::suite using namespace test::jtx; Env env(*this, features); + bool const withTouch = env.current()->rules().enabled(featureTouch); Account G1{"G1"}; Account alice{"alice"}; @@ -113,7 +114,7 @@ class Freeze_test : public beast::unit_test::suite env(trust(G1, bob["USD"](0), tfSetFreeze)); auto affected = env.meta()->getJson( JsonOptions::none)[sfAffectedNodes.fieldName]; - if (!BEAST_EXPECT(checkArraySize(affected, 2u))) + if (!BEAST_EXPECT(checkArraySize(affected, withTouch ? 3u : 2u))) return; auto ff = affected[1u][sfModifiedNode.fieldName][sfFinalFields.fieldName]; @@ -131,10 +132,10 @@ class Freeze_test : public beast::unit_test::suite env(offer(bob, G1["USD"](5), XRP(25))); auto affected = env.meta()->getJson( JsonOptions::none)[sfAffectedNodes.fieldName]; - if (!BEAST_EXPECT(checkArraySize(affected, 5u))) + if (!BEAST_EXPECT(checkArraySize(affected, withTouch ? 6u : 5u))) return; - auto ff = - affected[3u][sfModifiedNode.fieldName][sfFinalFields.fieldName]; + auto ff = affected[withTouch ? 4u : 3u][sfModifiedNode.fieldName] + [sfFinalFields.fieldName]; BEAST_EXPECT( ff[sfHighLimit.fieldName] == bob["USD"](100).value().getJson(JsonOptions::none)); @@ -199,7 +200,7 @@ class Freeze_test : public beast::unit_test::suite env(trust(G1, bob["USD"](0), tfClearFreeze)); auto affected = env.meta()->getJson( JsonOptions::none)[sfAffectedNodes.fieldName]; - if (!BEAST_EXPECT(checkArraySize(affected, 2u))) + if (!BEAST_EXPECT(checkArraySize(affected, withTouch ? 3u : 2u))) return; auto ff = affected[1u][sfModifiedNode.fieldName][sfFinalFields.fieldName]; @@ -377,6 +378,7 @@ class Freeze_test : public beast::unit_test::suite using namespace test::jtx; Env env(*this, features); + bool const withTouch = env.current()->rules().enabled(featureTouch); Account G1{"G1"}; Account A1{"A1"}; @@ -417,7 +419,7 @@ class Freeze_test : public beast::unit_test::suite env(trust(G1, A1["USD"](0), tfSetFreeze)); auto affected = env.meta()->getJson(JsonOptions::none)[sfAffectedNodes.fieldName]; - if (!BEAST_EXPECT(checkArraySize(affected, 1u))) + if (!BEAST_EXPECT(checkArraySize(affected, withTouch ? 2u : 1u))) return; auto let = @@ -432,6 +434,7 @@ class Freeze_test : public beast::unit_test::suite using namespace test::jtx; Env env(*this, features); + bool const withTouch = env.current()->rules().enabled(featureTouch); Account G1{"G1"}; Account A2{"A2"}; @@ -475,7 +478,7 @@ class Freeze_test : public beast::unit_test::suite env(trust(G1, A3["USD"](0), tfSetFreeze)); auto affected = env.meta()->getJson(JsonOptions::none)[sfAffectedNodes.fieldName]; - if (!BEAST_EXPECT(checkArraySize(affected, 2u))) + if (!BEAST_EXPECT(checkArraySize(affected, withTouch ? 3u : 2u))) return; auto ff = affected[1u][sfModifiedNode.fieldName][sfFinalFields.fieldName]; @@ -505,9 +508,10 @@ class Freeze_test : public beast::unit_test::suite env(trust(G1, A4["USD"](0), tfSetFreeze)); affected = env.meta()->getJson(JsonOptions::none)[sfAffectedNodes.fieldName]; - if (!BEAST_EXPECT(checkArraySize(affected, 2u))) + if (!BEAST_EXPECT(checkArraySize(affected, withTouch ? 3u : 2u))) return; - ff = affected[0u][sfModifiedNode.fieldName][sfFinalFields.fieldName]; + ff = affected[withTouch ? 1u : 0u][sfModifiedNode.fieldName] + [sfFinalFields.fieldName]; BEAST_EXPECT( ff[sfLowLimit.fieldName] == G1["USD"](0).value().getJson(JsonOptions::none)); @@ -521,7 +525,7 @@ class Freeze_test : public beast::unit_test::suite env.meta()->getJson(JsonOptions::none)[sfAffectedNodes.fieldName]; if (!BEAST_EXPECT(checkArraySize(affected, 8u))) return; - auto created = affected[0u][sfCreatedNode.fieldName]; + auto created = affected[5u][sfCreatedNode.fieldName]; BEAST_EXPECT( created[sfNewFields.fieldName][jss::Account] == A2.human()); env.close(); @@ -543,8 +547,9 @@ public: testOffersWhenFrozen(features); }; using namespace test::jtx; - auto const sa = supported_amendments() - featureXahauGenesis; + auto const sa = supported_amendments(); testAll(sa - featureFlowCross); + testAll(sa - featureTouch); testAll(sa); } }; diff --git a/src/test/app/Touch_test.cpp b/src/test/app/Touch_test.cpp new file mode 100644 index 000000000..2d59a8cd4 --- /dev/null +++ b/src/test/app/Touch_test.cpp @@ -0,0 +1,1411 @@ +//------------------------------------------------------------------------------ +/* + This file is part of rippled: https://github.com/ripple/rippled + Copyright (c) 2025 XRPL-Labs + + 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 S + OFTWARE 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 +#include +#include +#include + +namespace ripple { +namespace test { + +struct Touch_test : public beast::unit_test::suite +{ +private: + struct TestLedgerData + { + std::string txType; + std::string result; + }; + + void + validateTouch( + jtx::Env& env, + jtx::Account const& account, + TestLedgerData const& testCase) + { + Json::Value params; + params[jss::account] = account.human(); + params[jss::limit] = 1; + params[jss::ledger_index_min] = -1; + params[jss::ledger_index_max] = -1; + auto const jrr = env.rpc("json", "account_tx", to_string(params)); + auto const transactions = jrr[jss::result][jss::transactions]; + BEAST_EXPECT(transactions.size() == 1); + BEAST_EXPECT( + transactions[0u][jss::tx][jss::TransactionType] == testCase.txType); + BEAST_EXPECT( + transactions[0u][jss::meta][sfTransactionResult.jsonName] == + testCase.result); + } + + void + testAccountSet(FeatureBitset features) + { + using namespace test::jtx; + using namespace std::literals; + testcase("account set"); + + test::jtx::Env env{*this, envconfig(), features}; + + auto const alice = Account("alice"); + env.fund(XRP(1000), alice); + env.close(); + + // alice set + env(fset(alice, asfDefaultRipple), ter(tesSUCCESS)); + env.close(); + + // verify touch + validateTouch(env, alice, {"AccountSet", "tesSUCCESS"}); + } + + void + testAccountDelete(FeatureBitset features) + { + using namespace test::jtx; + using namespace std::literals; + testcase("account delete"); + + test::jtx::Env env{*this, envconfig(), features}; + + auto const alice = Account("alice"); + auto const bob = Account("bob"); + env.fund(XRP(1000), alice, bob); + env.close(); + + // AccountDelete + incLgrSeqForAccDel(env, alice); + env(acctdelete(alice, bob), + fee(env.current()->fees().reserve), + ter(tesSUCCESS)); + env.close(); + + // verify touch + validateTouch(env, alice, {"AccountDelete", "tesSUCCESS"}); + validateTouch(env, bob, {"AccountDelete", "tesSUCCESS"}); + } + + static uint256 + getCheckIndex(AccountID const& alice, std::uint32_t uSequence) + { + return keylet::check(alice, uSequence).key; + } + + void + testCheckCancel(FeatureBitset features) + { + testcase("check cancel"); + + using namespace test::jtx; + using namespace std::literals; + + test::jtx::Env env{*this, envconfig(), features}; + + auto const alice = Account("alice"); + auto const bob = Account("bob"); + env.fund(XRP(1000), alice, bob); + env.close(); + + // create check + uint256 const checkId{getCheckIndex(alice, env.seq(alice))}; + env(check::create(alice, bob, XRP(100)), ter(tesSUCCESS)); + env.close(); + + // cancel check + env(check::cancel(alice, checkId), ter(tesSUCCESS)); + env.close(); + + // verify touch + validateTouch(env, alice, {"CheckCancel", "tesSUCCESS"}); + validateTouch(env, bob, {"CheckCancel", "tesSUCCESS"}); + } + + void + testCheckCash(FeatureBitset features) + { + testcase("check cash"); + + using namespace test::jtx; + using namespace std::literals; + + test::jtx::Env env{*this, envconfig(), features}; + + auto const alice = Account("alice"); + auto const bob = Account("bob"); + env.fund(XRP(1000), alice, bob); + env.close(); + + // create check + uint256 const checkId{getCheckIndex(alice, env.seq(alice))}; + env(check::create(alice, bob, XRP(100)), ter(tesSUCCESS)); + env.close(); + + // cash check + env(check::cash(bob, checkId, XRP(100)), ter(tesSUCCESS)); + env.close(); + + // verify touch + validateTouch(env, alice, {"CheckCash", "tesSUCCESS"}); + validateTouch(env, bob, {"CheckCash", "tesSUCCESS"}); + } + + void + testCheckCreate(FeatureBitset features) + { + testcase("check create"); + + using namespace test::jtx; + using namespace std::literals; + + test::jtx::Env env{*this, envconfig(), features}; + + auto const alice = Account("alice"); + auto const bob = Account("bob"); + env.fund(XRP(1000), alice, bob); + env.close(); + + // create check + env(check::create(alice, bob, XRP(100)), ter(tesSUCCESS)); + env.close(); + + // verify touch + validateTouch(env, alice, {"CheckCreate", "tesSUCCESS"}); + validateTouch(env, bob, {"CheckCreate", "tesSUCCESS"}); + } + + void + testClaimReward(FeatureBitset features) + { + testcase("claim reward"); + + using namespace test::jtx; + using namespace std::literals; + + test::jtx::Env env{*this, envconfig(), features}; + + auto const alice = Account("alice"); + auto const issuer = Account("issuer"); + env.fund(XRP(1000), alice, issuer); + env.close(); + + // claim reward + env(reward::claim(alice), reward::issuer(issuer), ter(tesSUCCESS)); + env.close(); + + // verify touch + validateTouch(env, alice, {"ClaimReward", "tesSUCCESS"}); + auto const tt = env.current()->rules().enabled(featureTouch) + ? "ClaimReward" + : "AccountSet"; + validateTouch(env, issuer, {tt, "tesSUCCESS"}); + } + + void + testDepositPreauth(FeatureBitset features) + { + testcase("deposit preauth"); + + using namespace test::jtx; + using namespace std::literals; + + test::jtx::Env env{*this, envconfig(), features}; + + auto const alice = Account("alice"); + auto const bob = Account("bob"); + env.fund(XRP(1000), alice, bob); + env.close(); + + // require authorization for deposits. + env(fset(alice, asfDepositAuth)); + + // deposit preauth + env(deposit::auth(alice, bob), ter(tesSUCCESS)); + env.close(); + + // verify touch + validateTouch(env, alice, {"DepositPreauth", "tesSUCCESS"}); + validateTouch(env, bob, {"DepositPreauth", "tesSUCCESS"}); + } + + void + testEscrowCancel(FeatureBitset features) + { + testcase("escrow cancel"); + + using namespace jtx; + using namespace std::chrono; + using namespace std::literals; + + { + test::jtx::Env env{*this, envconfig(), features}; + + auto const alice = Account("alice"); + auto const bob = Account("bob"); + env.fund(XRP(1000), alice, bob); + env.close(); + + // create escrow + auto const seq1 = env.seq(alice); + NetClock::time_point const finishTime = env.now() + 1s; + NetClock::time_point const cancelTime = env.now() + 2s; + auto createTx = escrow::create(alice, bob, XRP(10)); + createTx[sfFinishAfter.jsonName] = + finishTime.time_since_epoch().count(); + createTx[sfCancelAfter.jsonName] = + cancelTime.time_since_epoch().count(); + env(createTx, ter(tesSUCCESS)); + env.close(); + + // cancel escrow + env(escrow::cancel(alice, alice, seq1), ter(tesSUCCESS)); + env.close(); + + // verify touch + validateTouch(env, alice, {"EscrowCancel", "tesSUCCESS"}); + validateTouch(env, bob, {"EscrowCancel", "tesSUCCESS"}); + } + + { + test::jtx::Env env{*this, envconfig(), features}; + + auto const alice = Account("alice"); + auto const bob = Account("bob"); + env.fund(XRP(1000), alice, bob); + env.close(); + + // create escrow + auto const seq1 = env.seq(alice); + NetClock::time_point const finishTime = env.now() + 1s; + NetClock::time_point const cancelTime = env.now() + 2s; + auto createTx = escrow::create(alice, bob, XRP(10)); + createTx[sfFinishAfter.jsonName] = + finishTime.time_since_epoch().count(); + createTx[sfCancelAfter.jsonName] = + cancelTime.time_since_epoch().count(); + env(createTx, ter(tesSUCCESS)); + env.close(); + + // cancel escrow + env(escrow::cancel(bob, alice, seq1), ter(tesSUCCESS)); + env.close(); + + // verify touch + validateTouch(env, alice, {"EscrowCancel", "tesSUCCESS"}); + validateTouch(env, bob, {"EscrowCancel", "tesSUCCESS"}); + } + } + + void + testEscrowCreate(FeatureBitset features) + { + testcase("escrow create"); + + using namespace jtx; + using namespace std::chrono; + using namespace std::literals; + + test::jtx::Env env{*this, envconfig(), features}; + + auto const alice = Account("alice"); + auto const bob = Account("bob"); + env.fund(XRP(1000), alice, bob); + env.close(); + + // create escrow + NetClock::time_point const finishTime = env.now() + 1s; + NetClock::time_point const cancelTime = env.now() + 2s; + auto createTx = escrow::create(alice, bob, XRP(10)); + createTx[sfFinishAfter.jsonName] = + finishTime.time_since_epoch().count(); + createTx[sfCancelAfter.jsonName] = + cancelTime.time_since_epoch().count(); + env(createTx, ter(tesSUCCESS)); + env.close(); + + // verify touch + validateTouch(env, alice, {"EscrowCreate", "tesSUCCESS"}); + validateTouch(env, bob, {"EscrowCreate", "tesSUCCESS"}); + } + + void + testEscrowFinish(FeatureBitset features) + { + testcase("escrow finish"); + + using namespace jtx; + using namespace std::chrono; + using namespace std::literals; + + { + test::jtx::Env env{*this, envconfig(), features}; + + auto const alice = Account("alice"); + auto const bob = Account("bob"); + env.fund(XRP(1000), alice, bob); + env.close(); + + // create escrow + auto const seq1 = env.seq(alice); + NetClock::time_point const finishTime = env.now() + 1s; + auto createTx = escrow::create(alice, bob, XRP(10)); + createTx[sfFinishAfter.jsonName] = + finishTime.time_since_epoch().count(); + env(createTx, ter(tesSUCCESS)); + env.close(); + + // finish escrow + env(escrow::finish(alice, alice, seq1), ter(tesSUCCESS)); + env.close(); + + // verify touch + validateTouch(env, alice, {"EscrowFinish", "tesSUCCESS"}); + validateTouch(env, bob, {"EscrowFinish", "tesSUCCESS"}); + } + + { + test::jtx::Env env{*this, envconfig(), features}; + + auto const alice = Account("alice"); + auto const bob = Account("bob"); + env.fund(XRP(1000), alice, bob); + env.close(); + + // create escrow + auto const seq1 = env.seq(alice); + NetClock::time_point const finishTime = env.now() + 1s; + auto createTx = escrow::create(alice, bob, XRP(10)); + createTx[sfFinishAfter.jsonName] = + finishTime.time_since_epoch().count(); + env(createTx, ter(tesSUCCESS)); + env.close(); + + // finish escrow + env(escrow::finish(bob, alice, seq1), ter(tesSUCCESS)); + env.close(); + + // verify touch + validateTouch(env, alice, {"EscrowFinish", "tesSUCCESS"}); + validateTouch(env, bob, {"EscrowFinish", "tesSUCCESS"}); + } + } + + void + testGenesisMint(FeatureBitset features) + { + testcase("genesis mint"); + + using namespace jtx; + using namespace std::chrono; + using namespace std::literals; + + test::jtx::Env env{ + *this, + network::makeNetworkConfig(21337, "10", "1000000", "200000"), + features}; + + auto const alice = Account("alice"); + auto const issuer = env.master; + auto const bene = Account("bob"); + env.fund(XRP(1000), alice, bene); + env.close(); + + // burn down the total ledger coins so that genesis mints don't mint + // above 100B tripping invariant + env(noop(issuer), fee(XRP(10'000'000ULL))); + env.close(); + + // set mint hook on master + env(hook(issuer, {{hso(genesis::MintTestHook, overrideFlag)}}, 0), + fee(XRP(1)), + ter(tesSUCCESS)); + env.close(); + + env(invoke::invoke( + alice, + issuer, + genesis::makeBlob({ + {bene.id(), XRP(123).value(), std::nullopt, std::nullopt}, + })), + fee(XRP(10)), + ter(tesSUCCESS)); + env.close(); + env.close(); + + // verify touch + validateTouch(env, alice, {"Invoke", "tesSUCCESS"}); + validateTouch(env, issuer, {"GenesisMint", "tesSUCCESS"}); + validateTouch(env, bene, {"GenesisMint", "tesSUCCESS"}); + } + + void + testImport(FeatureBitset features) + { + testcase("import"); + + using namespace test::jtx; + using namespace std::literals; + + std::vector const keys = { + "ED74D4036C6591A4BDF9C54CEFA39B996A5DCE5F86D11FDA1874481CE9D5A1CDC" + "1"}; + + test::jtx::Env env{ + *this, + network::makeNetworkVLConfig( + 21337, keys, "10", "1000000", "200000"), + features}; + + auto const alice = Account("alice"); + auto const issuer = Account("bob"); + env.fund(XRP(1000), alice, issuer); + env.close(); + + // burn down the total ledger coins so that genesis mints don't mint + // above 100B tripping invariant + env(noop(env.master), fee(XRP(10'000'000ULL))); + env.close(); + + // import + env(import::import(alice, import::loadXpop(ImportTCAccountSet::w_seed)), + import::issuer(issuer), + fee(XRP(1)), + ter(tesSUCCESS)); + env.close(); + + // verify touch + validateTouch(env, alice, {"Import", "tesSUCCESS"}); + auto const tt = env.current()->rules().enabled(featureTouch) + ? "Import" + : "AccountSet"; + validateTouch(env, issuer, {tt, "tesSUCCESS"}); + } + + void + testInvoke(FeatureBitset features) + { + testcase("invoke"); + + using namespace test::jtx; + using namespace std::literals; + + test::jtx::Env env{*this, envconfig(), features}; + + auto const alice = Account("alice"); + auto const bob = Account("bob"); + env.fund(XRP(1000), alice, bob); + env.close(); + + // ttINVOKE + env(invoke::invoke(alice), invoke::dest(bob), ter(tesSUCCESS)); + env.close(); + + // verify touch + validateTouch(env, alice, {"Invoke", "tesSUCCESS"}); + auto const tt = env.current()->rules().enabled(featureTouch) + ? "Invoke" + : "AccountSet"; + validateTouch(env, bob, {tt, "tesSUCCESS"}); + } + + void + testOfferCancel(FeatureBitset features) + { + testcase("offer cancel"); + + using namespace test::jtx; + using namespace std::literals; + + test::jtx::Env env{*this, envconfig(), features}; + + auto const alice = Account("alice"); + auto const gw = Account{"gateway"}; + auto const USD = gw["USD"]; + env.fund(XRP(1000), alice, gw); + env.close(); + + // gw create offer + env(offer(gw, USD(1000), XRP(1000))); + env.close(); + + // create offer + auto const offerSeq = env.seq(alice); + env(offer(alice, USD(1000), XRP(1000)), ter(tesSUCCESS)); + env.close(); + + // cancel offer + env(offer_cancel(alice, offerSeq), ter(tesSUCCESS)); + env.close(); + + // verify touch + validateTouch(env, alice, {"OfferCancel", "tesSUCCESS"}); + } + + void + testOfferCreate(FeatureBitset features) + { + testcase("offer create"); + + using namespace test::jtx; + using namespace std::literals; + + test::jtx::Env env{*this, envconfig(), features}; + + auto const alice = Account("alice"); + auto const gw = Account{"gateway"}; + auto const USD = gw["USD"]; + env.fund(XRP(1000), alice, gw); + env.close(); + + // gw create offer + env(offer(gw, USD(1000), XRP(1000))); + env.close(); + + // create offer + env(offer(alice, USD(1000), XRP(1000)), ter(tesSUCCESS)); + env.close(); + + // verify touch + validateTouch(env, gw, {"OfferCreate", "tesSUCCESS"}); + validateTouch(env, alice, {"OfferCreate", "tesSUCCESS"}); + } + + void + testPayment(FeatureBitset features) + { + testcase("payment"); + + using namespace test::jtx; + using namespace std::literals; + + { + test::jtx::Env env{*this, envconfig(), features}; + + auto const alice = Account("alice"); + auto const bob = Account{"bob"}; + env.fund(XRP(1000), alice, bob); + env.close(); + + // payment + env(pay(alice, bob, XRP(1)), ter(tesSUCCESS)); + env.close(); + + // verify touch + validateTouch(env, alice, {"Payment", "tesSUCCESS"}); + validateTouch(env, bob, {"Payment", "tesSUCCESS"}); + } + + { + test::jtx::Env env{*this, envconfig(), features}; + + auto const alice = Account("alice"); + auto const bob = Account{"bob"}; + auto const gw = Account{"gw"}; + auto const USD = gw["USD"]; + env.fund(XRP(1000), alice, bob, gw); + env.close(); + env(trust(alice, USD(100)), ter(tesSUCCESS)); + env(trust(bob, USD(100)), ter(tesSUCCESS)); + env.close(); + env(pay(gw, alice, USD(100)), ter(tesSUCCESS)); + env.close(); + + // payment + env(pay(alice, bob, USD(1)), ter(tesSUCCESS)); + env.close(); + + // verify touch + validateTouch(env, alice, {"Payment", "tesSUCCESS"}); + validateTouch(env, bob, {"Payment", "tesSUCCESS"}); + validateTouch(env, gw, {"Payment", "tesSUCCESS"}); + } + + { + test::jtx::Env env{*this, envconfig(), features}; + + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const carol = Account("carol"); + env.fund(XRP(1000), alice, bob, carol); + env.close(); + + // setup rippling + auto const USDA = alice["USD"]; + auto const USDB = bob["USD"]; + auto const USDC = carol["USD"]; + env.trust(USDA(10), bob); + env.trust(USDB(10), carol); + + // payment + env(pay(alice, carol, USDB(10)), paths(USDA)); + env.close(); + + // verify touch + validateTouch(env, alice, {"Payment", "tesSUCCESS"}); + validateTouch(env, bob, {"Payment", "tesSUCCESS"}); + validateTouch(env, carol, {"Payment", "tesSUCCESS"}); + } + } + + static uint256 + channel( + jtx::Account const& alice, + jtx::Account const& dst, + std::uint32_t seqProxyValue) + { + auto const k = keylet::payChan(alice, dst, seqProxyValue); + return k.key; + } + + static Buffer + signClaimAuth( + PublicKey const& pk, + SecretKey const& sk, + uint256 const& channel, + STAmount const& authAmt) + { + Serializer msg; + serializePayChanAuthorization(msg, channel, authAmt.xrp()); + return sign(pk, sk, msg.slice()); + } + + void + testPaymentChannelClaim(FeatureBitset features) + { + testcase("payment channel claim"); + + using namespace test::jtx; + using namespace std::literals; + + { + test::jtx::Env env{*this, envconfig(), features}; + + auto const alice = Account("alice"); + auto const bob = Account{"bob"}; + env.fund(XRP(1000), alice, bob); + env.close(); + + // create paychannel + auto const pk = alice.pk(); + auto const settleDelay = 100s; + auto const chan = channel(alice, bob, env.seq(alice)); + env(paychan::create(alice, bob, XRP(10), settleDelay, pk), + ter(tesSUCCESS)); + env.close(); + + auto const delta = XRP(1); + auto const reqBal = delta; + auto const authAmt = reqBal + XRP(1); + + // claim paychannel + env(paychan::claim(alice, chan, reqBal, authAmt), + txflags(tfClose), + ter(tesSUCCESS)); + env.close(); + + // verify touch + validateTouch(env, alice, {"PaymentChannelClaim", "tesSUCCESS"}); + validateTouch(env, bob, {"PaymentChannelClaim", "tesSUCCESS"}); + } + + { + test::jtx::Env env{*this, envconfig(), features}; + + auto const alice = Account("alice"); + auto const bob = Account{"bob"}; + env.fund(XRP(1000), alice, bob); + env.close(); + + // create paychannel + auto const pk = alice.pk(); + auto const settleDelay = 100s; + auto const chan = channel(alice, bob, env.seq(alice)); + env(paychan::create(alice, bob, XRP(10), settleDelay, pk), + ter(tesSUCCESS)); + env.close(); + + auto const delta = XRP(1); + auto const reqBal = delta; + auto const authAmt = reqBal + XRP(1); + + // claim paychannel + auto const sig = + signClaimAuth(alice.pk(), alice.sk(), chan, authAmt); + env(paychan::claim( + bob, chan, reqBal, authAmt, Slice(sig), alice.pk()), + txflags(tfClose), + ter(tesSUCCESS)); + env.close(); + + // verify touch + validateTouch(env, alice, {"PaymentChannelClaim", "tesSUCCESS"}); + validateTouch(env, bob, {"PaymentChannelClaim", "tesSUCCESS"}); + } + } + + void + testPaymentChannelCreate(FeatureBitset features) + { + testcase("payment channel create"); + + using namespace test::jtx; + using namespace std::literals; + + test::jtx::Env env{*this, envconfig(), features}; + + auto const alice = Account("alice"); + auto const bob = Account{"bob"}; + env.fund(XRP(1000), alice, bob); + env.close(); + + // create paychannel + auto const pk = alice.pk(); + auto const settleDelay = 100s; + env(paychan::create(alice, bob, XRP(10), settleDelay, pk), + ter(tesSUCCESS)); + env.close(); + + // verify touch + validateTouch(env, alice, {"PaymentChannelCreate", "tesSUCCESS"}); + validateTouch(env, bob, {"PaymentChannelCreate", "tesSUCCESS"}); + } + + void + testPaymentChannelFund(FeatureBitset features) + { + testcase("payment channel fund"); + + using namespace test::jtx; + using namespace std::literals; + + test::jtx::Env env{*this, envconfig(), features}; + + auto const alice = Account("alice"); + auto const bob = Account{"bob"}; + env.fund(XRP(1000), alice, bob); + env.close(); + + // create paychannel + auto const pk = alice.pk(); + auto const settleDelay = 100s; + auto const chan = channel(alice, bob, env.seq(alice)); + env(paychan::create(alice, bob, XRP(10), settleDelay, pk), + ter(tesSUCCESS)); + env.close(); + + // fund paychannel + env(paychan::fund(alice, chan, XRP(1)), ter(tesSUCCESS)); + env.close(); + + // verify touch + validateTouch(env, alice, {"PaymentChannelFund", "tesSUCCESS"}); + } + + // helper + void static overrideFlag(Json::Value& jv) + { + jv[jss::Flags] = hsfOVERRIDE; + } + + void + testSetHook(FeatureBitset features) + { + testcase("set hook"); + + using namespace test::jtx; + using namespace std::literals; + + test::jtx::Env env{*this, envconfig(), features}; + + auto const alice = Account("alice"); + env.fund(XRP(1000), alice); + env.close(); + + // set tsh hook + auto hook1 = hso(jtx::genesis::AcceptHook, overrideFlag); + hook1[jss::HookOn] = + "00000000000000000000000000000000000000000000000000000000004000" + "00"; + env(hook(alice, {{hook1}}, 0), fee(XRP(1)), ter(tesSUCCESS)); + env.close(); + + // verify touch + validateTouch(env, alice, {"SetHook", "tesSUCCESS"}); + } + + void + testSetRegularKey(FeatureBitset features) + { + testcase("set regular key"); + + using namespace test::jtx; + using namespace std::literals; + + test::jtx::Env env{*this, envconfig(), features}; + + auto const alice = Account("alice"); + auto const bob = Account{"bob"}; + env.fund(XRP(1000), alice, bob); + env.close(); + + // set regular key + env(regkey(alice, bob), ter(tesSUCCESS)); + env.close(); + + // verify touch + validateTouch(env, alice, {"SetRegularKey", "tesSUCCESS"}); + validateTouch(env, bob, {"SetRegularKey", "tesSUCCESS"}); + } + + void + testSignersListSet(FeatureBitset features) + { + testcase("signers list set"); + + using namespace test::jtx; + using namespace std::literals; + + test::jtx::Env env{*this, envconfig(), features}; + + auto const alice = Account("alice"); + auto const signer1 = Account{"bob"}; + auto const signer2 = Account{"carol"}; + env.fund(XRP(1000), alice, signer1, signer2); + env.close(); + + // signers list set + env(signers(alice, 2, {{signer1, 1}, {signer2, 1}}), ter(tesSUCCESS)); + env.close(); + + // verify touch + validateTouch(env, alice, {"SignerListSet", "tesSUCCESS"}); + auto const tt = env.current()->rules().enabled(featureTouch) + ? "SignerListSet" + : "AccountSet"; + validateTouch(env, signer1, {tt, "tesSUCCESS"}); + validateTouch(env, signer2, {tt, "tesSUCCESS"}); + } + + void + testTicketCreate(FeatureBitset features) + { + testcase("ticket create"); + + using namespace test::jtx; + using namespace std::literals; + + test::jtx::Env env{*this, envconfig(), features}; + + auto const alice = Account("alice"); + env.fund(XRP(1000), alice); + env.close(); + + // ticket create + env(ticket::create(alice, 2), ter(tesSUCCESS)); + env.close(); + + // verify touch + validateTouch(env, alice, {"TicketCreate", "tesSUCCESS"}); + } + + void + testTrustSet(FeatureBitset features) + { + testcase("trust set"); + + using namespace test::jtx; + using namespace std::literals; + + test::jtx::Env env{*this, envconfig(), features}; + + auto const alice = Account("alice"); + auto const issuer = Account{"gw"}; + auto const USD = issuer["USD"]; + env.fund(XRP(1000), alice, issuer); + env.close(); + + // trust set + env(trust(alice, USD(1000)), ter(tesSUCCESS)); + env.close(); + + // verify touch + validateTouch(env, alice, {"TrustSet", "tesSUCCESS"}); + validateTouch(env, issuer, {"TrustSet", "tesSUCCESS"}); + } + + void + testURITokenMint(FeatureBitset features) + { + testcase("uritoken mint"); + + using namespace test::jtx; + using namespace std::literals; + + { + test::jtx::Env env{*this, envconfig(), features}; + + auto const issuer = Account("alice"); + auto const buyer = Account("carol"); + env.fund(XRP(1000), issuer, buyer); + env.close(); + + std::string const uri(2, '?'); + auto const tid = uritoken::tokenid(issuer, uri); + std::string const hexid{strHex(tid)}; + + // mint uritoken + env(uritoken::mint(issuer, uri), + uritoken::amt(XRP(1)), + ter(tesSUCCESS)); + env.close(); + + // verify touch + validateTouch(env, issuer, {"URITokenMint", "tesSUCCESS"}); + } + + { + test::jtx::Env env{*this, envconfig(), features}; + + auto const issuer = Account("alice"); + auto const buyer = Account("carol"); + env.fund(XRP(1000), issuer, buyer); + env.close(); + + std::string const uri(2, '?'); + auto const tid = uritoken::tokenid(issuer, uri); + std::string const hexid{strHex(tid)}; + + // mint uritoken + env(uritoken::mint(issuer, uri), + uritoken::dest(buyer), + uritoken::amt(XRP(1)), + ter(tesSUCCESS)); + env.close(); + + // verify touch + validateTouch(env, buyer, {"URITokenMint", "tesSUCCESS"}); + validateTouch(env, issuer, {"URITokenMint", "tesSUCCESS"}); + } + } + + void + testURITokenBurn(FeatureBitset features) + { + testcase("uritoken burn"); + + using namespace test::jtx; + using namespace std::literals; + + { + test::jtx::Env env{*this, envconfig(), features}; + + auto const issuer = Account("alice"); + auto const owner = Account("bob"); + env.fund(XRP(1000), issuer, owner); + env.close(); + + std::string const uri(2, '?'); + auto const tid = uritoken::tokenid(issuer, uri); + std::string const hexid{strHex(tid)}; + + // mint uritoken + env(uritoken::mint(issuer, uri), + uritoken::dest(owner), + uritoken::amt(XRP(1)), + ter(tesSUCCESS)); + env.close(); + + // buy uritoken + env(uritoken::buy(owner, hexid), + uritoken::amt(XRP(1)), + ter(tesSUCCESS)); + env.close(); + + // ttURITOKEN_BURN + env(uritoken::burn(owner, hexid), ter(tesSUCCESS)); + env.close(); + + // verify touch + validateTouch(env, owner, {"URITokenBurn", "tesSUCCESS"}); + validateTouch(env, issuer, {"URITokenBurn", "tesSUCCESS"}); + } + + // Issuer + { + test::jtx::Env env{*this, envconfig(), features}; + + auto const issuer = Account("alice"); + auto const owner = Account("bob"); + env.fund(XRP(1000), issuer, owner); + env.close(); + + std::string const uri(2, '?'); + auto const tid = uritoken::tokenid(issuer, uri); + std::string const hexid{strHex(tid)}; + + // mint uritoken + env(uritoken::mint(issuer, uri), + uritoken::dest(owner), + uritoken::amt(XRP(1)), + txflags(tfBurnable), + ter(tesSUCCESS)); + env.close(); + + // buy uritoken + env(uritoken::buy(owner, hexid), + uritoken::amt(XRP(1)), + ter(tesSUCCESS)); + env.close(); + + // ttURITOKEN_BURN + env(uritoken::burn(issuer, hexid), ter(tesSUCCESS)); + env.close(); + + // verify touch + validateTouch(env, owner, {"URITokenBurn", "tesSUCCESS"}); + validateTouch(env, issuer, {"URITokenBurn", "tesSUCCESS"}); + } + } + + void + testURITokenBuy(FeatureBitset features) + { + testcase("uritoken buy"); + + using namespace test::jtx; + using namespace std::literals; + + test::jtx::Env env{*this, envconfig(), features}; + + auto const issuer = Account("alice"); + auto const owner = Account("bob"); + auto const buyer = Account("carol"); + env.fund(XRP(1000), issuer, owner, buyer); + env.close(); + + std::string const uri(2, '?'); + auto const tid = uritoken::tokenid(issuer, uri); + std::string const hexid{strHex(tid)}; + + // mint uritoken + env(uritoken::mint(issuer, uri), + uritoken::dest(owner), + uritoken::amt(XRP(1)), + ter(tesSUCCESS)); + env.close(); + + // buy uritoken + env(uritoken::buy(owner, hexid), + uritoken::amt(XRP(1)), + ter(tesSUCCESS)); + env.close(); + + // sell uritoken + env(uritoken::sell(owner, hexid), + uritoken::dest(buyer), + uritoken::amt(XRP(1)), + ter(tesSUCCESS)); + env.close(); + + // buy uritoken + env(uritoken::buy(buyer, hexid), + uritoken::amt(XRP(1)), + ter(tesSUCCESS)); + env.close(); + + // verify touch + validateTouch(env, buyer, {"URITokenBuy", "tesSUCCESS"}); + validateTouch(env, issuer, {"URITokenBuy", "tesSUCCESS"}); + } + + void + testURITokenCancelSellOffer(FeatureBitset features) + { + testcase("uritoken cancel sell offer"); + + using namespace test::jtx; + using namespace std::literals; + + test::jtx::Env env{*this, envconfig(), features}; + + auto const issuer = Account("alice"); + auto const owner = Account("bob"); + auto const buyer = Account("carol"); + env.fund(XRP(1000), issuer, owner, buyer); + env.close(); + + std::string const uri(2, '?'); + auto const tid = uritoken::tokenid(issuer, uri); + std::string const hexid{strHex(tid)}; + + // mint uritoken + env(uritoken::mint(issuer, uri), + uritoken::dest(owner), + uritoken::amt(XRP(1)), + ter(tesSUCCESS)); + env.close(); + + // buy uritoken + env(uritoken::buy(owner, hexid), + uritoken::amt(XRP(1)), + ter(tesSUCCESS)); + env.close(); + + // sell uritoken + env(uritoken::sell(owner, hexid), + uritoken::dest(buyer), + uritoken::amt(XRP(1)), + ter(tesSUCCESS)); + env.close(); + + // cancel uritoken + env(uritoken::cancel(owner, hexid), ter(tesSUCCESS)); + env.close(); + + // verify touch + validateTouch(env, owner, {"URITokenCancelSellOffer", "tesSUCCESS"}); + validateTouch(env, issuer, {"URITokenCancelSellOffer", "tesSUCCESS"}); + } + + void + testURITokenCreateSellOffer(FeatureBitset features) + { + testcase("uritoken create sell offer"); + + using namespace test::jtx; + using namespace std::literals; + + test::jtx::Env env{*this, envconfig(), features}; + + auto const issuer = Account("alice"); + auto const owner = Account("bob"); + auto const buyer = Account("carol"); + env.fund(XRP(1000), issuer, owner, buyer); + env.close(); + + std::string const uri(2, '?'); + auto const tid = uritoken::tokenid(issuer, uri); + std::string const hexid{strHex(tid)}; + + // mint uritoken + env(uritoken::mint(issuer, uri), + uritoken::dest(owner), + uritoken::amt(XRP(1)), + ter(tesSUCCESS)); + env.close(); + + // buy uritoken + env(uritoken::buy(owner, hexid), + uritoken::amt(XRP(1)), + ter(tesSUCCESS)); + env.close(); + + // sell uritoken + env(uritoken::sell(owner, hexid), + uritoken::dest(buyer), + uritoken::amt(XRP(1)), + ter(tesSUCCESS)); + env.close(); + + // verify touch + validateTouch(env, owner, {"URITokenCreateSellOffer", "tesSUCCESS"}); + validateTouch(env, buyer, {"URITokenCreateSellOffer", "tesSUCCESS"}); + validateTouch(env, issuer, {"URITokenCreateSellOffer", "tesSUCCESS"}); + } + + void + testRemit(FeatureBitset features) + { + testcase("remit"); + + using namespace test::jtx; + using namespace std::literals; + + // No Amount + { + test::jtx::Env env{*this, envconfig(), features}; + + auto const alice = Account("alice"); + auto const bob = Account{"bob"}; + env.fund(XRP(1000), alice, bob); + env.close(); + + // remit + env(remit::remit(alice, bob), ter(tesSUCCESS)); + env.close(); + + // verify touch + validateTouch(env, alice, {"Remit", "tesSUCCESS"}); + auto const tt = env.current()->rules().enabled(featureTouch) + ? "Remit" + : "AccountSet"; + validateTouch(env, bob, {tt, "tesSUCCESS"}); + } + + // IOU + { + test::jtx::Env env{*this, envconfig(), features}; + + auto const alice = Account("alice"); + auto const bob = Account{"bob"}; + auto const gw = Account{"gw"}; + auto const USD = gw["USD"]; + env.fund(XRP(1000), alice, bob, gw); + env.close(); + env(trust(alice, USD(100)), ter(tesSUCCESS)); + env(trust(bob, USD(100)), ter(tesSUCCESS)); + env.close(); + env(pay(gw, alice, USD(100)), ter(tesSUCCESS)); + env.close(); + + // remit + env(remit::remit(alice, bob), + remit::amts({USD(1)}), + ter(tesSUCCESS)); + env.close(); + + // verify touch + validateTouch(env, alice, {"Remit", "tesSUCCESS"}); + validateTouch(env, bob, {"Remit", "tesSUCCESS"}); + validateTouch(env, gw, {"Remit", "tesSUCCESS"}); + } + + // Inform + { + test::jtx::Env env{*this, envconfig(), features}; + + auto const alice = Account("alice"); + auto const bob = Account{"bob"}; + auto const inform = Account{"inform"}; + env.fund(XRP(1000), alice, bob, inform); + env.close(); + + // remit + env(remit::remit(alice, bob), + remit::inform(inform), + ter(tesSUCCESS)); + env.close(); + + // verify touch + validateTouch(env, alice, {"Remit", "tesSUCCESS"}); + auto const tt = env.current()->rules().enabled(featureTouch) + ? "Remit" + : "AccountSet"; + validateTouch(env, bob, {tt, "tesSUCCESS"}); + validateTouch(env, inform, {tt, "tesSUCCESS"}); + } + + // URITokenIDs + { + test::jtx::Env env{*this, envconfig(), features}; + + auto const alice = Account("alice"); + auto const bob = Account{"bob"}; + auto const issuer = Account{"issuer"}; + env.fund(XRP(1000), alice, bob, issuer); + env.close(); + + // mint uritoken + std::string const uri(maxTokenURILength, '?'); + auto const tid = uritoken::tokenid(issuer, uri); + env(uritoken::mint(issuer, uri), + txflags(tfBurnable), + ter(tesSUCCESS)); + + // sell uritoken + env(uritoken::sell(issuer, strHex(tid)), + uritoken::amt(XRP(1)), + uritoken::dest(alice), + ter(tesSUCCESS)); + env.close(); + + // buy uritoken + env(uritoken::buy(alice, strHex(tid)), + uritoken::amt(XRP(1)), + ter(tesSUCCESS)); + env.close(); + + // remit + env(remit::remit(alice, bob), + remit::token_ids({strHex(tid)}), + ter(tesSUCCESS)); + env.close(); + + // verify touch + validateTouch(env, alice, {"Remit", "tesSUCCESS"}); + validateTouch(env, bob, {"Remit", "tesSUCCESS"}); + validateTouch(env, issuer, {"Remit", "tesSUCCESS"}); + } + } + + void + testAllTxns(FeatureBitset features) + { + testAccountSet(features); + testAccountDelete(features); + testCheckCancel(features); + testCheckCash(features); + testCheckCreate(features); + testClaimReward(features); + testDepositPreauth(features); + testEscrowCancel(features); + testEscrowCreate(features); + testEscrowFinish(features); + testGenesisMint(features); + testImport(features); + testInvoke(features); + testOfferCancel(features); + testOfferCreate(features); + testPayment(features); + testPaymentChannelClaim(features); + testPaymentChannelCreate(features); + testPaymentChannelFund(features); + testSetHook(features); + testSetRegularKey(features); + testSignersListSet(features); + testTicketCreate(features); + testTrustSet(features); + testURITokenMint(features); + testURITokenBurn(features); + testURITokenBuy(features); + testURITokenCancelSellOffer(features); + testURITokenCreateSellOffer(features); + testRemit(features); + } + +public: + void + run() override + { + using namespace test::jtx; + auto const sa = supported_amendments(); + testAllTxns(sa - featureTouch); + testAllTxns(sa); + } +}; + +BEAST_DEFINE_TESTSUITE(Touch, app, ripple); + +} // namespace test +} // namespace ripple \ No newline at end of file diff --git a/src/test/rpc/Subscribe_test.cpp b/src/test/rpc/Subscribe_test.cpp index 7a6f840fd..e359685b8 100644 --- a/src/test/rpc/Subscribe_test.cpp +++ b/src/test/rpc/Subscribe_test.cpp @@ -1138,6 +1138,209 @@ public: } } + const std::vector TshHook = { + 0x00U, 0x61U, 0x73U, 0x6DU, 0x01U, 0x00U, 0x00U, 0x00U, 0x01U, 0x28U, + 0x06U, 0x60U, 0x05U, 0x7FU, 0x7FU, 0x7FU, 0x7FU, 0x7FU, 0x01U, 0x7EU, + 0x60U, 0x04U, 0x7FU, 0x7FU, 0x7FU, 0x7FU, 0x01U, 0x7EU, 0x60U, 0x00U, + 0x01U, 0x7EU, 0x60U, 0x03U, 0x7FU, 0x7FU, 0x7EU, 0x01U, 0x7EU, 0x60U, + 0x02U, 0x7FU, 0x7FU, 0x01U, 0x7FU, 0x60U, 0x01U, 0x7FU, 0x01U, 0x7EU, + 0x02U, 0x45U, 0x05U, 0x03U, 0x65U, 0x6EU, 0x76U, 0x05U, 0x74U, 0x72U, + 0x61U, 0x63U, 0x65U, 0x00U, 0x00U, 0x03U, 0x65U, 0x6EU, 0x76U, 0x0AU, + 0x6FU, 0x74U, 0x78U, 0x6EU, 0x5FU, 0x70U, 0x61U, 0x72U, 0x61U, 0x6DU, + 0x00U, 0x01U, 0x03U, 0x65U, 0x6EU, 0x76U, 0x0AU, 0x68U, 0x6FU, 0x6FU, + 0x6BU, 0x5FU, 0x61U, 0x67U, 0x61U, 0x69U, 0x6EU, 0x00U, 0x02U, 0x03U, + 0x65U, 0x6EU, 0x76U, 0x06U, 0x61U, 0x63U, 0x63U, 0x65U, 0x70U, 0x74U, + 0x00U, 0x03U, 0x03U, 0x65U, 0x6EU, 0x76U, 0x02U, 0x5FU, 0x67U, 0x00U, + 0x04U, 0x03U, 0x02U, 0x01U, 0x05U, 0x05U, 0x03U, 0x01U, 0x00U, 0x02U, + 0x06U, 0x2BU, 0x07U, 0x7FU, 0x01U, 0x41U, 0xC0U, 0x8BU, 0x04U, 0x0BU, + 0x7FU, 0x00U, 0x41U, 0x80U, 0x08U, 0x0BU, 0x7FU, 0x00U, 0x41U, 0xBCU, + 0x0BU, 0x0BU, 0x7FU, 0x00U, 0x41U, 0x80U, 0x08U, 0x0BU, 0x7FU, 0x00U, + 0x41U, 0xC0U, 0x8BU, 0x04U, 0x0BU, 0x7FU, 0x00U, 0x41U, 0x00U, 0x0BU, + 0x7FU, 0x00U, 0x41U, 0x01U, 0x0BU, 0x07U, 0x08U, 0x01U, 0x04U, 0x68U, + 0x6FU, 0x6FU, 0x6BU, 0x00U, 0x05U, 0x0AU, 0x8EU, 0x84U, 0x00U, 0x01U, + 0x8AU, 0x84U, 0x00U, 0x02U, 0x09U, 0x7EU, 0x05U, 0x7FU, 0x02U, 0x40U, + 0x02U, 0x40U, 0x23U, 0x00U, 0x21U, 0x0AU, 0x20U, 0x0AU, 0x41U, 0x10U, + 0x6BU, 0x21U, 0x0AU, 0x20U, 0x0AU, 0x24U, 0x00U, 0x20U, 0x0AU, 0x20U, + 0x00U, 0x36U, 0x02U, 0x0CU, 0x41U, 0x9EU, 0x0BU, 0x41U, 0x0FU, 0x41U, + 0xC1U, 0x09U, 0x41U, 0x0EU, 0x41U, 0x00U, 0x10U, 0x00U, 0x21U, 0x02U, + 0x20U, 0x02U, 0x1AU, 0x20U, 0x0AU, 0x41U, 0x0BU, 0x6AU, 0x21U, 0x00U, + 0x20U, 0x00U, 0x41U, 0x01U, 0x41U, 0xBDU, 0x09U, 0x41U, 0x03U, 0x10U, + 0x01U, 0x21U, 0x01U, 0x20U, 0x01U, 0x42U, 0x01U, 0x51U, 0x21U, 0x00U, + 0x20U, 0x00U, 0x41U, 0x01U, 0x71U, 0x21U, 0x00U, 0x20U, 0x00U, 0x45U, + 0x21U, 0x00U, 0x20U, 0x00U, 0x45U, 0x21U, 0x00U, 0x0BU, 0x20U, 0x00U, + 0x04U, 0x40U, 0x02U, 0x40U, 0x02U, 0x40U, 0x10U, 0x02U, 0x21U, 0x03U, + 0x20U, 0x03U, 0x1AU, 0x0BU, 0x01U, 0x0BU, 0x05U, 0x01U, 0x0BU, 0x0BU, + 0x02U, 0x7EU, 0x02U, 0x40U, 0x20U, 0x0AU, 0x28U, 0x02U, 0x0CU, 0x21U, + 0x00U, 0x02U, 0x40U, 0x02U, 0x40U, 0x02U, 0x40U, 0x02U, 0x40U, 0x02U, + 0x40U, 0x20U, 0x00U, 0x0EU, 0x03U, 0x02U, 0x01U, 0x00U, 0x04U, 0x0BU, + 0x02U, 0x40U, 0x02U, 0x40U, 0x02U, 0x40U, 0x41U, 0xDBU, 0x09U, 0x41U, + 0xC3U, 0x00U, 0x41U, 0x80U, 0x08U, 0x41U, 0xC2U, 0x00U, 0x41U, 0x00U, + 0x10U, 0x00U, 0x21U, 0x04U, 0x20U, 0x04U, 0x1AU, 0x0BU, 0x0CU, 0x06U, + 0x0BU, 0x00U, 0x0BU, 0x00U, 0x0BU, 0x02U, 0x40U, 0x02U, 0x40U, 0x02U, + 0x40U, 0x41U, 0x9FU, 0x0AU, 0x41U, 0x3DU, 0x41U, 0xC2U, 0x08U, 0x41U, + 0x3CU, 0x41U, 0x00U, 0x10U, 0x00U, 0x21U, 0x05U, 0x20U, 0x05U, 0x1AU, + 0x0BU, 0x0CU, 0x05U, 0x0BU, 0x00U, 0x0BU, 0x00U, 0x0BU, 0x02U, 0x40U, + 0x02U, 0x40U, 0x02U, 0x40U, 0x41U, 0xDDU, 0x0AU, 0x41U, 0xC0U, 0x00U, + 0x41U, 0xFEU, 0x08U, 0x41U, 0x3FU, 0x41U, 0x00U, 0x10U, 0x00U, 0x21U, + 0x06U, 0x20U, 0x06U, 0x1AU, 0x0BU, 0x01U, 0x0BU, 0x0BU, 0x0BU, 0x0BU, + 0x0BU, 0x02U, 0x7EU, 0x02U, 0x7EU, 0x41U, 0xAEU, 0x0BU, 0x41U, 0x0DU, + 0x41U, 0xCFU, 0x09U, 0x41U, 0x0CU, 0x41U, 0x00U, 0x10U, 0x00U, 0x21U, + 0x07U, 0x20U, 0x07U, 0x1AU, 0x20U, 0x0AU, 0x41U, 0x07U, 0x6AU, 0x21U, + 0x0CU, 0x20U, 0x0CU, 0x21U, 0x00U, 0x20U, 0x0AU, 0x20U, 0x00U, 0x36U, + 0x02U, 0x00U, 0x20U, 0x0AU, 0x28U, 0x02U, 0x0CU, 0x21U, 0x00U, 0x20U, + 0x00U, 0xADU, 0x21U, 0x01U, 0x20U, 0x01U, 0x42U, 0x18U, 0x88U, 0x21U, + 0x01U, 0x20U, 0x01U, 0x42U, 0xFFU, 0x01U, 0x83U, 0x21U, 0x01U, 0x20U, + 0x01U, 0xA7U, 0x21U, 0x00U, 0x20U, 0x0AU, 0x28U, 0x02U, 0x00U, 0x21U, + 0x0BU, 0x20U, 0x0BU, 0x20U, 0x00U, 0x3AU, 0x00U, 0x00U, 0x20U, 0x0AU, + 0x28U, 0x02U, 0x0CU, 0x21U, 0x00U, 0x20U, 0x00U, 0xADU, 0x21U, 0x01U, + 0x20U, 0x01U, 0x42U, 0x10U, 0x88U, 0x21U, 0x01U, 0x20U, 0x01U, 0x42U, + 0xFFU, 0x01U, 0x83U, 0x21U, 0x01U, 0x20U, 0x01U, 0xA7U, 0x21U, 0x00U, + 0x20U, 0x0AU, 0x28U, 0x02U, 0x00U, 0x21U, 0x0BU, 0x20U, 0x0BU, 0x20U, + 0x00U, 0x3AU, 0x00U, 0x01U, 0x20U, 0x0AU, 0x28U, 0x02U, 0x0CU, 0x21U, + 0x00U, 0x20U, 0x00U, 0xADU, 0x21U, 0x01U, 0x20U, 0x01U, 0x42U, 0x08U, + 0x88U, 0x21U, 0x01U, 0x20U, 0x01U, 0x42U, 0xFFU, 0x01U, 0x83U, 0x21U, + 0x01U, 0x20U, 0x01U, 0xA7U, 0x21U, 0x00U, 0x20U, 0x0AU, 0x28U, 0x02U, + 0x00U, 0x21U, 0x0BU, 0x20U, 0x0BU, 0x20U, 0x00U, 0x3AU, 0x00U, 0x02U, + 0x20U, 0x0AU, 0x28U, 0x02U, 0x0CU, 0x21U, 0x00U, 0x20U, 0x00U, 0xADU, + 0x21U, 0x01U, 0x20U, 0x01U, 0x42U, 0x00U, 0x88U, 0x21U, 0x01U, 0x20U, + 0x01U, 0x42U, 0xFFU, 0x01U, 0x83U, 0x21U, 0x01U, 0x20U, 0x01U, 0xA7U, + 0x21U, 0x00U, 0x20U, 0x0AU, 0x28U, 0x02U, 0x00U, 0x21U, 0x0BU, 0x20U, + 0x0BU, 0x20U, 0x00U, 0x3AU, 0x00U, 0x03U, 0x20U, 0x0CU, 0x21U, 0x00U, + 0x20U, 0x00U, 0x41U, 0x04U, 0x42U, 0x1CU, 0x10U, 0x03U, 0x21U, 0x08U, + 0x20U, 0x08U, 0x1AU, 0x41U, 0x01U, 0x41U, 0x01U, 0x10U, 0x04U, 0x21U, + 0x0DU, 0x20U, 0x0DU, 0x1AU, 0x20U, 0x0AU, 0x41U, 0x10U, 0x6AU, 0x21U, + 0x00U, 0x20U, 0x00U, 0x24U, 0x00U, 0x42U, 0x00U, 0x21U, 0x09U, 0x42U, + 0x00U, 0x0BU, 0x0BU, 0x0BU, 0x0BU, 0x0BU, 0xC3U, 0x03U, 0x01U, 0x00U, + 0x41U, 0x80U, 0x08U, 0x0BU, 0xBBU, 0x03U, 0x74U, 0x73U, 0x68U, 0x2EU, + 0x63U, 0x3AU, 0x20U, 0x57U, 0x65U, 0x61U, 0x6BU, 0x20U, 0x41U, 0x67U, + 0x61U, 0x69U, 0x6EU, 0x2EU, 0x20U, 0x45U, 0x78U, 0x65U, 0x63U, 0x75U, + 0x74U, 0x65U, 0x20U, 0x41U, 0x46U, 0x54U, 0x45U, 0x52U, 0x20U, 0x74U, + 0x72U, 0x61U, 0x6EU, 0x73U, 0x61U, 0x63U, 0x74U, 0x69U, 0x6FU, 0x6EU, + 0x20U, 0x69U, 0x73U, 0x20U, 0x61U, 0x70U, 0x70U, 0x6CU, 0x69U, 0x65U, + 0x64U, 0x20U, 0x74U, 0x6FU, 0x20U, 0x6CU, 0x65U, 0x64U, 0x67U, 0x65U, + 0x72U, 0x00U, 0x74U, 0x73U, 0x68U, 0x2EU, 0x63U, 0x3AU, 0x20U, 0x57U, + 0x65U, 0x61U, 0x6BU, 0x2EU, 0x20U, 0x45U, 0x78U, 0x65U, 0x63U, 0x75U, + 0x74U, 0x65U, 0x20U, 0x41U, 0x46U, 0x54U, 0x45U, 0x52U, 0x20U, 0x74U, + 0x72U, 0x61U, 0x6EU, 0x73U, 0x61U, 0x63U, 0x74U, 0x69U, 0x6FU, 0x6EU, + 0x20U, 0x69U, 0x73U, 0x20U, 0x61U, 0x70U, 0x70U, 0x6CU, 0x69U, 0x65U, + 0x64U, 0x20U, 0x74U, 0x6FU, 0x20U, 0x6CU, 0x65U, 0x64U, 0x67U, 0x65U, + 0x72U, 0x00U, 0x74U, 0x73U, 0x68U, 0x2EU, 0x63U, 0x3AU, 0x20U, 0x53U, + 0x74U, 0x72U, 0x6FU, 0x6EU, 0x67U, 0x2EU, 0x20U, 0x45U, 0x78U, 0x65U, + 0x63U, 0x75U, 0x74U, 0x65U, 0x20U, 0x42U, 0x45U, 0x46U, 0x4FU, 0x52U, + 0x45U, 0x20U, 0x74U, 0x72U, 0x61U, 0x6EU, 0x73U, 0x61U, 0x63U, 0x74U, + 0x69U, 0x6FU, 0x6EU, 0x20U, 0x69U, 0x73U, 0x20U, 0x61U, 0x70U, 0x70U, + 0x6CU, 0x69U, 0x65U, 0x64U, 0x20U, 0x74U, 0x6FU, 0x20U, 0x6CU, 0x65U, + 0x64U, 0x67U, 0x65U, 0x72U, 0x00U, 0x41U, 0x41U, 0x57U, 0x00U, 0x74U, + 0x73U, 0x68U, 0x2EU, 0x63U, 0x3AU, 0x20U, 0x53U, 0x74U, 0x61U, 0x72U, + 0x74U, 0x2EU, 0x00U, 0x74U, 0x73U, 0x68U, 0x2EU, 0x63U, 0x3AU, 0x20U, + 0x45U, 0x6EU, 0x64U, 0x2EU, 0x00U, 0x22U, 0x74U, 0x73U, 0x68U, 0x2EU, + 0x63U, 0x3AU, 0x20U, 0x57U, 0x65U, 0x61U, 0x6BU, 0x20U, 0x41U, 0x67U, + 0x61U, 0x69U, 0x6EU, 0x2EU, 0x20U, 0x45U, 0x78U, 0x65U, 0x63U, 0x75U, + 0x74U, 0x65U, 0x20U, 0x41U, 0x46U, 0x54U, 0x45U, 0x52U, 0x20U, 0x74U, + 0x72U, 0x61U, 0x6EU, 0x73U, 0x61U, 0x63U, 0x74U, 0x69U, 0x6FU, 0x6EU, + 0x20U, 0x69U, 0x73U, 0x20U, 0x61U, 0x70U, 0x70U, 0x6CU, 0x69U, 0x65U, + 0x64U, 0x20U, 0x74U, 0x6FU, 0x20U, 0x6CU, 0x65U, 0x64U, 0x67U, 0x65U, + 0x72U, 0x22U, 0x00U, 0x22U, 0x74U, 0x73U, 0x68U, 0x2EU, 0x63U, 0x3AU, + 0x20U, 0x57U, 0x65U, 0x61U, 0x6BU, 0x2EU, 0x20U, 0x45U, 0x78U, 0x65U, + 0x63U, 0x75U, 0x74U, 0x65U, 0x20U, 0x41U, 0x46U, 0x54U, 0x45U, 0x52U, + 0x20U, 0x74U, 0x72U, 0x61U, 0x6EU, 0x73U, 0x61U, 0x63U, 0x74U, 0x69U, + 0x6FU, 0x6EU, 0x20U, 0x69U, 0x73U, 0x20U, 0x61U, 0x70U, 0x70U, 0x6CU, + 0x69U, 0x65U, 0x64U, 0x20U, 0x74U, 0x6FU, 0x20U, 0x6CU, 0x65U, 0x64U, + 0x67U, 0x65U, 0x72U, 0x22U, 0x00U, 0x22U, 0x74U, 0x73U, 0x68U, 0x2EU, + 0x63U, 0x3AU, 0x20U, 0x53U, 0x74U, 0x72U, 0x6FU, 0x6EU, 0x67U, 0x2EU, + 0x20U, 0x45U, 0x78U, 0x65U, 0x63U, 0x75U, 0x74U, 0x65U, 0x20U, 0x42U, + 0x45U, 0x46U, 0x4FU, 0x52U, 0x45U, 0x20U, 0x74U, 0x72U, 0x61U, 0x6EU, + 0x73U, 0x61U, 0x63U, 0x74U, 0x69U, 0x6FU, 0x6EU, 0x20U, 0x69U, 0x73U, + 0x20U, 0x61U, 0x70U, 0x70U, 0x6CU, 0x69U, 0x65U, 0x64U, 0x20U, 0x74U, + 0x6FU, 0x20U, 0x6CU, 0x65U, 0x64U, 0x67U, 0x65U, 0x72U, 0x22U, 0x00U, + 0x22U, 0x74U, 0x73U, 0x68U, 0x2EU, 0x63U, 0x3AU, 0x20U, 0x53U, 0x74U, + 0x61U, 0x72U, 0x74U, 0x2EU, 0x22U, 0x00U, 0x22U, 0x74U, 0x73U, 0x68U, + 0x2EU, 0x63U, 0x3AU, 0x20U, 0x45U, 0x6EU, 0x64U, 0x2EU, 0x22U}; + + void static overrideFlag(Json::Value& jv) + { + jv[jss::Flags] = 0b00000001U; + } + + void + setTSHHook(jtx::Env& env, jtx::Account const& account) + { + using namespace test::jtx; + env(hook(account, {{hso(TshHook, overrideFlag)}}, 0), + fee(XRP(2)), + ter(tesSUCCESS)); + env.close(); + } + + void + testAccount(FeatureBitset features) + { + testcase("AccountWithHookStream"); + + using namespace std::chrono_literals; + using namespace jtx; + Env env(*this, features); + + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const gw = Account("gw"); + auto const USD = gw["USD"]; + + env.fund(XRP(10000), alice, bob, gw); + env.trust(USD(20000), alice, bob); + env.close(); + + auto wsc = makeWSClient(env.app().config()); + Json::Value stream; + + bool const withTouch = env.current()->rules().enabled(featureTouch); + { + // RPC subscribe to account stream + stream[jss::accounts] = Json::arrayValue; + stream[jss::accounts].append(bob.human()); + auto jv = wsc->invoke("subscribe", stream); + if (wsc->version() == 2) + { + BEAST_EXPECT( + jv.isMember(jss::jsonrpc) && jv[jss::jsonrpc] == "2.0"); + BEAST_EXPECT( + jv.isMember(jss::ripplerpc) && jv[jss::ripplerpc] == "2.0"); + BEAST_EXPECT(jv.isMember(jss::id) && jv[jss::id] == 5); + } + BEAST_EXPECT(jv[jss::result][jss::status] == "success"); + } + + // Test Invoke Tx + { + setTSHHook(env, bob); + // Submit and Close + env(invoke::invoke(alice), + invoke::dest(bob), + fee(XRP(1)), + ter(tesSUCCESS)); + env.close(); + + // Check stream update + BEAST_EXPECT(wsc->findMsg(5s, [&](auto const& jv) { + if (jv[jss::transaction][jss::TransactionType] == "Invoke") + return true; + return withTouch ? false : true; + })); + } + + // RPC unsubscribe + auto jv = wsc->invoke("unsubscribe", stream); + if (wsc->version() == 2) + { + BEAST_EXPECT( + jv.isMember(jss::jsonrpc) && jv[jss::jsonrpc] == "2.0"); + BEAST_EXPECT( + jv.isMember(jss::ripplerpc) && jv[jss::ripplerpc] == "2.0"); + BEAST_EXPECT(jv.isMember(jss::id) && jv[jss::id] == 5); + } + BEAST_EXPECT(jv[jss::status] == "success"); + } + void run() override { @@ -1155,6 +1358,8 @@ public: testSubErrors(false); testSubByUrl(); testHistoryTxStream(); + testAccount(all); + testAccount(all - featureTouch); } };