From e319619dce2ef54d7fcb6a9b05802de71121f4cf Mon Sep 17 00:00:00 2001 From: tequ Date: Mon, 1 Dec 2025 15:26:15 +0900 Subject: [PATCH] Combine 3 Hook Api fix amendments (#648) --- src/ripple/app/hook/impl/applyHook.cpp | 6 +- src/ripple/app/tx/impl/Transactor.cpp | 10 +- src/ripple/protocol/Feature.h | 7 +- src/ripple/protocol/impl/Feature.cpp | 4 +- src/test/app/GenesisMint_test.cpp | 6 +- src/test/app/HookAPI_test.cpp | 4276 ++++++++++++++++++++++++ src/test/app/SetHook_test.cpp | 14 +- src/test/app/Touch_test.cpp | 4 +- src/test/app/TxQ_test.cpp | 5 +- src/test/rpc/LedgerRPC_test.cpp | 4 +- 10 files changed, 4304 insertions(+), 32 deletions(-) create mode 100644 src/test/app/HookAPI_test.cpp diff --git a/src/ripple/app/hook/impl/applyHook.cpp b/src/ripple/app/hook/impl/applyHook.cpp index 849f8d482..f27b7d23b 100644 --- a/src/ripple/app/hook/impl/applyHook.cpp +++ b/src/ripple/app/hook/impl/applyHook.cpp @@ -4239,7 +4239,7 @@ DEFINE_HOOK_FUNCTION( // eg) Amounts field value = 0x5C => 0xF0, 0x5C if ((*upto & 0xF0U) == 0xF0U) { - if (view.rules().enabled(fixStoSubarray) && *upto == 0xF0U) + if (view.rules().enabled(fixHookAPI20251128) && *upto == 0xF0U) { // field value > 15 upto++; @@ -4484,7 +4484,7 @@ DEFINE_HOOK_FUNCTION( return MEM_OVERLAP; } - if (fread_len > 0 && view.rules().enabled(fixStoEmplaceFieldIdCheck)) + if (fread_len > 0 && view.rules().enabled(fixHookAPI20251128)) { // inject field should be valid sto object and it's field id should // match the field_id @@ -4748,7 +4748,7 @@ DEFINE_HOOK_FUNCTION( std::unique_ptr stpTrans; stpTrans = std::make_unique(std::ref(sitTrans)); - if (!view.rules().enabled(fixEtxnFeeBase)) + if (!view.rules().enabled(fixHookAPI20251128)) return Transactor::calculateBaseFee( *(applyCtx.app.openLedger().current()), *stpTrans) .drops(); diff --git a/src/ripple/app/tx/impl/Transactor.cpp b/src/ripple/app/tx/impl/Transactor.cpp index 0cdc22e09..80646172a 100644 --- a/src/ripple/app/tx/impl/Transactor.cpp +++ b/src/ripple/app/tx/impl/Transactor.cpp @@ -1519,7 +1519,7 @@ Transactor::doTSH( for (auto& weakTsh : additionalWeakTSH_) tsh.emplace_back(weakTsh, false); - if (view.rules().enabled(fixEtxnFeeBase)) + if (view.rules().enabled(fixHookAPI20251128)) { // if account_ is not included in tsh , add it only once bool found = false; @@ -1547,7 +1547,7 @@ Transactor::doTSH( // blindly nominate any TSHes they find but // obviously we will never execute OTXN account // as a TSH because they already had first execution - if (!view.rules().enabled(fixEtxnFeeBase)) + if (!view.rules().enabled(fixHookAPI20251128)) { if (tshAccountID == account_) continue; @@ -1564,10 +1564,10 @@ Transactor::doTSH( touchAccount(view, tshAccountID); - if (view.rules().enabled(fixEtxnFeeBase)) + if (view.rules().enabled(fixHookAPI20251128)) { - // After fixEtxnFeeBase, the otxn account is prosessed as touched - // account + // After fixHookAPI20251128, the otxn account is prosessed as + // touched account if (tshAccountID == account_) continue; } diff --git a/src/ripple/protocol/Feature.h b/src/ripple/protocol/Feature.h index e89193e94..2766859dc 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 = 92; +static constexpr std::size_t numFeatures = 90; /** Amendments that this server supports and the default voting behavior. Whether they are enabled depends on the Rules defined in the validated @@ -377,10 +377,7 @@ extern uint256 const featureCron; extern uint256 const fixInvalidTxFlags; extern uint256 const featureExtendedHookState; extern uint256 const fixCronStacking; -extern uint256 const fixEtxnFeeBase; -extern uint256 const fixStoEmplaceFieldIdCheck; -extern uint256 const fixStoSubarray; - +extern uint256 const fixHookAPI20251128; } // namespace ripple #endif diff --git a/src/ripple/protocol/impl/Feature.cpp b/src/ripple/protocol/impl/Feature.cpp index 58b492750..24383f896 100644 --- a/src/ripple/protocol/impl/Feature.cpp +++ b/src/ripple/protocol/impl/Feature.cpp @@ -483,9 +483,7 @@ REGISTER_FEATURE(Cron, Supported::yes, VoteBehavior::De REGISTER_FIX (fixInvalidTxFlags, Supported::yes, VoteBehavior::DefaultYes); REGISTER_FEATURE(ExtendedHookState, Supported::yes, VoteBehavior::DefaultNo); REGISTER_FIX (fixCronStacking, Supported::yes, VoteBehavior::DefaultYes); -REGISTER_FIX (fixEtxnFeeBase, Supported::yes, VoteBehavior::DefaultYes); -REGISTER_FIX (fixStoEmplaceFieldIdCheck, Supported::yes, VoteBehavior::DefaultYes); -REGISTER_FIX (fixStoSubarray, Supported::yes, VoteBehavior::DefaultYes); +REGISTER_FIX (fixHookAPI20251128, Supported::yes, VoteBehavior::DefaultYes); // The following amendments are obsolete, but must remain supported // because they could potentially get enabled. diff --git a/src/test/app/GenesisMint_test.cpp b/src/test/app/GenesisMint_test.cpp index 4f45ff9b6..9f183f0c6 100644 --- a/src/test/app/GenesisMint_test.cpp +++ b/src/test/app/GenesisMint_test.cpp @@ -192,7 +192,7 @@ struct GenesisMint_test : public beast::unit_test::suite } auto const postCoins = env.current()->info().drops; auto const txnFee = - env.current()->rules().enabled(fixEtxnFeeBase) ? 0 : 10; + env.current()->rules().enabled(fixHookAPI20251128) ? 0 : 10; BEAST_EXPECT( initCoins - 1'000'000 /* txn fee */ - txnFee /* emitted txn fee */ @@ -630,7 +630,7 @@ struct GenesisMint_test : public beast::unit_test::suite auto const postCoins = env.current()->info().drops; auto const txnFee = - env.current()->rules().enabled(fixEtxnFeeBase) ? 0 : 10; + env.current()->rules().enabled(fixHookAPI20251128) ? 0 : 10; BEAST_EXPECT( initCoins - 1'000'000 /* txn fee */ - txnFee /* emitted txn fee */ @@ -699,7 +699,7 @@ public: auto const sa = supported_amendments(); testWithFeats(sa); testWithFeats(sa - fixXahauV1); - testWithFeats(sa - fixEtxnFeeBase); + testWithFeats(sa - fixHookAPI20251128); } }; diff --git a/src/test/app/HookAPI_test.cpp b/src/test/app/HookAPI_test.cpp new file mode 100644 index 000000000..bf5876566 --- /dev/null +++ b/src/test/app/HookAPI_test.cpp @@ -0,0 +1,4276 @@ +//------------------------------------------------------------------------------ +/* + 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 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 "ripple/protocol/Indexes.h" +#include "test/jtx/genesis.h" +#include "test/jtx/hook.h" +#include +#include +#include +#include +#include + +namespace ripple { + +namespace test { + +class HookAPI_test : public beast::unit_test::suite +{ +private: + ApplyContext + createApplyContext(jtx::Env& env, OpenView& ov, STTx const& tx) + { + ApplyContext applyCtx{ + env.app(), + ov, + tx, + tesSUCCESS, + env.current()->fees().base, + tapNONE, + env.journal}; + return applyCtx; + } + +public: + void + test_accept(FeatureBitset features) + { + testcase("Test accept() hookapi"); + + // TODO + BEAST_EXPECT(true); + } + + void + test_rollback(FeatureBitset features) + { + testcase("Test rollback() hookapi"); + + // TODO + BEAST_EXPECT(true); + } + + void + testGuards(FeatureBitset features) + { + testcase("Test guards"); + + // TODO + BEAST_EXPECT(true); + } + + void + test_emit(FeatureBitset features) + { + testcase("Test emit"); + using namespace jtx; + + auto const alice = Account{"alice"}; + auto const bob = Account{"bob"}; + + using namespace hook_api; + Env env{*this, features}; + + STTx invokeTx = STTx(ttINVOKE, [&](STObject& obj) {}); + OpenView ov{*env.current()}; + ApplyContext applyCtx = createApplyContext(env, ov, invokeTx); + + STTx const emitInvokeTx = STTx(ttINVOKE, [&](STObject& obj) { + obj[sfAccount] = alice.id(); + obj[sfSequence] = 0; + obj[sfSigningPubKey] = PublicKey(); + obj[sfFirstLedgerSequence] = env.closed()->seq() + 1; + obj[sfLastLedgerSequence] = env.closed()->seq() + 5; + obj[sfFee] = env.closed()->fees().base; + + auto& emitDetails = obj.peekFieldObject(sfEmitDetails); + emitDetails[sfEmitGeneration] = 1; + emitDetails[sfEmitBurden] = 1; + emitDetails[sfEmitParentTxnID] = invokeTx.getTransactionID(); + emitDetails[sfEmitNonce] = uint256(); + emitDetails[sfEmitHookHash] = uint256(); + }); + + STTx const emitSetHookTx = STTx(ttHOOK_SET, [&](STObject& obj) { + obj[sfAccount] = alice.id(); + obj[sfSequence] = 0; + obj[sfSigningPubKey] = PublicKey(); + obj[sfFirstLedgerSequence] = env.closed()->seq() + 1; + obj[sfLastLedgerSequence] = env.closed()->seq() + 5; + obj[sfFee] = env.closed()->fees().base; + STObject hookobj(sfHook); + auto& hooks = obj.peekFieldArray(sfHooks); + hooks.emplace_back(std::move(hookobj)); + + auto& emitDetails = obj.peekFieldObject(sfEmitDetails); + emitDetails[sfEmitGeneration] = 1; + emitDetails[sfEmitBurden] = 1; + emitDetails[sfEmitParentTxnID] = invokeTx.getTransactionID(); + emitDetails[sfEmitNonce] = uint256(); + emitDetails[sfEmitHookHash] = uint256(); + }); + + { + // PREREQUISITE_NOT_MET + auto hookCtx = makeStubHookContext( + applyCtx, alice.id(), alice.id(), {.expected_etxn_count = -1}); + hook::HookAPI api(hookCtx); + + auto const result = api.emit(emitInvokeTx.getSerializer().slice()); + BEAST_EXPECT(result.error() == PREREQUISITE_NOT_MET); + } + { + // TOO_MANY_EMITTED_TXN + std::string reason; + auto tx = std::make_shared( + std::make_shared(invokeTx), + reason, + env.app()); + std::queue> emittedTxn; + emittedTxn.push(tx); + auto hookCtx = makeStubHookContext( + applyCtx, + alice.id(), + alice.id(), + { + .expected_etxn_count = 1, + .result = {.emittedTxn = emittedTxn}, + }); + hook::HookAPI api(hookCtx); + + auto const result = api.emit(emitInvokeTx.getSerializer().slice()); + BEAST_EXPECT(result.error() == TOO_MANY_EMITTED_TXN); + } + // EMISSION_FAILURE + { + // Invalid txn + auto hookCtx = makeStubHookContext( + applyCtx, + alice.id(), + alice.id(), + { + .expected_etxn_count = 1, + .nonce_used = {{uint256(0), true}}, + }); + hook::HookAPI api(hookCtx); + auto tx = emitInvokeTx; + Serializer s = tx.getSerializer(); + s.add8(0); // invalid value + auto const result = api.emit(s.slice()); + BEAST_EXPECT(result.error() == EMISSION_FAILURE); + } + { + // Pseudo txn + auto hookCtx = makeStubHookContext( + applyCtx, + alice.id(), + alice.id(), + { + .expected_etxn_count = 1, + .nonce_used = {{uint256(0), true}}, + }); + hook::HookAPI api(hookCtx); + auto tx = emitInvokeTx; + tx.setFieldU16(sfTransactionType, ttFEE); + auto const result = api.emit(tx.getSerializer().slice()); + BEAST_EXPECT(result.error() == EMISSION_FAILURE); + } + { + // HookCanEmit (non-SetHook) + auto hookCtx = makeStubHookContext( + applyCtx, + alice.id(), + alice.id(), + {.expected_etxn_count = 1, + .nonce_used = {{uint256(0), true}}, + .result = { + .hookCanEmit = UINT256_BIT[ttINVOKE], + }}); + hook::HookAPI api(hookCtx); + auto tx = emitInvokeTx; + auto const result = api.emit(tx.getSerializer().slice()); + BEAST_EXPECT(result.error() == EMISSION_FAILURE); + } + { + // HookCanEmit (SetHook) Error + auto hookCtx = makeStubHookContext( + applyCtx, + alice.id(), + alice.id(), + { + .expected_etxn_count = 1, + .nonce_used = {{uint256(0), true}}, + .result = {.hookCanEmit = uint256()}, + }); + hook::HookAPI api(hookCtx); + auto const result = api.emit(emitSetHookTx.getSerializer().slice()); + BEAST_EXPECT(result.error() == EMISSION_FAILURE); + } + { + // HookCanEmit (SetHook) Success + auto hookCtx = makeStubHookContext( + applyCtx, + alice.id(), + alice.id(), + { + .expected_etxn_count = 1, + .nonce_used = {{uint256(0), true}}, + .result = {.hookCanEmit = UINT256_BIT[ttHOOK_SET]}, + }); + hook::HookAPI api(hookCtx); + auto tx = emitSetHookTx; + auto const result = api.emit(tx.getSerializer().slice()); + BEAST_EXPECT(result.has_value()); + } + + auto hookCtx = makeStubHookContext( + applyCtx, + alice.id(), + alice.id(), + { + .expected_etxn_count = 1, + .nonce_used = {{uint256(0), true}}, + .result = {.hookCanEmit = uint256()}, + }); + hook::HookAPI api(hookCtx); + { + // Invalid sfAccount + auto tx = emitInvokeTx; + { + // Missing sfAccount + tx.makeFieldAbsent(sfAccount); + auto const result = api.emit(tx.getSerializer().slice()); + BEAST_EXPECT(result.error() == EMISSION_FAILURE); + } + { + // Invalid sfAccount (!= HookAccount) + tx.setAccountID(sfAccount, bob.id()); + auto const result = api.emit(tx.getSerializer().slice()); + BEAST_EXPECT(result.error() == EMISSION_FAILURE); + } + } + { + // Invalid sfSequence + auto tx = emitInvokeTx; + { + // Missing sfSequence + tx.makeFieldAbsent(sfSequence); + auto const result = api.emit(tx.getSerializer().slice()); + BEAST_EXPECT(result.error() == EMISSION_FAILURE); + } + { + // Invalid sfSequence (non-zero) + tx.setFieldU32(sfSequence, 1); + auto const result = api.emit(tx.getSerializer().slice()); + BEAST_EXPECT(result.error() == EMISSION_FAILURE); + } + } + { + // Invalid sfSigningPubKey + auto tx = emitInvokeTx; + { + // Missing sfSigningPubKey + tx.makeFieldAbsent(sfSigningPubKey); + auto const result = api.emit(tx.getSerializer().slice()); + BEAST_EXPECT(result.error() == EMISSION_FAILURE); + } + { + // Invalid sfSigningPubKey (wrong size) + for (int i = 1; i < 33; ++i) + { + tx.setFieldVL(sfSigningPubKey, std::vector(i, 0)); + auto const result = api.emit(tx.getSerializer().slice()); + BEAST_EXPECT(result.error() == EMISSION_FAILURE); + } + } + { + // Invalid sfSigningPubKey (non-zero) + for (int i = 0; i < 33; ++i) + { + auto vec = std::vector(33, 0); + vec[i] = 1; + tx.setFieldVL(sfSigningPubKey, vec); + auto const result = api.emit(tx.getSerializer().slice()); + BEAST_EXPECT(result.error() == EMISSION_FAILURE); + } + } + } + { + // Invalid sfSigners + auto tx = emitInvokeTx; + tx.setFieldArray(sfSigners, STArray(sfSigners, 1)); + auto const result = api.emit(tx.getSerializer().slice()); + BEAST_EXPECT(result.error() == EMISSION_FAILURE); + } + { + // Invalid sfTicketSequence + auto tx = emitInvokeTx; + tx.setFieldU32(sfTicketSequence, 1); + auto const result = api.emit(tx.getSerializer().slice()); + BEAST_EXPECT(result.error() == EMISSION_FAILURE); + } + { + // Invalid sfAccountTxnID + auto tx = emitInvokeTx; + tx.setFieldH256(sfAccountTxnID, uint256(1)); + auto const result = api.emit(tx.getSerializer().slice()); + BEAST_EXPECT(result.error() == EMISSION_FAILURE); + } + { + ; // Invalid sfEmitDetails + { + // Missing sfEmitDetails + auto tx = emitInvokeTx; + tx.makeFieldAbsent(sfEmitDetails); + auto const result = api.emit(tx.getSerializer().slice()); + BEAST_EXPECT(result.error() == EMISSION_FAILURE); + } + { + std::vector> + detail_fields = { + sfEmitGeneration, + sfEmitBurden, + sfEmitParentTxnID, + sfEmitNonce, + sfEmitHookHash, + }; + // Missing fields in sfEmitDetails + for (auto const& rf : detail_fields) + { + SField const& field = rf.get(); + auto tx = emitInvokeTx; + auto& details = tx.peekFieldObject(sfEmitDetails); + details.makeFieldAbsent(field); + auto const result = api.emit(tx.getSerializer().slice()); + BEAST_EXPECT(result.error() == EMISSION_FAILURE); + } + } + // TODO: test callback + { + ; // Invalid sfEmitGeneration + { + // Over Max sfEmitGeneration + auto tx = emitInvokeTx; + auto& details = tx.peekFieldObject(sfEmitDetails); + details.setFieldU32(sfEmitGeneration, 11); + auto const result = api.emit(tx.getSerializer().slice()); + BEAST_EXPECT(result.error() == EMISSION_FAILURE); + } + { + // Invalid sfEmitGeneration + auto tx = emitInvokeTx; + auto& details = tx.peekFieldObject(sfEmitDetails); + details.setFieldU32( + sfEmitGeneration, hookCtx.generation + 2); + auto const result = api.emit(tx.getSerializer().slice()); + BEAST_EXPECT(result.error() == EMISSION_FAILURE); + } + } + { + // Invalid sfEmitBurden + auto tx = emitInvokeTx; + auto& details = tx.peekFieldObject(sfEmitDetails); + BEAST_EXPECT(hookCtx.burden == 0); + details.setFieldU64(sfEmitBurden, 2); + auto const result = api.emit(tx.getSerializer().slice()); + BEAST_EXPECT(result.error() == EMISSION_FAILURE); + } + { + // Invalid sfEmitParentTxnID + auto tx = emitInvokeTx; + auto& details = tx.peekFieldObject(sfEmitDetails); + BEAST_EXPECT(applyCtx.tx.getTransactionID() != uint256(1)); + details.setFieldH256(sfEmitParentTxnID, uint256(1)); + auto const result = api.emit(tx.getSerializer().slice()); + BEAST_EXPECT(result.error() == EMISSION_FAILURE); + } + { + // Invalid sfEmitNonce + auto tx = emitInvokeTx; + auto& details = tx.peekFieldObject(sfEmitDetails); + BEAST_EXPECT( + hookCtx.nonce_used.find(uint256(1)) == + hookCtx.nonce_used.end()); + details.setFieldH256(sfEmitNonce, uint256(1)); + auto const result = api.emit(tx.getSerializer().slice()); + BEAST_EXPECT(result.error() == EMISSION_FAILURE); + } + // TODO: test Callback + { + // Invalid sfEmitHookHash + auto tx = emitInvokeTx; + auto& details = tx.peekFieldObject(sfEmitDetails); + BEAST_EXPECT(hookCtx.result.hookHash != uint256(1)); + details.setFieldH256(sfEmitHookHash, uint256(1)); + auto const result = api.emit(tx.getSerializer().slice()); + BEAST_EXPECT(result.error() == EMISSION_FAILURE); + } + } + { + // Invalid sfTxnSignature + auto tx = emitInvokeTx; + tx.setFieldVL(sfTxnSignature, std::vector(1, 0)); + auto const result = api.emit(tx.getSerializer().slice()); + BEAST_EXPECT(result.error() == EMISSION_FAILURE); + } + { + ; // Invalid sfLastLedgerSequence + { + // Missing sfLastLedgerSequence + auto tx = emitInvokeTx; + tx.makeFieldAbsent(sfLastLedgerSequence); + auto const result = api.emit(tx.getSerializer().slice()); + BEAST_EXPECT(result.error() == EMISSION_FAILURE); + } + { + // Invalid sfLastLedgerSequence + // (smaller than next ledger seq) + auto tx = emitInvokeTx; + auto const currentSeq = applyCtx.view().info().seq; + tx.setFieldU32(sfLastLedgerSequence, currentSeq); + auto const result = api.emit(tx.getSerializer().slice()); + BEAST_EXPECT(result.error() == EMISSION_FAILURE); + } + { + // Invalid sfLastLedgerSequence + // (greater than current ledger seq + 5) + auto tx = emitInvokeTx; + auto const currentSeq = applyCtx.view().info().seq; + tx.setFieldU32(sfLastLedgerSequence, currentSeq + 6); + auto const result = api.emit(tx.getSerializer().slice()); + BEAST_EXPECT(result.error() == EMISSION_FAILURE); + } + } + { + ; // Invalid sfFirstLedgerSequence + { + // missing sfFirstLedgerSequence + auto tx = emitInvokeTx; + tx.makeFieldAbsent(sfFirstLedgerSequence); + auto const result = api.emit(tx.getSerializer().slice()); + BEAST_EXPECT(result.error() == EMISSION_FAILURE); + } + { + // Invalid sfFirstLedgerSequence + auto tx = emitInvokeTx; + auto const lastLedgerSeq = tx.getFieldU32(sfLastLedgerSequence); + tx.setFieldU32(sfFirstLedgerSequence, lastLedgerSeq + 1); + auto const result = api.emit(tx.getSerializer().slice()); + BEAST_EXPECT(result.error() == EMISSION_FAILURE); + } + } + { + ; // Invalid sfFee + { + // Missing sfFee + auto tx = emitInvokeTx; + tx.makeFieldAbsent(sfFee); + auto const result = api.emit(tx.getSerializer().slice()); + BEAST_EXPECT(result.error() == EMISSION_FAILURE); + } + { + // Invalid sfFee + auto tx = emitInvokeTx; + tx.setFieldAmount(sfFee, drops(1)); + auto const result = api.emit(tx.getSerializer().slice()); + BEAST_EXPECT(result.error() == EMISSION_FAILURE); + } + } + { + // Preflight failure + auto tx = emitInvokeTx; + tx.setFieldVL(sfBlob, std::vector(128 * 1024 + 1, 1)); + auto const result = api.emit(tx.getSerializer().slice()); + BEAST_EXPECT(result.error() == EMISSION_FAILURE); + } + { + // Success + auto tx = emitInvokeTx; + Serializer s; + tx.add(s); + auto const result = api.emit(s.slice()); + BEAST_EXPECT(result.has_value()); + } + } + + void + test_etxn_details(FeatureBitset features) + { + testcase("Test etxn_details"); + + using namespace jtx; + using namespace hook_api; + + auto const alice = Account{"alice"}; + Env env{*this, features}; + STTx invokeTx = STTx(ttINVOKE, [&](STObject& obj) {}); + OpenView ov{*env.current()}; + ApplyContext applyCtx = createApplyContext(env, ov, invokeTx); + + { + // PREREQUISITE_NOT_MET + auto hookCtx = + makeStubHookContext(applyCtx, alice.id(), alice.id(), {}); + hook::HookAPI api(hookCtx); + std::array buffer{}; + auto const result = api.etxn_details(buffer.data()); + BEAST_EXPECT(!result.has_value()); + BEAST_EXPECT(result.error() == PREREQUISITE_NOT_MET); + } + + { + // FEE_TOO_LARGE (via etxn_burden overflow) + StubHookContext stubCtx{ + .expected_etxn_count = 2, + .burden = std::numeric_limits::max(), + }; + auto hookCtx = + makeStubHookContext(applyCtx, alice.id(), alice.id(), stubCtx); + hook::HookAPI api(hookCtx); + std::array buffer{}; + auto const result = api.etxn_details(buffer.data()); + BEAST_EXPECT(!result.has_value()); + BEAST_EXPECT(result.error() == FEE_TOO_LARGE); + } + + { + // SUCCESS path length check and nonce increment (no-callback) + StubHookContext stubCtx{ + .expected_etxn_count = 2, + .result = {.hookHash = uint256{3}}, + }; + auto hookCtx = + makeStubHookContext(applyCtx, alice.id(), alice.id(), stubCtx); + hook::HookAPI api(hookCtx); + std::array buffer{}; + auto const result = api.etxn_details(buffer.data()); + BEAST_EXPECT(result.has_value()); + BEAST_EXPECT(result.value() == 116); + BEAST_EXPECT(hookCtx.emit_nonce_counter == 1); + } + + { + // SUCCESS path length check and nonce increment (callback) + StubHookContext stubCtx{ + .expected_etxn_count = 2, + .result = {.hookHash = uint256{3}, .hasCallback = true}, + }; + auto hookCtx = + makeStubHookContext(applyCtx, alice.id(), alice.id(), stubCtx); + hook::HookAPI api(hookCtx); + std::array buffer{}; + auto const result = api.etxn_details(buffer.data()); + BEAST_EXPECT(result.has_value()); + BEAST_EXPECT(result.value() == 138); + BEAST_EXPECT(hookCtx.emit_nonce_counter == 1); + } + } + + void + test_etxn_fee_base(FeatureBitset features) + { + testcase("Test etxn_fee_base"); + using namespace jtx; + using namespace hook_api; + + auto const alice = Account{"alice"}; + Env env{*this, features}; + STTx invokeTx = STTx(ttINVOKE, [&](STObject& obj) {}); + OpenView ov{*env.current()}; + ApplyContext applyCtx = createApplyContext(env, ov, invokeTx); + hook::HookContext hookCtx = makeStubHookContext( + applyCtx, + alice.id(), + alice.id(), + { + .expected_etxn_count = -1, + .nonce_used = {{uint256(0), true}}, + .result = {.hookCanEmit = uint256()}, + }); + hook::HookAPI api(hookCtx); + + // PREREQUISITE_NOT_MET + { + auto const result = + api.etxn_fee_base(invokeTx.getSerializer().slice()); + BEAST_EXPECT(result.error() == PREREQUISITE_NOT_MET); + } + + hookCtx.expected_etxn_count = 1; + + // INVALID_TXN + { + auto tx = invokeTx; + Serializer s = tx.getSerializer(); + s.add8(0); // invalid value + auto const result = api.etxn_fee_base(s.slice()); + BEAST_EXPECT(result.error() == INVALID_TXN); + } + { + // SUCCESS + auto const result = + api.etxn_fee_base(invokeTx.getSerializer().slice()); + BEAST_EXPECT(result.has_value()); + auto const baseFee = env.closed()->fees().base; + BEAST_EXPECT(result.value() == baseFee); + } + { + // Fee value + auto tx = invokeTx; + // add 100 bytes of blob + tx.setFieldVL(sfBlob, std::vector(100, 1)); + // add 100 bytes of memo + tx.setFieldArray(sfMemos, STArray(sfMemos, 1)); + auto& memos = tx.peekFieldArray(sfMemos); + STObject memo = STObject(sfMemo); + memo.setFieldVL(sfMemoData, std::vector(100, 1)); + memos.emplace_back(memo); + auto const result = api.etxn_fee_base(tx.getSerializer().slice()); + BEAST_EXPECT(result.has_value()); + auto const baseFee = env.closed()->fees().base; + auto const blobSize = 100; + auto const memoSize = 100; + if (env.closed()->rules().enabled(fixHookAPI20251128)) + BEAST_EXPECT(result.value() == baseFee + blobSize + memoSize); + else + BEAST_EXPECT(result.value() == baseFee + memoSize); + } + } + + void + test_etxn_burden(FeatureBitset features) + { + testcase("Test etxn_burden"); + using namespace jtx; + using namespace hook_api; + + auto const alice = Account{"alice"}; + Env env{*this, features}; + STTx invokeTx = STTx(ttINVOKE, [&](STObject& obj) {}); + OpenView ov{*env.current()}; + ApplyContext applyCtx = createApplyContext(env, ov, invokeTx); + + { + // PREREQUISITE_NOT_MET + auto hookCtx = + makeStubHookContext(applyCtx, alice.id(), alice.id(), {}); + hook::HookAPI api(hookCtx); + auto const result = api.etxn_burden(); + BEAST_EXPECT(result.error() == PREREQUISITE_NOT_MET); + } + + { + // FEE_TOO_LARGE (overflow) + StubHookContext stubCtx{ + .expected_etxn_count = 2, + .burden = std::numeric_limits::max(), + }; + auto hookCtx = + makeStubHookContext(applyCtx, alice.id(), alice.id(), stubCtx); + hook::HookAPI api(hookCtx); + auto const result = api.etxn_burden(); + BEAST_EXPECT(result.error() == FEE_TOO_LARGE); + } + + { + // SUCCESS + StubHookContext stubCtx{.expected_etxn_count = 3, .burden = 5}; + auto hookCtx = + makeStubHookContext(applyCtx, alice.id(), alice.id(), stubCtx); + hook::HookAPI api(hookCtx); + auto const result = api.etxn_burden(); + BEAST_EXPECT(result.has_value()); + BEAST_EXPECT(result.value() == 15); + } + } + + void + test_etxn_generation(FeatureBitset features) + { + testcase("Test etxn_generation"); + using namespace jtx; + + auto const alice = Account{"alice"}; + Env env{*this, features}; + STTx baseTx = STTx(ttINVOKE, [&](STObject& obj) {}); + + { + // Cached generation value + OpenView ov{*env.current()}; + ApplyContext applyCtx = createApplyContext(env, ov, baseTx); + StubHookContext stubCtx{.generation = 4}; + auto hookCtx = + makeStubHookContext(applyCtx, alice.id(), alice.id(), stubCtx); + hook::HookAPI api(hookCtx); + BEAST_EXPECT(api.etxn_generation() == 5); + } + + { + // No emit details -> otxn_generation() == 0 + OpenView ov{*env.current()}; + ApplyContext applyCtx = createApplyContext(env, ov, baseTx); + auto hookCtx = + makeStubHookContext(applyCtx, alice.id(), alice.id(), {}); + hook::HookAPI api(hookCtx); + BEAST_EXPECT(api.etxn_generation() == 1); + } + + { + // Emit details supply generation + STTx emitTx = STTx(ttINVOKE, [&](STObject& obj) { + obj.peekFieldObject(sfEmitDetails) + .setFieldU32(sfEmitGeneration, 2); + }); + OpenView ov{*env.current()}; + ApplyContext applyCtx = createApplyContext(env, ov, emitTx); + auto hookCtx = + makeStubHookContext(applyCtx, alice.id(), alice.id(), {}); + hook::HookAPI api(hookCtx); + BEAST_EXPECT(api.etxn_generation() == 3); + } + } + + void + test_etxn_nonce(FeatureBitset features) + { + testcase("Test etxn_nonce"); + + using namespace jtx; + using namespace hook_api; + + auto const alice = Account{"alice"}; + Env env{*this, features}; + STTx invokeTx = STTx(ttINVOKE, [&](STObject& obj) {}); + OpenView ov{*env.current()}; + ApplyContext applyCtx = createApplyContext(env, ov, invokeTx); + hook::HookContext hookCtx = makeStubHookContext( + applyCtx, + alice.id(), + alice.id(), + { + .expected_etxn_count = -1, + .nonce_used = {{uint256(0), true}}, + .result = {.hookCanEmit = uint256()}, + }); + + // TOO_MANY_NONCES + { + hookCtx.emit_nonce_counter = hook_api::max_nonce + 1; + hook::HookAPI api(hookCtx); + auto const result = api.etxn_nonce(); + BEAST_EXPECT(result.error() == TOO_MANY_NONCES); + } + + // SUCCESS + { + hookCtx.emit_nonce_counter = hook_api::max_nonce; + hook::HookAPI api(hookCtx); + auto const result = api.etxn_nonce(); + BEAST_EXPECT(result.has_value()); + } + + { + // Flags and cache tracking + StubHookContext stubCtx{ + .nonce_used = {}, + .result = + {.isCallback = true, + .isStrong = true, + .hookChainPosition = 2}, + }; + auto hookCtxNonce = + makeStubHookContext(applyCtx, alice.id(), alice.id(), stubCtx); + hook::HookAPI api(hookCtxNonce); + auto const expectedFlags = + static_cast(0b11U | (2U << 2U)); + auto const expected = ripple::sha512Half( + ripple::HashPrefix::emitTxnNonce, + hookCtxNonce.applyCtx.tx.getTransactionID(), + hookCtxNonce.emit_nonce_counter, + hookCtxNonce.result.account, + hookCtxNonce.result.hookHash, + expectedFlags); + auto const result = api.etxn_nonce(); + BEAST_EXPECT(result.has_value()); + BEAST_EXPECT(result.value() == expected); + BEAST_EXPECT(hookCtxNonce.emit_nonce_counter == 1); + BEAST_EXPECT(hookCtxNonce.nonce_used.count(expected) == 1); + } + } + + void + test_etxn_reserve(FeatureBitset features) + { + testcase("Test etxn_reserve"); + + using namespace jtx; + using namespace hook_api; + + auto const alice = Account{"alice"}; + Env env{*this, features}; + STTx invokeTx = STTx(ttINVOKE, [&](STObject& obj) {}); + OpenView ov{*env.current()}; + ApplyContext applyCtx = createApplyContext(env, ov, invokeTx); + + { + // ALREADY_SET + StubHookContext stubCtx{.expected_etxn_count = 1}; + auto hookCtx = + makeStubHookContext(applyCtx, alice.id(), alice.id(), stubCtx); + hook::HookAPI api(hookCtx); + auto const result = api.etxn_reserve(2); + BEAST_EXPECT(result.error() == ALREADY_SET); + } + + { + // TOO_SMALL + auto hookCtx = + makeStubHookContext(applyCtx, alice.id(), alice.id(), {}); + hook::HookAPI api(hookCtx); + auto const result = api.etxn_reserve(0); + BEAST_EXPECT(result.error() == TOO_SMALL); + } + + { + // TOO_BIG + auto hookCtx = + makeStubHookContext(applyCtx, alice.id(), alice.id(), {}); + hook::HookAPI api(hookCtx); + auto const result = api.etxn_reserve(hook_api::max_emit + 1); + BEAST_EXPECT(result.error() == TOO_BIG); + } + + { + // SUCCESS + auto hookCtx = + makeStubHookContext(applyCtx, alice.id(), alice.id(), {}); + hook::HookAPI api(hookCtx); + auto const result = api.etxn_reserve(3); + BEAST_EXPECT(result.has_value()); + BEAST_EXPECT(hookCtx.expected_etxn_count == 3); + } + } + + void + test_fee_base(FeatureBitset features) + { + testcase("Test fee_base"); + + using namespace jtx; + auto const alice = Account{"alice"}; + Env env{*this, features}; + STTx invokeTx = STTx(ttINVOKE, [&](STObject& obj) {}); + OpenView ov{*env.current()}; + ApplyContext applyCtx = createApplyContext(env, ov, invokeTx); + auto hookCtx = + makeStubHookContext(applyCtx, alice.id(), alice.id(), {}); + hook::HookAPI api(hookCtx); + + auto const expected = env.closed()->fees().base.drops(); + BEAST_EXPECT(api.fee_base() == expected); + } + + void + ASSERT_FLOAT_EQUAL(hook::HookAPI& api, uint64_t x, uint64_t y) + { + auto float_exponent = [](uint64_t f) -> int32_t { + return ((int32_t)(((f) >> 54U) & 0xFFU)) - 97; + }; + int64_t px = (x); + int64_t py = (y); + int64_t mx = api.float_mantissa(px).value(); + int64_t my = api.float_mantissa(py).value(); + int32_t diffexp = float_exponent(px) - float_exponent(py); + if (diffexp == 1) + mx *= 10LL; + if (diffexp == -1) + my *= 10LL; + int64_t diffman = mx - my; + if (diffman < 0) + diffman *= -1LL; + if (diffexp < 0) + diffexp *= -1; + if (diffexp > 1 || diffman > 5000000 || mx < 0 || my < 0) + BEAST_EXPECT(false); + else + BEAST_EXPECT(true); + } + + void + test_float_compare(FeatureBitset features) + { + testcase("Test float_compare"); + + using namespace jtx; + using namespace hook_api; + using namespace compare_mode; + + auto const alice = Account{"alice"}; + Env env{*this, features}; + STTx invokeTx = STTx(ttINVOKE, [&](STObject& obj) {}); + OpenView ov{*env.current()}; + ApplyContext applyCtx = createApplyContext(env, ov, invokeTx); + auto hookCtx = + makeStubHookContext(applyCtx, alice.id(), alice.id(), {}); + hook::HookAPI api(hookCtx); + + auto const testSuccess = + [&](uint64_t left, uint64_t right, uint32_t mode) { + auto const result = api.float_compare(left, right, mode); + BEAST_EXPECT(result.has_value()); + BEAST_EXPECT(result.value() == 1); + }; + + auto const testError = [&](uint64_t left, + uint64_t right, + uint32_t mode, + hook::HookReturnCode error) { + auto const result = api.float_compare(left, right, mode); + BEAST_EXPECT(!result.has_value()); + BEAST_EXPECT(result.error() == error); + }; + + auto const one = api.float_one(); + auto const two = api.float_set(0, 2).value(); + auto const three = 6091866696204910592ULL; // encoded 3.0 from SetHook + + testSuccess(one, one, EQUAL); + testSuccess(one, two, LESS); + testSuccess(two, one, GREATER); + testError(one, two, 0, INVALID_ARGUMENT); + + // Invalid flags + testError(one, two, 0b1000U, INVALID_ARGUMENT); + testError(one, two, ~0b111U, INVALID_ARGUMENT); + testError(one, two, 0b111U, INVALID_ARGUMENT); + + // Relative ordering samples + uint64_t largeNegative = 1622844335003378560ULL; // -154846915 + uint64_t smallNegative = 1352229899321148800ULL; // -1.15001111e-7 + uint64_t smallPositive = + 5713898440837102138ULL; // 3.33411333131321e-21 + uint64_t largePositive = 7749425685711506120ULL; // 3.234326634253e+92 + + testSuccess(largeNegative, smallNegative, LESS); + testSuccess(largeNegative, largePositive, LESS); + testSuccess(smallNegative, smallPositive, LESS); + testSuccess(smallPositive, largePositive, LESS); + testSuccess(smallNegative, 0, LESS); + testSuccess(largeNegative, 0, LESS); + testSuccess(smallPositive, 0, GREATER); + testSuccess(largePositive, 0, GREATER); + + // Not-equal flag check + testSuccess(two, three, GREATER | LESS); + } + + void + test_float_divide(FeatureBitset features) + { + testcase("Test float_divide"); + + using namespace jtx; + using namespace hook_api; + using namespace compare_mode; + + auto const alice = Account{"alice"}; + Env env{*this, features}; + STTx invokeTx = STTx(ttINVOKE, [&](STObject& obj) {}); + OpenView ov{*env.current()}; + ApplyContext applyCtx = createApplyContext(env, ov, invokeTx); + auto hookCtx = + makeStubHookContext(applyCtx, alice.id(), alice.id(), {}); + hook::HookAPI api(hookCtx); + + auto const one = api.float_one(); + + // divide by 0 + BEAST_EXPECT(api.float_divide(one, 0).error() == DIVISION_BY_ZERO); + BEAST_EXPECT(api.float_divide(0, one).value() == 0); + + // check 1 + BEAST_EXPECT(api.float_divide(one, one).value() == one); + BEAST_EXPECT( + api.float_divide(one, api.float_negate(one)).value() == + api.float_negate(one)); + BEAST_EXPECT( + api.float_divide(api.float_negate(one), one).value() == + api.float_negate(one)); + BEAST_EXPECT( + api.float_divide(api.float_negate(one), api.float_negate(one)) + .value() == one); + + // 1 / 10 = 0.1 + ASSERT_FLOAT_EQUAL( + api, + api.float_divide(one, 6107881094714392576LL).value(), + 6071852297695428608LL); + + // 123456789 / 1623 = 76067.0295749 + ASSERT_FLOAT_EQUAL( + api, + api.float_divide(6234216452170766464LL, 6144532891733356544LL) + .value(), + 6168530993200328528LL); + + // -1.245678451111 / 1.3546984132111e+42 = -9.195245517106014e-43 + ASSERT_FLOAT_EQUAL( + api, + api.float_divide(1478426356228633688LL, 6846826132016365020LL) + .value(), + 711756787386903390LL); + + // 9.134546514878452e-81 / 1 + ASSERT_FLOAT_EQUAL( + api, + api.float_divide(4638834963451748340LL, one).value(), + 4638834963451748340LL); + + // 9.134546514878452e-81 / 1.41649684651e+75 = (underflow 0) + ASSERT_FLOAT_EQUAL( + api, + api.float_divide(4638834963451748340LL, 7441363081262569392LL) + .value(), + 0); + + // 1.3546984132111e+42 / 9.134546514878452e-81 = XFL_OVERFLOW + BEAST_EXPECT( + api.float_divide(6846826132016365020LL, 4638834963451748340LL) + .error() == XFL_OVERFLOW); + + // clang-format off + std::vector> tests = { + {3121244226425810900LL /* -4.753284285427668e+91 */, 2135203055881892282LL /* -9.50403176301817e+36 */, 7066645550312560102LL /* 5.001334595622374e+54 */}, + {2473507938381460320LL /* -5.535342582428512e+55 */, 6365869885731270068LL /* 6787211884129716 */ , 2187897766692155363LL /* -8.155547044835299e+39 */}, + {1716271542690607496LL /* -49036842898190.16 */, 3137794549622534856LL /* -3.28920897266964e+92 */, 4667220053951274769LL /* 1.490839995440913e-79 */}, + {1588045991926420391LL /* -2778923.092005799 */, 5933338827267685794LL /* 6.601717648113058e-9 */, 1733591650950017206LL /* -420939403974674.2 */}, + {5880783758174228306LL /* 8.089844083101523e-12 */, 1396720886139976383LL /* -0.00009612200909863615 */, 1341481714205255877LL /* -8.416224503589061e-8 */}, + {5567703563029955929LL /* 1.254423600022873e-29 */, 2184969513100691140LL /* -5.227293453371076e+39 */, 236586937995245543LL /* -2.399757371979751e-69 */}, + {7333313065548121054LL /* 1.452872188953566e+69 */, 1755926008837497886LL /* -8529353417745438 */, 2433647177826281173LL /* -1.703379046213333e+53 */}, + {1172441975040622050LL /* -1.50607192429309e-17 */, 6692015311011173216LL /* 8.673463993357152e+33 */, 560182767210134346LL /* -1.736413416192842e-51 */}, + {577964843368607493LL /* -1.504091065184005e-50 */, 6422931182144699580LL /* 9805312769113276000 */, 235721135837751035LL /* -1.533955214485243e-69 */}, + {6039815413139899240LL /* 0.0049919124634346 */, 2117655488444284242LL /* -9.970862834892113e+35 */, 779625635892827768LL /* -5.006499985102456e-39 */}, + {1353563835098586141LL /* -2.483946887437341e-7 */, 6450909070545770298LL /* 175440415122002600000 */, 992207753070525611LL /* -1.415835049016491e-27 */}, + {6382158843584616121LL /* 50617712279937850 */, 5373794957212741595LL /* 5.504201387110363e-40 */, 7088854809772330055LL /* 9.196195545910343e+55 */}, + {2056891719200540975LL /* -3.250289119594799e+32 */, 1754532627802542730LL /* -7135972382790282 */, 6381651867337939070LL /* 45547949813167340 */}, + {5730152450208688630LL /* 1.573724193417718e-20 */, 1663581695074866883LL /* -62570322025.24355 */, 921249452789827075LL /* -2.515128806245891e-31 */}, + {6234301156018475310LL /* 131927173.7708846 */, 2868710604383082256LL /* -4.4212413754468e+77 */, 219156721749007916LL /* -2.983939635224108e-70 */}, + {2691125731495874243LL /* -6.980353583058627e+67 */, 7394070851520237320LL /* 8.16746263262388e+72 */, 1377640825464715759LL /* -0.000008546538744084975 */}, + {5141867696142208039LL /* 7.764120939842599e-53 */, 5369434678231981897LL /* 1.143922406350665e-40 */, 5861466794943198400LL /* 6.7872793615536e-13 */}, + {638296190872832492LL /* -7.792243040963052e-47 */, 5161669734904371378LL /* 9.551761192523954e-52 */, 1557396184145861422LL /* -81579.12330410798 */}, + {2000727145906286285LL /* -1.128911353786061e+29 */, 2096625200460673392LL /* -6.954973360763248e+34 */, 5982403476503576795LL /* 0.000001623171355558107 */}, + {640472838055334326LL /* -9.968890223464885e-47 */, 5189754252349396763LL /* 1.607481618585371e-50 */, 1537425431139169736LL /* -6201.557833201096 */}, + }; + // clang-format on + + for (auto const& test : tests) + { + ASSERT_FLOAT_EQUAL( + api, + api.float_divide(std::get<0>(test), std::get<1>(test)).value(), + std::get<2>(test)); + } + } + + void + test_float_int(FeatureBitset features) + { + testcase("Test float_int"); + + using namespace jtx; + using namespace hook_api; + + auto const alice = Account{"alice"}; + Env env{*this, features}; + STTx invokeTx = STTx(ttINVOKE, [&](STObject& obj) {}); + OpenView ov{*env.current()}; + ApplyContext applyCtx = createApplyContext(env, ov, invokeTx); + auto hookCtx = + makeStubHookContext(applyCtx, alice.id(), alice.id(), {}); + hook::HookAPI api(hookCtx); + + auto const one = api.float_one(); + + // clang-format off + + // check 1 + BEAST_EXPECT(api.float_int(one, 0, 0).value() == 1LL); + + // check 1.23e-20 always returns 0 (too small to display) + BEAST_EXPECT(api.float_int(5729808726015270912LL, 0, 0).value() == 0); + BEAST_EXPECT(api.float_int(5729808726015270912LL, 15, 0).value() == 0); + BEAST_EXPECT( + api.float_int(5729808726015270912LL, 16, 0).error() == + INVALID_ARGUMENT); + + BEAST_EXPECT(api.float_int(one, 15, 0).value() == 1000000000000000LL); + BEAST_EXPECT(api.float_int(one, 14, 0).value() == 100000000000000LL); + BEAST_EXPECT(api.float_int(one, 13, 0).value() == 10000000000000LL); + BEAST_EXPECT(api.float_int(one, 12, 0).value() == 1000000000000LL); + BEAST_EXPECT(api.float_int(one, 11, 0).value() == 100000000000LL); + BEAST_EXPECT(api.float_int(one, 10, 0).value() == 10000000000LL); + BEAST_EXPECT(api.float_int(one, 9, 0).value() == 1000000000LL); + BEAST_EXPECT(api.float_int(one, 8, 0).value() == 100000000LL); + BEAST_EXPECT(api.float_int(one, 7, 0).value() == 10000000LL); + BEAST_EXPECT(api.float_int(one, 6, 0).value() == 1000000LL); + BEAST_EXPECT(api.float_int(one, 5, 0).value() == 100000LL); + BEAST_EXPECT(api.float_int(one, 4, 0).value() == 10000LL); + BEAST_EXPECT(api.float_int(one, 3, 0).value() == 1000LL); + BEAST_EXPECT(api.float_int(one, 2, 0).value() == 100LL); + BEAST_EXPECT(api.float_int(one, 1, 0).value() == 10LL); + BEAST_EXPECT(api.float_int(one, 0, 0).value() == 1LL); + + // normal upper limit on exponent + BEAST_EXPECT(api.float_int(6360317241828374919LL, 0, 0).value() == 1234567981234567LL); + + // ask for one decimal above limit + BEAST_EXPECT(api.float_int(6360317241828374919LL, 1, 0).error() == TOO_BIG); + + // ask for 15 decimals above limit + BEAST_EXPECT(api.float_int(6360317241828374919LL, 15, 0).error() == TOO_BIG); + + // every combination for 1.234567981234567 + BEAST_EXPECT(api.float_int(6090101264186145159LL, 0, 0).value() == 1LL); + BEAST_EXPECT(api.float_int(6090101264186145159LL, 1, 0).value() == 12LL); + BEAST_EXPECT(api.float_int(6090101264186145159LL, 2, 0).value() == 123LL); + BEAST_EXPECT(api.float_int(6090101264186145159LL, 3, 0).value() == 1234LL); + BEAST_EXPECT(api.float_int(6090101264186145159LL, 4, 0).value() == 12345LL); + BEAST_EXPECT(api.float_int(6090101264186145159LL, 5, 0).value() == 123456LL); + BEAST_EXPECT(api.float_int(6090101264186145159LL, 6, 0).value() == 1234567LL); + BEAST_EXPECT(api.float_int(6090101264186145159LL, 7, 0).value() == 12345679LL); + BEAST_EXPECT(api.float_int(6090101264186145159LL, 8, 0).value() == 123456798LL); + BEAST_EXPECT(api.float_int(6090101264186145159LL, 9, 0).value() == 1234567981LL); + BEAST_EXPECT(api.float_int(6090101264186145159LL, 10, 0).value() == 12345679812LL); + BEAST_EXPECT(api.float_int(6090101264186145159LL, 11, 0).value() == 123456798123LL); + BEAST_EXPECT(api.float_int(6090101264186145159LL, 12, 0).value() == 1234567981234LL); + BEAST_EXPECT(api.float_int(6090101264186145159LL, 13, 0).value() == 12345679812345LL); + BEAST_EXPECT(api.float_int(6090101264186145159LL, 14, 0).value() == 123456798123456LL); + BEAST_EXPECT(api.float_int(6090101264186145159LL, 15, 0).value() == 1234567981234567LL); + + // same with absolute parameter + BEAST_EXPECT(api.float_int(1478415245758757255LL, 0, 1) .value() ==1LL); + BEAST_EXPECT(api.float_int(1478415245758757255LL, 1, 1) .value() ==12LL); + BEAST_EXPECT(api.float_int(1478415245758757255LL, 2, 1) .value() ==123LL); + BEAST_EXPECT(api.float_int(1478415245758757255LL, 3, 1) .value() ==1234LL); + BEAST_EXPECT(api.float_int(1478415245758757255LL, 4, 1) .value() ==12345LL); + BEAST_EXPECT(api.float_int(1478415245758757255LL, 5, 1) .value() ==123456LL); + BEAST_EXPECT(api.float_int(1478415245758757255LL, 6, 1) .value() ==1234567LL); + BEAST_EXPECT(api.float_int(1478415245758757255LL, 7, 1) .value() ==12345679LL); + BEAST_EXPECT(api.float_int(1478415245758757255LL, 8, 1) .value() ==123456798LL); + BEAST_EXPECT(api.float_int(1478415245758757255LL, 9, 1) .value() ==1234567981LL); + BEAST_EXPECT(api.float_int(1478415245758757255LL, 10, 1).value() == 12345679812LL); + BEAST_EXPECT(api.float_int(1478415245758757255LL, 11, 1).value() == 123456798123LL); + BEAST_EXPECT(api.float_int(1478415245758757255LL, 12, 1).value() == 1234567981234LL); + BEAST_EXPECT(api.float_int(1478415245758757255LL, 13, 1).value() == 12345679812345LL); + BEAST_EXPECT(api.float_int(1478415245758757255LL, 14, 1).value() == 123456798123456LL); + BEAST_EXPECT(api.float_int(1478415245758757255LL, 15, 1).value() == 1234567981234567LL); + + // neg xfl sans absolute parameter + BEAST_EXPECT(api.float_int(1478415245758757255LL, 15, 0).error() == CANT_RETURN_NEGATIVE); + + // 1.234567981234567e-16 + BEAST_EXPECT(api.float_int(5819885286543915399LL, 15, 0).value() == 1LL); + for (uint32_t i = 1; i < 15; ++i) + BEAST_EXPECT(api.float_int(5819885286543915399LL, i, 0).value() == 0); + + // clang-format on + } + + void + test_float_invert(FeatureBitset features) + { + testcase("Test float_invert"); + + using namespace jtx; + using namespace hook_api; + + auto const alice = Account{"alice"}; + Env env{*this, features}; + STTx invokeTx = STTx(ttINVOKE, [&](STObject& obj) {}); + OpenView ov{*env.current()}; + ApplyContext applyCtx = createApplyContext(env, ov, invokeTx); + auto hookCtx = + makeStubHookContext(applyCtx, alice.id(), alice.id(), {}); + hook::HookAPI api(hookCtx); + + // divide by 0 + BEAST_EXPECT(api.float_invert(0).error() == DIVISION_BY_ZERO); + + // check 1 + BEAST_EXPECT( + api.float_invert(api.float_one()).value() == api.float_one()); + + // 1 / 10 = 0.1 + ASSERT_FLOAT_EQUAL( + api, + api.float_invert(6107881094714392576LL).value(), + 6071852297695428608LL); + + // 1 / 123 = 0.008130081300813009 + ASSERT_FLOAT_EQUAL( + api, + api.float_invert(6126125493223874560LL).value(), + 6042953581977277649LL); + + // 1 / 1234567899999999 = 8.100000008100007e-16 + ASSERT_FLOAT_EQUAL( + api, + api.float_invert(6360317241747140351LL).value(), + 5808736320061298855LL); + + // 1/ 1*10^-81 = 10**81 + ASSERT_FLOAT_EQUAL( + api, + api.float_invert(4630700416936869888LL).value(), + 7540018576963469311LL); + } + + void + test_float_log(FeatureBitset features) + { + testcase("Test float_log"); + + using namespace jtx; + using namespace hook_api; + + auto const alice = Account{"alice"}; + Env env{*this, features}; + STTx invokeTx = STTx(ttINVOKE, [&](STObject& obj) {}); + OpenView ov{*env.current()}; + ApplyContext applyCtx = createApplyContext(env, ov, invokeTx); + auto hookCtx = + makeStubHookContext(applyCtx, alice.id(), alice.id(), {}); + hook::HookAPI api(hookCtx); + + // check 0 is not allowed + BEAST_EXPECT(api.float_log(0).error() == INVALID_ARGUMENT); + + // log10( 846513684968451 ) = 14.92763398342338 + ASSERT_FLOAT_EQUAL( + api, + api.float_log(6349533412187342878LL).value(), + 6108373858112734914LL); + + // log10 ( -1000 ) = invalid (complex not supported) + BEAST_EXPECT( + api.float_log(1532223873305968640LL).error() == + COMPLEX_NOT_SUPPORTED); + + // log10 (1000) == 3 + ASSERT_FLOAT_EQUAL( + api, + api.float_log(6143909891733356544LL).value(), + 6091866696204910592LL); + + // log10 (0.112381) == -0.949307107740766 + ASSERT_FLOAT_EQUAL( + api, + api.float_log(6071976107695428608LL).value(), + 1468659350345448364LL); + + // log10 (0.00000000000000001123) = -16.94962024373854221 + ASSERT_FLOAT_EQUAL( + api, + api.float_log(5783744921543716864LL).value(), + 1496890038311378526LL); + + // log10(100000000000000000000000000000000000000000000000000000000000000) + // = 62 + ASSERT_FLOAT_EQUAL( + api, + api.float_log(7206759403792793600LL).value(), + 6113081094714392576LL); + } + + void + test_float_mantissa(FeatureBitset features) + { + testcase("Test float_mantissa"); + + using namespace jtx; + using namespace hook_api; + + auto const alice = Account{"alice"}; + Env env{*this, features}; + STTx invokeTx = STTx(ttINVOKE, [&](STObject& obj) {}); + OpenView ov{*env.current()}; + ApplyContext applyCtx = createApplyContext(env, ov, invokeTx); + auto hookCtx = + makeStubHookContext(applyCtx, alice.id(), alice.id(), {}); + hook::HookAPI api(hookCtx); + + // test canonical zero + BEAST_EXPECT(api.float_mantissa(0).value() == 0); + + // test one, negative one + { + BEAST_EXPECT( + api.float_mantissa(api.float_one()).value() == + 1000000000000000LL); + BEAST_EXPECT( + api.float_mantissa(api.float_negate(api.float_one())).value() == + 1000000000000000LL); + } + + // test random numbers + { + // clang-format off + std::vector> tests = { + {4763370308433150973LL /* 7.569101929907197e-74 */, 7569101929907197LL}, + {668909658849475214LL /* -2.376913998641806e-45 */, 2376913998641806LL}, + {962271544155031248LL /* -7.508423152486096e-29 */, 7508423152486096LL}, + {7335644976228470276LL /* 3.784782869302788e+69 */, 3784782869302788LL}, + {2837780149340315954LL /* -9.519583351644467e+75 */, 9519583351644466LL}, + {2614004940018599738LL /* -1.917156143712058e+63 */, 1917156143712058LL}, + {4812250541755005603LL /* 2.406139723315875e-71 */, 2406139723315875LL}, + {5140304866732560580LL /* 6.20129153019514e-53 */, 6201291530195140LL}, + {1124677839589482624LL /* -7.785132001599617e-20 */, 7785132001599616LL}, + {5269336076015865585LL /* 9.131711247126257e-46 */, 9131711247126257LL}, + {2296179634826760368LL /* -8.3510241225484e+45 */, 8351024122548400LL}, + {1104028240398536470LL /* -5.149931320135446e-21 */, 5149931320135446LL}, + {2691222059222981864LL /* -7.076681310166248e+67 */, 7076681310166248LL}, + {6113256168823855946LL /* 63.7507410946337 */, 6375074109463370LL}, + {311682216630003626LL /* -5.437441968809898e-65 */, 5437441968809898LL}, + {794955605753965262LL /* -2.322071336757966e-38 */, 2322071336757966LL}, + {204540636400815950LL /* -6.382252796514126e-71 */, 6382252796514126LL}, + {5497195278343034975LL /* 2.803732951029855e-33 */, 2803732951029855LL}, + {1450265914369875626LL /* -0.09114033611316906 */, 9114033611316906LL}, + {7481064015089962668LL /* 5.088633654939308e+77 */, 5088633654939308LL}, + }; + // clang-format on + + for (auto const& test : tests) + { + ASSERT_FLOAT_EQUAL( + api, + api.float_mantissa(std::get<0>(test)).value(), + std::get<1>(test)); + } + } + } + + void + test_float_mulratio(FeatureBitset features) + { + testcase("Test float_mulratio"); + + using namespace jtx; + using namespace hook_api; + + auto const alice = Account{"alice"}; + Env env{*this, features}; + STTx invokeTx = STTx(ttINVOKE, [&](STObject& obj) {}); + OpenView ov{*env.current()}; + ApplyContext applyCtx = createApplyContext(env, ov, invokeTx); + auto hookCtx = + makeStubHookContext(applyCtx, alice.id(), alice.id(), {}); + hook::HookAPI api(hookCtx); + + auto const one = api.float_one(); + auto const neg_one = api.float_negate(one); + + // multiply by 0 + BEAST_EXPECT(api.float_mulratio(one, 0, 0, 1).value() == 0); + BEAST_EXPECT(api.float_mulratio(0, 0, 1, 1).value() == 0); + + // check 1 + BEAST_EXPECT(api.float_mulratio(one, 0, 1, 1).value() == one); + BEAST_EXPECT(api.float_mulratio(neg_one, 0, 1, 1).value() == neg_one); + + // check overflow + // 1e+95 * 1e+95 + BEAST_EXPECT( + api.float_mulratio(7801234554605699072LL, 0, 0xFFFFFFFFUL, 1) + .error() == XFL_OVERFLOW); + // 1e+95 * 10 + BEAST_EXPECT( + api.float_mulratio(7801234554605699072LL, 0, 10, 1).error() == + XFL_OVERFLOW); + // -1e+95 * 10 + BEAST_EXPECT( + api.float_mulratio(3189548536178311168LL, 0, 10, 1).error() == + XFL_OVERFLOW); + + // identity + ASSERT_FLOAT_EQUAL( + api, + api.float_mulratio(3189548536178311168LL, 0, 1, 1).value(), + 3189548536178311168LL); + + // random mulratios + + // clang-format off + std::vector> tests = { + {2296131684119423544LL, 2210828011U, 2814367554U, 2294351094683836182LL}, + {565488225163275031LL, 2373474507U, 4203973264U, 562422045628095449LL}, + {2292703263479286183LL, 3170020147U, 773892643U, 2307839765178024100LL}, + {758435948837102675LL, 3802740780U, 1954123588U, 760168290112163547LL}, + {3063742137774439410LL, 2888815591U, 4122448592U, 3053503824756415637LL}, + {974014561126802184LL, 689168634U, 3222648522U, 957408554638995792LL}, + {2978333847445611553LL, 1718558513U, 2767410870U, 2976075722223325259LL}, + {6577058837932757648LL, 1423256719U, 1338068927U, 6577173649752398013LL}, + {2668681541248816636LL, 345215754U, 4259223936U, 2650183845127530219LL}, + {651803640367065917LL, 327563234U, 1191613855U, 639534906402789368LL}, + {3154958130393015979LL, 1304112625U, 3024066701U, 3153571282364880740LL}, + {1713286099776800976LL, 1902151138U, 2927030061U, 1712614441093927706LL}, + {2333142120591277120LL, 914099656U, 108514965U, 2349692988167140475LL}, + {995968561418010814LL, 1334462574U, 846156977U, 998955931389416094LL}, + {6276035843030312442LL, 2660687613U, 236740983U, 6294920527635363073LL}, + {7333118474702086419LL, 46947714U, 2479204760U, 7298214153648998535LL}, + {2873297486994296492LL, 880591893U, 436034100U, 2884122995598532757LL}, + {1935815261812737573LL, 3123665800U, 3786746543U, 1934366328810191207LL}, + {7249556282125616118LL, 2378803159U, 2248850590U, 7250005170160875417LL}, + {311005347529659996LL, 992915590U, 2433548552U, 308187142737041830LL}, + }; + // clang-format on + + for (auto const& test : tests) + { + ASSERT_FLOAT_EQUAL( + api, + api.float_mulratio( + std::get<0>(test), + 0U, + std::get<1>(test), + std::get<2>(test)) + .value(), + std::get<3>(test)); + } + } + + void + test_float_multiply(FeatureBitset features) + { + testcase("Test float_multiply"); + + using namespace jtx; + using namespace hook_api; + + auto const alice = Account{"alice"}; + Env env{*this, features}; + STTx invokeTx = STTx(ttINVOKE, [&](STObject& obj) {}); + OpenView ov{*env.current()}; + ApplyContext applyCtx = createApplyContext(env, ov, invokeTx); + auto hookCtx = + makeStubHookContext(applyCtx, alice.id(), alice.id(), {}); + hook::HookAPI api(hookCtx); + + auto const one = api.float_one(); + auto const neg_one = api.float_negate(one); + + // multiply by 0 + BEAST_EXPECT(api.float_multiply(one, 0).value() == 0); + BEAST_EXPECT(api.float_multiply(0, one).value() == 0); + + // check 1 + BEAST_EXPECT(api.float_multiply(one, one).value() == one); + BEAST_EXPECT(api.float_multiply(one, neg_one).value() == neg_one); + BEAST_EXPECT(api.float_multiply(neg_one, one).value() == neg_one); + BEAST_EXPECT(api.float_multiply(neg_one, neg_one).value() == one); + + // check overflow + // 1e+95 * 1e+95 + BEAST_EXPECT( + api.float_multiply(7801234554605699072LL, 7801234554605699072LL) + .error() == XFL_OVERFLOW); + // 1e+95 * 10 + BEAST_EXPECT( + api.float_multiply(7801234554605699072LL, 6107881094714392576LL) + .error() == XFL_OVERFLOW); + BEAST_EXPECT( + api.float_multiply(6107881094714392576LL, 7801234554605699072LL) + .error() == XFL_OVERFLOW); + // -1e+95 * 10 + BEAST_EXPECT( + api.float_multiply(3189548536178311168LL, 6107881094714392576LL) + .error() == XFL_OVERFLOW); + + // identity + ASSERT_FLOAT_EQUAL( + api, + api.float_multiply(3189548536178311168LL, one).value(), + 3189548536178311168LL); + ASSERT_FLOAT_EQUAL( + api, + api.float_multiply(one, 3189548536178311168LL).value(), + 3189548536178311168LL); + + // random multiplications + + // clang-format off + std::vector> tests = { + {7791757438262485039LL /* 9.537282166267951e+94 */, 4759088999670263908LL /* 3.287793167020132e-74 */, 6470304726017852129LL /* 3.135661113819873e+21 */}, + {7534790022873909775LL /* 4.771445910440463e+80 */, 1017891960669847079LL /* -9.085644138855975e-26 */, 2472307761756037979LL /* -4.335165957006171e+55 */}, + {2813999069907898454LL /* -3.75290242870895e+74 */, 4962524721184225460LL /* 8.56513107667986e-63 */, 1696567870013294731LL /* -3214410121988.235 */}, + {2151742066453140308LL /* -8.028643824784212e+37 */, 437647738130579252LL /* -5.302173903011636e-58 */, 5732835652591705549LL /* 4.256926576434637e-20 */}, + {5445302332922546340LL /* 4.953983058987172e-36 */, 7770966530708354172LL /* 6.760773121619068e+93 */, 7137051085305881332LL /* 3.349275551015668e+58 */}, + {2542989542826132533LL /* -2.959352989172789e+59 */, 6308418769944702613LL /* 3379291626008.213 */, 2775217422137696934LL /* -1.000051677471398e+72 */}, + {5017652318929433511LL /* 9.649533293441959e-60 */, 6601401767766764916LL /* 8.131913296358772e+28 */, 5538267259220228820LL /* 7.846916809259732e-31 */}, + {892430323307269235LL /* -9.724796342652019e-33 */, 1444078017997143500LL /* -0.0292613723858478 */, 5479222755754111850LL /* 2.845608871588714e-34 */}, + {7030632722283214253LL /* 5.017303585240493e+52 */, 297400838197636668LL /* -9.170462045924924e-66 */, 1247594596364389994LL /* -4.601099210133098e-13 */}, + {1321751204165279730LL /* -6.700112973094898e-9 */, 2451801790748530375LL /* -1.843593458980551e+54 */, 6918764256086244704LL /* 1.235228445162848e+46 */}, + {2055496484261758590LL /* -1.855054180812414e+32 */, 2079877890137711361LL /* -8.222061547283201e+33 */, 7279342234795540005LL /* 1.525236964818469e+66 */}, + {2439875962311968674LL /* -7.932163531900834e+53 */, 4707485682591872793LL /* 5.727671617074969e-77 */, 1067392794851803610LL /* -4.543282792366554e-23 */}, + {6348574818322812800LL /* 750654298515443.2 */, 6474046245013515838LL /* 6.877180109483582e+21 */, 6742547427357110773LL /* 5.162384810848757e+36 */}, + {1156137305783593424LL /* -3.215801176746448e-18 */, 351790564990861307LL /* -9.516993310703611e-63 */, 4650775291275116747LL /* 3.060475828764875e-80 */}, + {5786888485280994123LL /* 4.266563737277259e-17 */, 6252137323085080394LL /* 1141040294.831946 */, 5949619829273756852LL /* 4.868321144702132e-8 */}, + {2078182880999439640LL /* -6.52705240901148e+33 */, 1662438186251269392LL /* -51135233789.26864 */, 6884837854131013998LL /* 3.33762350889611e+44 */}, + {1823781083140711248LL /* -43268336830308640000 */, 1120252241608199010LL /* -3.359534020316002e-20 */, 6090320310700749729LL /* 1.453614495839137 */}, + {6617782604883935174LL /* 6.498351904047046e+29 */, 6185835042802056262LL /* 689635.404973575 */, 6723852137583788319LL /* 4.481493547008287e+35 */}, + {333952667495151166LL /* -9.693494324475454e-64 */, 1556040883317758614LL /* -68026.1150230799 */, 5032611291744396930LL /* 6.594107598923394e-59 */}, + {2326968399632616779LL /* -3.110991909440843e+47 */, 707513695207834635LL /* -4.952153338037259e-43 */, 6180479299649214949LL /* 154061.0896894437 */}, + {1271003508324696477LL /* -9.995612660957597e-12 */, 5321949753651889765LL /* 7.702193354704484e-43 */, 512101972406838314LL /* -7.698814141342762e-54 */}, + {1928646740923345323LL /* -1.106100408773035e+25 */, 4639329980209973352LL /* 9.629563273103463e-81 */, 487453886143282122LL /* -1.065126387268554e-55 */}, + {6023906813956669432LL /* 0.0007097711789686777 */, 944348444470060009LL /* -7.599721976996842e-30 */, 888099590592064434LL /* -5.394063627447218e-33 */}, + {6580290597764062787LL /* 5.035141803138627e+27 */, 6164319297265300034LL /* 33950.07022461506 */, 6667036882686408593LL /* 1.709434178074513e+32 */}, + {2523439530503240484LL /* -1.423739175762724e+58 */, 5864448766677980801LL /* 9.769251096336e-13 */, 2307233895764065602LL /* -1.39088655037165e+46 */}, + {6760707453987140465LL /* 5.308012931396465e+37 */, 5951641080643457645LL /* 6.889572514402925e-8 */, 6632955645489194550LL /* 3.656993999824438e+30 */}, + {6494270716308443375LL /* 9.087252894929135e+22 */, 564752637895553836LL /* -6.306284101612332e-51 */, 978508199357889360LL /* -5.730679845862224e-28 */}, + {6759145618427534062LL /* 3.746177371790062e+37 */, 4721897842483633304LL /* 2.125432999353496e-76 */, 5394267403342547165LL /* 7.962249007433949e-39 */}, + {1232673571201806425LL /* -7.694472557031513e-14 */, 6884256144221925318LL /* 2.75591359980743e+44 */, 2037747561727791012LL /* -2.12053015632682e+31 */}, + {1427694775835421031LL /* -0.004557293586344295 */, 4883952867277976402LL /* 2.050871208358738e-67 */, 225519204318055258LL /* -9.34642220427145e-70 */}, + {5843509949864662087LL /* 6.84483279249927e-14 */, 5264483986612843822LL /* 4.279621844104494e-46 */, 5028946513739275800LL /* 2.929329593802264e-59 */}, + {6038444022009738988LL /* 0.003620521333274348 */, 7447499078040748850LL /* 7.552493624689458e+75 */, 7406652183825856093LL /* 2.734396428760669e+73 */}, + {939565473697468970LL /* -2.816751204405802e-30 */, 1100284903077087966LL /* -1.406593998686942e-21 */, 5174094397561240825LL /* 3.962025339911417e-51 */}, + {5694071830210473617LL /* 1.521901214166673e-22 */, 5536709154363579683LL /* 6.288811952610595e-31 */, 5143674525748709391LL /* 9.570950546343951e-53 */}, + {600729862341871819LL /* -6.254711528966347e-49 */, 6330630279715378440LL /* 75764028872020.56 */, 851415551394320910LL /* -4.738821448667662e-35 */}, + {1876763139233864902LL /* -3.265694247738566e+22 */, 4849561230315278754LL /* 3.688031264625058e-69 */, 649722744589988028LL /* -1.204398248636604e-46 */}, + {3011947542126279863LL /* -3.542991042788535e+85 */, 1557732559110376235LL /* -84942.87294925611 */,7713172080438368541LL /* 3.009518380079389e+90 */}, + {5391579936313268788LL /* 5.274781978155572e-39 */, 1018647290024655822LL /* -9.840973493664718e-26 */, 329450072133864644LL /* -5.190898963188932e-64 */}, + {2815029221608845312LL /* -4.783054129655808e+74 */, 4943518985822088837LL /* 7.57379422402522e-64 */,1678961648155863225LL /* -362258677403.8713 */ }, + {1377509900308195934LL /* -0.00000841561358756515 */, 7702104197062186199LL /* 9.95603351337903e+89 */, 2998768765665354000LL /* -8.378613091344656e+84 */}, + }; + // clang-format on + + for (auto const& test : tests) + { + ASSERT_FLOAT_EQUAL( + api, + api.float_multiply(std::get<0>(test), std::get<1>(test)) + .value(), + std::get<2>(test)); + } + } + + void + test_float_negate(FeatureBitset features) + { + testcase("Test float_negate"); + + using namespace jtx; + using namespace hook_api; + + auto const alice = Account{"alice"}; + Env env{*this, features}; + STTx invokeTx = STTx(ttINVOKE, [&](STObject& obj) {}); + OpenView ov{*env.current()}; + ApplyContext applyCtx = createApplyContext(env, ov, invokeTx); + auto hookCtx = + makeStubHookContext(applyCtx, alice.id(), alice.id(), {}); + hook::HookAPI api(hookCtx); + + auto const one = api.float_one(); + + // test canonical zero + BEAST_EXPECT(api.float_negate(0) == 0); + + // test double negation + { + BEAST_EXPECT(api.float_negate(one) != one); + BEAST_EXPECT(api.float_negate(api.float_negate(one)) == one); + } + + // test random numbers + { + // +/- 3.463476342523e+22 + BEAST_EXPECT( + api.float_negate(6488646939756037240LL) == + 1876960921328649336LL); + + BEAST_EXPECT(api.float_negate(one) == 1478180677777522688LL); + + BEAST_EXPECT( + api.float_negate(1838620299498162368LL) == + 6450306317925550272LL); + } + } + + void + test_float_one(FeatureBitset features) + { + testcase("Test float_one"); + + using namespace jtx; + using namespace hook_api; + + auto const alice = Account{"alice"}; + Env env{*this, features}; + STTx invokeTx = STTx(ttINVOKE, [&](STObject& obj) {}); + OpenView ov{*env.current()}; + ApplyContext applyCtx = createApplyContext(env, ov, invokeTx); + auto hookCtx = + makeStubHookContext(applyCtx, alice.id(), alice.id(), {}); + hook::HookAPI api(hookCtx); + + auto const one = api.float_one(); + BEAST_EXPECT(one == 6089866696204910592ULL); + } + + void + test_float_root(FeatureBitset features) + { + testcase("Test float_root"); + + using namespace jtx; + using namespace hook_api; + using namespace compare_mode; + + auto const alice = Account{"alice"}; + Env env{*this, features}; + STTx invokeTx = STTx(ttINVOKE, [&](STObject& obj) {}); + OpenView ov{*env.current()}; + ApplyContext applyCtx = createApplyContext(env, ov, invokeTx); + auto hookCtx = + makeStubHookContext(applyCtx, alice.id(), alice.id(), {}); + hook::HookAPI api(hookCtx); + + auto const one = api.float_one(); + + // sqrt 1 is 1 + ASSERT_FLOAT_EQUAL(api, api.float_root(one, 2).value(), one); + + // sqrt 9 is 3 + auto const nine = 6097866696204910592LL; + auto const three = 6091866696204910592LL; + ASSERT_FLOAT_EQUAL(api, api.float_root(nine, 2).value(), three); + + // cube root of 1000 is 10 + auto const thousand = 6143909891733356544LL; + auto const ten = 6107881094714392576LL; + ASSERT_FLOAT_EQUAL(api, api.float_root(thousand, 3).value(), ten); + + // sqrt of negative is "complex not supported error" + auto const negative_one = 1478180677777522688LL; + BEAST_EXPECT( + api.float_root(negative_one, 2).error() == COMPLEX_NOT_SUPPORTED); + + // tenth root of 0 is 0 + ASSERT_FLOAT_EQUAL(api, api.float_root(0, 10).value(), 0); + } + + void + test_float_set(FeatureBitset features) + { + testcase("Test float_set"); + + using namespace jtx; + using namespace hook_api; + + auto const alice = Account{"alice"}; + Env env{*this, features}; + STTx invokeTx = STTx(ttINVOKE, [&](STObject& obj) {}); + OpenView ov{*env.current()}; + ApplyContext applyCtx = createApplyContext(env, ov, invokeTx); + auto hookCtx = + makeStubHookContext(applyCtx, alice.id(), alice.id(), {}); + hook::HookAPI api(hookCtx); + + // zero mantissa should return canonical zero + BEAST_EXPECT(api.float_set(-5, 0).value() == 0); + BEAST_EXPECT(api.float_set(50, 0).value() == 0); + BEAST_EXPECT(api.float_set(-50, 0).value() == 0); + BEAST_EXPECT(api.float_set(0, 0).value() == 0); + + // an exponent lower than -96 should produce an invalid float error + BEAST_EXPECT(api.float_set(-97, 1).error() == INVALID_FLOAT); + + // an exponent larger than +96 should produce an invalid float error + BEAST_EXPECT(api.float_set(+97, 1).error() == INVALID_FLOAT); + + // clang-format off + std::vector> tests = { + {-5, 6541432897943971LL, 6275552114197674403LL}, + {-83, 7906202688397446LL, 4871793800248533126LL}, + {76, 4760131426754533LL, 7732937091994525669LL}, + {37, -8019384286534438LL, 2421948784557120294LL}, + {50, 5145342538007840LL, 7264947941859247392LL}, + {-70, 4387341302202416LL, 5102462119485603888LL}, + {-26, -1754544005819476LL, 1280776838179040340LL}, + {36, 8261761545780560LL, 7015862781734272336LL}, + {35, 7975622850695472LL, 6997562244529705264LL}, + {17, -4478222822793996LL, 2058119652903740172LL}, + {-53, 5506604247857835LL, 5409826157092453035LL}, + {-60, 5120164869507050LL, 5283338928147728362LL}, + {41, 5176113875683063LL, 7102849126611584759LL}, + {-54, -3477931844992923LL, 778097067752718235LL}, + {21, 6345031894305479LL, 6743730074440567495LL}, + {-23, 5091583691147091LL, 5949843091820201811LL}, + {-33, 7509684078851678LL, 5772117207113086558LL}, + {-72, -1847771838890268LL, 452207734575939868LL}, + {71, -9138413713437220LL, 3035557363306410532LL}, + {28, 4933894067102586LL, 6868419726179738490LL}, + }; + // clang-format on + + for (auto const& test : tests) + { + ASSERT_FLOAT_EQUAL( + api, + api.float_set(std::get<0>(test), std::get<1>(test)).value(), + std::get<2>(test)); + } + } + + void + test_float_sign(FeatureBitset features) + { + testcase("Test float_sign"); + + using namespace jtx; + using namespace hook_api; + + auto const alice = Account{"alice"}; + Env env{*this, features}; + STTx invokeTx = STTx(ttINVOKE, [&](STObject& obj) {}); + OpenView ov{*env.current()}; + ApplyContext applyCtx = createApplyContext(env, ov, invokeTx); + auto hookCtx = + makeStubHookContext(applyCtx, alice.id(), alice.id(), {}); + hook::HookAPI api(hookCtx); + + // test canonical zero + BEAST_EXPECT(api.float_sign(0) == 0); + + // test one + auto const one = api.float_one(); + BEAST_EXPECT(api.float_sign(one) == 0); + BEAST_EXPECT(api.float_sign(api.float_negate(one)) == 1); + + // test random numbers + + // clang-format off + std::vector> tests = { + {7248434512952957686LL /* 6.646312141200119e+64 */, 0LL}, + {889927818394811978LL /* -7.222291430194763e-33 */, 1LL}, + {5945816149233111421LL /* 1.064641104056701e-8 */, 0LL}, + {6239200145838704863LL /* 621826155.7938399 */, 0LL}, + {6992780785042190360LL /* 3.194163363180568e+50 */, 0LL}, + {6883099933108789087LL /* 1.599702486671199e+44 */, 0LL}, + {890203738162163464LL /* -7.498211197546248e-33 */, 1LL}, + {4884803073052080964LL /* 2.9010769824633e-67 */, 0LL}, + {2688292350356944394LL /* -4.146972444128778e+67 */, 1LL}, + {4830109852288093280LL /* 2.251051746921568e-70 */, 0LL}, + {294175951907940320LL /* -5.945575756228576e-66 */, 1LL}, + {7612037404955382316LL /* 9.961233953985069e+84 */, 0LL}, + {7520840929603658997LL /* 8.83675114967167e+79 */, 0LL}, + {4798982086157926282LL /* 7.152082635718538e-72 */, 0LL}, + {689790136568817905LL /* -5.242993208502513e-44 */, 1LL}, + {5521738045011558042LL /* 9.332101110070938e-32 */, 0LL}, + {728760820583452906LL /* -8.184880204173546e-42 */, 1LL}, + {2272937984362856794LL /* -3.12377216812681e+44 */, 1LL}, + {1445723661896317830LL /* -0.0457178113775911 */, 1LL}, + {5035721527359772724LL /* 9.704343214299189e-59 */, 0LL}, + }; + // clang-format on + + for (auto const& test : tests) + { + BEAST_EXPECT( + api.float_sign(std::get<0>(test)) == std::get<1>(test)); + } + } + + void + test_float_sto(FeatureBitset features) + { + testcase("Test float_sto"); + + using namespace jtx; + using namespace hook_api; + + auto const alice = Account{"alice"}; + Env env{*this, features}; + STTx invokeTx = STTx(ttINVOKE, [&](STObject& obj) {}); + OpenView ov{*env.current()}; + ApplyContext applyCtx = createApplyContext(env, ov, invokeTx); + auto hookCtx = + makeStubHookContext(applyCtx, alice.id(), alice.id(), {}); + hook::HookAPI api(hookCtx); + + uint64_t const xfl = 6198187654261802496ULL; // 1234567.0 as in SetHook + // XRP serialization + auto const ser = api.float_sto(std::nullopt, std::nullopt, xfl, 0, 8); + BEAST_EXPECT(ser.has_value()); + auto const parsed = api.float_sto_set(ser.value()); + BEAST_EXPECT(parsed.has_value()); + BEAST_EXPECT( + api.float_compare(parsed.value(), xfl, compare_mode::EQUAL) + .value() == 1); + } + + void + test_float_sto_set(FeatureBitset features) + { + testcase("Test float_sto_set"); + + using namespace jtx; + using namespace hook_api; + using namespace hook; + + auto const alice = Account{"alice"}; + Env env{*this, features}; + STTx invokeTx = STTx(ttINVOKE, [&](STObject& obj) {}); + OpenView ov{*env.current()}; + ApplyContext applyCtx = createApplyContext(env, ov, invokeTx); + auto hookCtx = + makeStubHookContext(applyCtx, alice.id(), alice.id(), {}); + hook::HookAPI api(hookCtx); + + // Not enough bytes + BEAST_EXPECT( + api.float_sto_set(Bytes{1, 2, 3}).error() == NOT_AN_OBJECT); + + uint64_t const xfl = 6198187654261802496ULL; // 1234567.0 as in SetHook + auto const ser = api.float_sto(std::nullopt, std::nullopt, xfl, 0, 8); + BEAST_EXPECT(ser.has_value()); + auto const parsed = api.float_sto_set(ser.value()); + BEAST_EXPECT(parsed.has_value()); + BEAST_EXPECT( + api.float_compare(parsed.value(), xfl, compare_mode::EQUAL) + .value() == 1); + } + + void + test_float_sum(FeatureBitset features) + { + testcase("Test float_sum"); + + using namespace jtx; + using namespace hook_api; + + auto const alice = Account{"alice"}; + Env env{*this, features}; + STTx invokeTx = STTx(ttINVOKE, [&](STObject& obj) {}); + OpenView ov{*env.current()}; + ApplyContext applyCtx = createApplyContext(env, ov, invokeTx); + auto hookCtx = + makeStubHookContext(applyCtx, alice.id(), alice.id(), {}); + hook::HookAPI api(hookCtx); + + auto const one = api.float_one(); + auto const neg_one = api.float_negate(api.float_one()); + + // 1 + 1 = 2 + ASSERT_FLOAT_EQUAL( + api, 6090866696204910592LL, api.float_sum(one, one).value()); + + // 1 - 1 = 0 + ASSERT_FLOAT_EQUAL(api, 0, api.float_sum(one, neg_one).value()); + // 45678 + 0.345678 = 45678.345678 + ASSERT_FLOAT_EQUAL( + api, + 6165492124810638528LL, + api.float_sum(6165492090242838528LL, 6074309077695428608LL) + .value()); + // -151864512641 + 100000000000000000 = 99999848135487359 + ASSERT_FLOAT_EQUAL( + api, + 6387097057170171072LL, + api.float_sum(1676857706508234512LL, 6396111470866104320LL) + .value()); + + // auto generated random sums + // clang-format off + std::vector> tests = { + {7607324992379065667 /* 5.248821377668419e+84 */, 95785354843184473 /* -5.713362295774553e-77 */, 7607324992379065667 /* 5.248821377668419e+84 */}, + {1011203427860697296 /* -2.397111329706192e-26 */, 7715811566197737722 /* 5.64900413944857e+90 */, 7715811566197737722 /* 5.64900413944857e+90 */}, + {6507979072644559603 /* 4.781210721563379e+23 */, 422214339164556094 /* -7.883173446470462e-59 */, 6507979072644559603 /* 4.781210721563379e+23 */}, + {129493221419941559 /* -3.392431853567671e-75 */, 6742079437952459317 /* 4.694395406197301e+36 */, 6742079437952459317 /* 4.694395406197301e+36 */}, + {5172806703808250354 /* 2.674331586920946e-51 */, 3070396690523275533 /* -7.948943911338253e+88 */, 3070396690523275533 /* -7.948943911338253e+88 */}, + {2440992231195047997 /* -9.048432414980156e+53 */, 4937813945440933271 /* 1.868753842869655e-64 */, 2440992231195047996 /* -9.048432414980156e+53 */}, + {7351918685453062372 /* 2.0440935844129e+70 */, 6489541496844182832 /* 4.358033430668592e+22 */, 7351918685453062372 /* 2.0440935844129e+70 */}, + {4960621423606196948 /* 6.661833498651348e-63 */, 6036716382996689576 /* 0.001892882320224936 */, 6036716382996689576 /* 0.001892882320224936 */}, + {1342689232407435206 /* -9.62374270576839e-8 */, 5629833007898276923 /* 9.340672939897915e-26 */, 1342689232407435206 /* -9.62374270576839e-8 */}, + {7557687707019793516 /* 9.65473154684222e+81 */, 528084028396448719 /* -5.666471621471183e-53 */, 7557687707019793516 /* 9.65473154684222e+81 */}, + {130151633377050812 /* -4.050843810676924e-75 */, 2525286695563827336 /* -3.270904236349576e+58 */, 2525286695563827336 /* -3.270904236349576e+58 */}, + {5051914485221832639 /* 7.88290256687712e-58 */, 7518727241611221951 /* 6.723063157234623e+79 */, 7518727241611221951 /* 6.723063157234623e+79 */}, + {3014788764095798870 /* -6.384213012307542e+85 */, 7425019819707800346 /* 3.087633801222938e+74 */, 3014788764095767995 /* -6.384213012276667e+85 */}, + {4918950856932792129 /* 1.020063844210497e-65 */, 7173510242188034581 /* 3.779635414204949e+60 */, 7173510242188034581 /* 3.779635414204949e+60 */}, + {20028000442705357 /* -2.013601933223373e-81 */, 95248745393457140 /* -5.17675284604722e-77 */, 95248946753650462 /* -5.176954206240542e-77 */}, + {5516870225060928024 /* 4.46428115944092e-32 */, 7357202055584617194 /* 7.327463715967722e+70 */, 7357202055584617194 /* 7.327463715967722e+70 */}, + {2326103538819088036 /* -2.2461310959121e+47 */, 1749360946246242122 /* -1964290826489674 */, 2326103538819088036 /* -2.2461310959121e+47 */}, + {1738010758208819410 /* -862850129854894.6 */, 2224610859005732191 /* -8.83984233944816e+41 */, 2224610859005732192 /* -8.83984233944816e+41 */}, + {4869534730307487904 /* 5.647132747352224e-68 */, 2166841923565712115 /* -5.114102427874035e+38 */, 2166841923565712115 /* -5.114102427874035e+38 */}, + {1054339559322014937 /* -9.504445772059864e-24 */, 1389511416678371338 /* -0.0000240273144825857 */, 1389511416678371338 /* -0.0000240273144825857 */}, + }; + // clang-format on + + for (auto const& test : tests) + { + ASSERT_FLOAT_EQUAL( + api, + api.float_sum(std::get<0>(test), std::get<1>(test)).value(), + std::get<2>(test)); + } + } + + void + test_hook_account(FeatureBitset features) + { + testcase("Test hook_account"); + + using namespace jtx; + + auto const alice = Account{"alice"}; + Env env{*this, features}; + STTx invokeTx = STTx(ttINVOKE, [&](STObject& obj) {}); + OpenView ov{*env.current()}; + ApplyContext applyCtx = createApplyContext(env, ov, invokeTx); + StubHookContext stubCtx{}; + auto hookCtx = + makeStubHookContext(applyCtx, alice.id(), alice.id(), stubCtx); + hook::HookAPI api(hookCtx); + + BEAST_EXPECT(api.hook_account() == alice.id()); + } + + void + test_hook_again(FeatureBitset features) + { + testcase("Test hook_again"); + + using namespace jtx; + using namespace hook_api; + + auto const alice = Account{"alice"}; + Env env{*this, features}; + STTx invokeTx = STTx(ttINVOKE, [&](STObject& obj) {}); + OpenView ov{*env.current()}; + ApplyContext applyCtx = createApplyContext(env, ov, invokeTx); + + { + // Already requested + StubHookContext stubCtx{}; + stubCtx.result.executeAgainAsWeak = true; + auto hookCtx = + makeStubHookContext(applyCtx, alice.id(), alice.id(), stubCtx); + BEAST_EXPECT(hookCtx.result.executeAgainAsWeak); + hook::HookAPI api(hookCtx); + BEAST_EXPECT(api.hook_again().error() == ALREADY_SET); + } + { + // Strong hook requests weak re-exec + StubHookContext stubCtx{}; + stubCtx.result.isStrong = true; + auto hookCtx = + makeStubHookContext(applyCtx, alice.id(), alice.id(), stubCtx); + hook::HookAPI api(hookCtx); + BEAST_EXPECT(!hookCtx.result.executeAgainAsWeak); + auto const result = api.hook_again(); + BEAST_EXPECT(result.has_value()); + BEAST_EXPECT(result.value() == 1); + BEAST_EXPECT(hookCtx.result.executeAgainAsWeak); + } + + { + // Not strong -> prerequisite not met + auto hookCtx = + makeStubHookContext(applyCtx, alice.id(), alice.id(), {}); + BEAST_EXPECT(!hookCtx.result.executeAgainAsWeak); + hook::HookAPI api(hookCtx); + BEAST_EXPECT(api.hook_again().error() == PREREQUISITE_NOT_MET); + } + } + + void + test_hook_hash(FeatureBitset features) + { + testcase("Test hook_hash"); + + using namespace jtx; + using namespace hook_api; + + auto const alice = Account{"alice"}; + Env env{*this, features}; + env.fund(XRP(10000), alice); + env.close(); + + env(hook( + alice, + {{ + hso(genesis::AcceptHook), + hso(genesis::MintTestHook), + }}, + 0), + fee(XRP(100))); + env.close(); + + STTx invokeTx = STTx(ttINVOKE, [&](STObject& obj) {}); + OpenView ov{*env.current()}; + ApplyContext applyCtx = createApplyContext(env, ov, invokeTx); + uint256 const expectedHash{2}; + StubHookContext stubCtx{ + .result = {.hookHash = expectedHash}, + }; + auto hookCtx = + makeStubHookContext(applyCtx, alice.id(), alice.id(), stubCtx); + hook::HookAPI api(hookCtx); + + { + // current hook hash + auto const result = api.hook_hash(-1); + BEAST_EXPECT(result.has_value()); + BEAST_EXPECT(result.value() == expectedHash); + } + + { + // Does not exist + auto const result = api.hook_hash(2); + BEAST_EXPECT(!result.has_value()); + BEAST_EXPECT(result.error() == DOESNT_EXIST); + } + + { + // Success index = 0 + auto const wasm = genesis::AcceptHook; + auto const hash = + ripple::sha512Half_s(ripple::Slice(wasm.data(), wasm.size())); + auto const result = api.hook_hash(0); + BEAST_EXPECT(result.has_value()); + BEAST_EXPECT(result.value() == hash); + } + + { + // Success index = 1 + auto const wasm = genesis::MintTestHook; + auto const hash = + ripple::sha512Half_s(ripple::Slice(wasm.data(), wasm.size())); + auto const result = api.hook_hash(1); + BEAST_EXPECT(result.has_value()); + BEAST_EXPECT(result.value() == hash); + } + } + + void + test_hook_param(FeatureBitset features) + { + testcase("Test hook_param"); + + using namespace jtx; + using namespace hook_api; + using namespace hook; + + auto const alice = Account{"alice"}; + Env env{*this, features}; + STTx invokeTx = STTx(ttINVOKE, [&](STObject& obj) {}); + OpenView ov{*env.current()}; + ApplyContext applyCtx = createApplyContext(env, ov, invokeTx); + + { + // TOO_SMALL / TOO_BIG + auto hookCtx = + makeStubHookContext(applyCtx, alice.id(), alice.id(), {}); + hook::HookAPI api(hookCtx); + BEAST_EXPECT(api.hook_param({}).error() == TOO_SMALL); + BEAST_EXPECT(api.hook_param(Bytes(33, 1)).error() == TOO_BIG); + } + + // { + // // Hook params map + // Bytes const name{'k'}; + // Bytes const value{1, 2, 3}; + // StubHookContext stubCtx{ + // .result = { + // .hookParams = {{{name, value}}}, + // }}; + // auto hookCtx = + // makeStubHookContext(applyCtx, alice.id(), alice.id(), + // stubCtx); + // hook::HookAPI api(hookCtx); + // auto const result = api.hook_param(name); + // BEAST_EXPECT(result.has_value()); + // BEAST_EXPECT(result.value() == value); + // } + + { + // Override deletion wins + Bytes const name{'d'}; + StubHookContext stubCtx{}; + stubCtx.result.hookParamOverrides = { + {stubCtx.result.hookHash, {{name, Bytes{}}}}}; + auto hookCtx = + makeStubHookContext(applyCtx, alice.id(), alice.id(), stubCtx); + hook::HookAPI api(hookCtx); + BEAST_EXPECT(api.hook_param(name).error() == DOESNT_EXIST); + } + + { + // Override takes precedence + Bytes const name{'o'}; + Bytes const overrideValue{9}; + StubHookContext stubCtx{}; + stubCtx.result.hookParams = + std::map{{name, Bytes{1}}}; // base value + stubCtx.result.hookParamOverrides = { + {stubCtx.result.hookHash, {{name, overrideValue}}}}; + auto hookCtx = + makeStubHookContext(applyCtx, alice.id(), alice.id(), stubCtx); + hook::HookAPI api(hookCtx); + auto const result = api.hook_param(name); + BEAST_EXPECT(result.has_value()); + BEAST_EXPECT(result.value() == overrideValue); + } + } + + void + test_hook_param_set(FeatureBitset features) + { + testcase("Test hook_param_set"); + + using namespace jtx; + using namespace hook_api; + using namespace hook; + + auto const alice = Account{"alice"}; + Env env{*this, features}; + STTx invokeTx = STTx(ttINVOKE, [&](STObject& obj) {}); + OpenView ov{*env.current()}; + ApplyContext applyCtx = createApplyContext(env, ov, invokeTx); + auto hash = uint256{7}; + + { + // TOO_SMALL + auto hookCtx = + makeStubHookContext(applyCtx, alice.id(), alice.id(), {}); + hook::HookAPI api(hookCtx); + BEAST_EXPECT( + api.hook_param_set(hash, {}, Bytes{1}).error() == TOO_SMALL); + } + + { + // TOO_BIG key/value and TOO_MANY_PARAMS + StubHookContext stubCtx{}; + stubCtx.result.overrideCount = hook_api::max_params; + auto hookCtx = + makeStubHookContext(applyCtx, alice.id(), alice.id(), stubCtx); + hook::HookAPI api(hookCtx); + BEAST_EXPECT( + api.hook_param_set( + hash, + Bytes(hook::maxHookParameterKeySize() + 1, 1), + Bytes{}) + .error() == TOO_BIG); + BEAST_EXPECT( + api.hook_param_set( + hash, + Bytes{1}, + Bytes(hook::maxHookParameterValueSize() + 1, 1)) + .error() == TOO_BIG); + BEAST_EXPECT( + api.hook_param_set(hash, Bytes{1}, Bytes{1}).error() == + TOO_MANY_PARAMS); + } + + { + // SUCCESS and override stored + Bytes const name{'x'}; + Bytes const value{5, 6}; + auto hookCtx = + makeStubHookContext(applyCtx, alice.id(), alice.id(), {}); + hook::HookAPI api(hookCtx); + auto const result = api.hook_param_set(hash, name, value); + BEAST_EXPECT(result.has_value()); + BEAST_EXPECT(result.value() == value.size()); + BEAST_EXPECT(hookCtx.result.overrideCount == 1); + BEAST_EXPECT( + hookCtx.result.hookParamOverrides[hash].at(name) == value); + } + } + + void + test_hook_pos(FeatureBitset features) + { + testcase("Test hook_pos"); + + using namespace jtx; + + auto const alice = Account{"alice"}; + Env env{*this, features}; + STTx invokeTx = STTx(ttINVOKE, [&](STObject& obj) {}); + OpenView ov{*env.current()}; + ApplyContext applyCtx = createApplyContext(env, ov, invokeTx); + StubHookContext stubCtx{}; + stubCtx.result.hookChainPosition = 3; + auto hookCtx = + makeStubHookContext(applyCtx, alice.id(), alice.id(), stubCtx); + hook::HookAPI api(hookCtx); + + BEAST_EXPECT(api.hook_pos() == 3); + } + + void + test_hook_skip(FeatureBitset features) + { + testcase("Test hook_skip"); + + using namespace jtx; + using namespace hook_api; + + auto const alice = Account{"alice"}; + Env env{*this, features}; + env.fund(XRP(10000), alice); + env.close(); + env(hook(alice, {{hso(genesis::AcceptHook)}}, 0), fee(XRP(1))); + env.close(); + + STTx invokeTx = STTx(ttINVOKE, [&](STObject& obj) {}); + OpenView ov{*env.current()}; + ApplyContext applyCtx = createApplyContext(env, ov, invokeTx); + auto hookCtx = makeStubHookContext( + applyCtx, + alice.id(), + alice.id(), + { + .result = {.hookSkips = {uint256{123}, uint256{456}}}, + }); + hook::HookAPI api(hookCtx); + + { + // INVALID_ARGUMENT + auto const result = api.hook_skip(uint256{0}, 2); + BEAST_EXPECT(!result.has_value()); + BEAST_EXPECT(result.error() == INVALID_ARGUMENT); + } + + { + // DOESNT_EXIST + auto const result = api.hook_skip(uint256{1}, 0); + BEAST_EXPECT(!result.has_value()); + BEAST_EXPECT(result.error() == DOESNT_EXIST); + } + + { + // with Delete flag (1) + auto result = api.hook_skip(uint256{0}, 1); + BEAST_EXPECT(!result.has_value()); + BEAST_EXPECT(result.error() == DOESNT_EXIST); + BEAST_EXPECT(hookCtx.result.hookSkips.size() == 2); + + result = api.hook_skip(uint256{123}, 1); + BEAST_EXPECT(result.has_value()); + BEAST_EXPECT(result.value() == 1); + BEAST_EXPECT(hookCtx.result.hookSkips.size() == 1); + } + + { + // already skipped + auto result = api.hook_skip(uint256{456}, 0); + BEAST_EXPECT(result.has_value()); + BEAST_EXPECT(result.value() == 1); + BEAST_EXPECT(hookCtx.result.hookSkips.size() == 1); + } + + { + // doesn't found + auto result = api.hook_skip(uint256{123}, 0); + BEAST_EXPECT(!result.has_value()); + BEAST_EXPECT(result.error() == DOESNT_EXIST); + BEAST_EXPECT(hookCtx.result.hookSkips.size() == 1); + } + + { + // success + auto const wasm = genesis::AcceptHook; + auto const hash = + ripple::sha512Half_s(ripple::Slice(wasm.data(), wasm.size())); + auto result = api.hook_skip(hash, 0); + BEAST_EXPECT(result.has_value()); + BEAST_EXPECT(result.value() == 1); + BEAST_EXPECT(hookCtx.result.hookSkips.size() == 2); + BEAST_EXPECT( + hookCtx.result.hookSkips.find(hash) != + hookCtx.result.hookSkips.end()); + } + } + + void + test_ledger_keylet(FeatureBitset features) + { + testcase("Test ledger_keylet"); + + using namespace jtx; + using namespace hook_api; + + auto const alice = Account{"alice"}; + Env env{*this, features}; + env.fund(XRP(10000), alice); + env.close(); + env(cron::set(alice), cron::startTime(1000), fee(XRP(1))); + env.close(); + STTx invokeTx = STTx(ttINVOKE, [&](STObject& obj) {}); + OpenView ov{*env.current()}; + ApplyContext applyCtx = createApplyContext(env, ov, invokeTx); + auto hookCtx = + makeStubHookContext(applyCtx, alice.id(), alice.id(), {}); + hook::HookAPI api(hookCtx); + + { + // does not match + auto const result = api.ledger_keylet( + keylet::account(alice), keylet::ownerDir(alice)); + BEAST_EXPECT(!result.has_value()); + BEAST_EXPECT(result.error() == DOES_NOT_MATCH); + } + { + // does not exist + auto const result = + api.ledger_keylet(keylet::cron(100), keylet::cron(101)); + BEAST_EXPECT(!result.has_value()); + BEAST_EXPECT(result.error() == DOESNT_EXIST); + } + + // Success case + { + auto const lo = keylet::cron(1000); + auto const hi = keylet::cron(1001); + auto const result = api.ledger_keylet(lo, hi); + BEAST_EXPECT(result.has_value()); + auto const expected = keylet::cron(1000, alice); + BEAST_EXPECT(result.value().type == expected.type); + BEAST_EXPECT(result.value().key == expected.key); + } + } + + void + test_ledger_last_hash(FeatureBitset features) + { + testcase("Test ledger_last_hash"); + + using namespace jtx; + auto const alice = Account{"alice"}; + Env env{*this, features}; + STTx invokeTx = STTx(ttINVOKE, [&](STObject& obj) {}); + OpenView ov{*env.current()}; + ApplyContext applyCtx = createApplyContext(env, ov, invokeTx); + auto hookCtx = + makeStubHookContext(applyCtx, alice.id(), alice.id(), {}); + hook::HookAPI api(hookCtx); + + BEAST_EXPECT(api.ledger_last_hash() == ov.info().parentHash); + } + + void + test_ledger_last_time(FeatureBitset features) + { + testcase("Test ledger_last_time"); + + using namespace jtx; + auto const alice = Account{"alice"}; + Env env{*this, features}; + STTx invokeTx = STTx(ttINVOKE, [&](STObject& obj) {}); + OpenView ov{*env.current()}; + ApplyContext applyCtx = createApplyContext(env, ov, invokeTx); + auto hookCtx = + makeStubHookContext(applyCtx, alice.id(), alice.id(), {}); + hook::HookAPI api(hookCtx); + + auto const expected = + ov.info().parentCloseTime.time_since_epoch().count(); + BEAST_EXPECT(api.ledger_last_time() == expected); + } + + void + test_ledger_nonce(FeatureBitset features) + { + testcase("Test ledger_nonce"); + + using namespace jtx; + using namespace hook_api; + + auto const alice = Account{"alice"}; + Env env{*this, features}; + STTx invokeTx = STTx(ttINVOKE, [&](STObject& obj) {}); + OpenView ov{*env.current()}; + ApplyContext applyCtx = createApplyContext(env, ov, invokeTx); + + { + // TOO_MANY_NONCES + StubHookContext stubCtx{ + .ledger_nonce_counter = + static_cast(hook_api::max_nonce + 1)}; + auto hookCtx = + makeStubHookContext(applyCtx, alice.id(), alice.id(), stubCtx); + hook::HookAPI api(hookCtx); + auto const result = api.ledger_nonce(); + BEAST_EXPECT(result.error() == TOO_MANY_NONCES); + } + + { + // SUCCESS + auto hookCtx = + makeStubHookContext(applyCtx, alice.id(), alice.id(), {}); + hook::HookAPI api(hookCtx); + auto const info = hookCtx.applyCtx.view().info(); + auto const expected = ripple::sha512Half( + ripple::HashPrefix::hookNonce, + info.seq, + info.parentCloseTime.time_since_epoch().count(), + info.parentHash, + hookCtx.applyCtx.tx.getTransactionID(), + hookCtx.ledger_nonce_counter, + hookCtx.result.account); + auto const result = api.ledger_nonce(); + BEAST_EXPECT(result.has_value()); + BEAST_EXPECT(result.value() == expected); + BEAST_EXPECT(hookCtx.ledger_nonce_counter == 1); + } + } + + void + test_ledger_seq(FeatureBitset features) + { + testcase("Test ledger_seq"); + + using namespace jtx; + auto const alice = Account{"alice"}; + Env env{*this, features}; + STTx invokeTx = STTx(ttINVOKE, [&](STObject& obj) {}); + OpenView ov{*env.current()}; + ApplyContext applyCtx = createApplyContext(env, ov, invokeTx); + auto hookCtx = + makeStubHookContext(applyCtx, alice.id(), alice.id(), {}); + hook::HookAPI api(hookCtx); + + BEAST_EXPECT(api.ledger_seq() == ov.info().seq); + } + + void + test_meta_slot(FeatureBitset features) + { + testcase("Test meta_slot"); + + using namespace jtx; + using namespace hook_api; + auto const alice = Account{"alice"}; + Env env{*this, features}; + env.fund(XRP(10000), alice); + + env(noop(alice)); + env.close(); + + STTx invokeTx = STTx(ttINVOKE, [&](STObject& obj) {}); + OpenView ov{*env.current()}; + ApplyContext applyCtx = createApplyContext(env, ov, invokeTx); + auto hookCtx = makeStubHookContext( + applyCtx, + alice.id(), + alice.id(), + {.result = {.provisionalMeta = env.meta()}}); + hook::HookAPI api(hookCtx); + + { + auto hookCtx = + makeStubHookContext(applyCtx, alice.id(), alice.id(), {}); + hook::HookAPI api(hookCtx); + // prerequisite not met + auto const result = api.meta_slot(0); + BEAST_EXPECT(!result.has_value()); + BEAST_EXPECT(result.error() == PREREQUISITE_NOT_MET); + } + + { + // invalid argument + auto const result = api.meta_slot(hook_api::max_slots + 1); + BEAST_EXPECT(!result.has_value()); + BEAST_EXPECT(result.error() == INVALID_ARGUMENT); + } + + { + auto hookCtx = makeStubHookContext( + applyCtx, + alice.id(), + alice.id(), + {.result = {.provisionalMeta = env.meta()}}); + for (uint32_t i = 1; i <= hook_api::max_slots; ++i) + hookCtx.slot[i] = hook::SlotEntry{}; + // no free slots + hook::HookAPI api(hookCtx); + auto const result = api.meta_slot(0); + BEAST_EXPECT(!result.has_value()); + BEAST_EXPECT(result.error() == NO_FREE_SLOTS); + } + + { + hook::HookAPI api(hookCtx); + auto const result = api.meta_slot(0); + BEAST_EXPECT(result.has_value()); + BEAST_EXPECT(result.value() == 1); + + BEAST_EXPECT(*hookCtx.slot[1].entry == *env.meta()); + } + } + + void + test_xpop_slot(FeatureBitset features) + { + testcase("Test xpop_slot"); + + using namespace jtx; + using namespace hook_api; + auto const alice = Account{"alice"}; + + Env env{*this, features}; + + STTx invokeTx = STTx(ttIMPORT, [&](STObject& obj) {}); + OpenView ov{*env.current()}; + ApplyContext applyCtx = createApplyContext(env, ov, invokeTx); + auto hookCtx = + makeStubHookContext(applyCtx, alice.id(), alice.id(), {}); + hook::HookAPI api(hookCtx); + + { + STTx invokeTx = STTx(ttINVOKE, [&](STObject& obj) {}); + OpenView ov{*env.current()}; + ApplyContext applyCtx = createApplyContext(env, ov, invokeTx); + auto hookCtx = + makeStubHookContext(applyCtx, alice.id(), alice.id(), {}); + hook::HookAPI api(hookCtx); + // invalid transaction type + auto const result = api.xpop_slot(0, 0); + BEAST_EXPECT(!result.has_value()); + BEAST_EXPECT(result.error() == PREREQUISITE_NOT_MET); + } + + { + // invalid slot number + auto const result1 = api.xpop_slot(hook_api::max_slots + 1, 0); + BEAST_EXPECT(!result1.has_value()); + BEAST_EXPECT(result1.error() == INVALID_ARGUMENT); + + auto const result2 = api.xpop_slot(0, hook_api::max_slots + 1); + BEAST_EXPECT(!result2.has_value()); + BEAST_EXPECT(result2.error() == INVALID_ARGUMENT); + } + + { + // no free slots + for (uint32_t i = 1; i <= hook_api::max_slots - 1; ++i) + hookCtx.slot[i] = hook::SlotEntry{}; + hook::HookAPI api(hookCtx); + auto const result = api.xpop_slot(0, 0); + BEAST_EXPECT(!result.has_value()); + BEAST_EXPECT(result.error() == NO_FREE_SLOTS); + } + + { + // same slot number for both + auto const result = api.xpop_slot(1, 1); + BEAST_EXPECT(!result.has_value()); + BEAST_EXPECT(result.error() == INVALID_ARGUMENT); + } + + // TODO: test INVALID_TXN + + { + // Success + auto const xpopJson = import::loadXpop(ImportTCAccountSet::w_seed); + std::string xpopStr = Json::FastWriter().write(xpopJson); + STTx invokeTx = STTx(ttIMPORT, [&](STObject& obj) { + obj.setFieldVL(sfBlob, *strUnHex(strHex(xpopStr))); + }); + OpenView ov{*env.current()}; + ApplyContext applyCtx = createApplyContext(env, ov, invokeTx); + auto hookCtx = + makeStubHookContext(applyCtx, alice.id(), alice.id(), {}); + hook::HookAPI api(hookCtx); + auto const result = api.xpop_slot(0, 0); + BEAST_EXPECT(result.has_value()); + BEAST_EXPECT(result.value().first == 1); + BEAST_EXPECT(result.value().second == 2); + Serializer stx, smeta; + hookCtx.slot[1].entry->add(stx); + hookCtx.slot[2].entry->add(smeta); + stx.getData(); + smeta.getData(); + + std::string blob = strHex(stx.getData()); + std::string meta = strHex(smeta.getData()); + BEAST_EXPECT(xpopJson["transaction"]["blob"] == blob); + BEAST_EXPECT(xpopJson["transaction"]["meta"] == meta); + } + } + + void + test_otxn_burden(FeatureBitset features) + { + testcase("Test otxn_burden"); + using namespace jtx; + + auto const alice = Account{"alice"}; + Env env{*this, features}; + STTx baseTx = STTx(ttINVOKE, [&](STObject& obj) {}); + + { + // Cached burden + OpenView ov{*env.current()}; + ApplyContext applyCtx = createApplyContext(env, ov, baseTx); + StubHookContext stubCtx{.burden = 7}; + auto hookCtx = + makeStubHookContext(applyCtx, alice.id(), alice.id(), stubCtx); + hook::HookAPI api(hookCtx); + BEAST_EXPECT(api.otxn_burden() == 7); + } + + { + // No sfEmitDetails + OpenView ov{*env.current()}; + ApplyContext applyCtx = createApplyContext(env, ov, baseTx); + auto hookCtx = + makeStubHookContext(applyCtx, alice.id(), alice.id(), {}); + hook::HookAPI api(hookCtx); + BEAST_EXPECT(api.otxn_burden() == 1); + } + + { + // sfEmitDetails without sfEmitBurden + STTx tx = STTx(ttINVOKE, [&](STObject& obj) { + obj.peekFieldObject(sfEmitDetails); + }); + OpenView ov{*env.current()}; + ApplyContext applyCtx = createApplyContext(env, ov, tx); + auto hookCtx = + makeStubHookContext(applyCtx, alice.id(), alice.id(), {}); + hook::HookAPI api(hookCtx); + BEAST_EXPECT(api.otxn_burden() == 1); + } + + { + // sfEmitBurden with high bit set is masked + STTx tx = STTx(ttINVOKE, [&](STObject& obj) { + auto& details = obj.peekFieldObject(sfEmitDetails); + details.setFieldU64( + sfEmitBurden, (static_cast(1) << 63) | 25); + }); + OpenView ov{*env.current()}; + ApplyContext applyCtx = createApplyContext(env, ov, tx); + auto hookCtx = + makeStubHookContext(applyCtx, alice.id(), alice.id(), {}); + hook::HookAPI api(hookCtx); + auto const burden = api.otxn_burden(); + BEAST_EXPECT(burden == 25); + BEAST_EXPECT(api.otxn_burden() == burden); + } + } + + void + test_otxn_generation(FeatureBitset features) + { + testcase("Test otxn_generation"); + using namespace jtx; + + auto const alice = Account{"alice"}; + Env env{*this, features}; + STTx baseTx = STTx(ttINVOKE, [&](STObject& obj) {}); + + { + // Cached generation + OpenView ov{*env.current()}; + ApplyContext applyCtx = createApplyContext(env, ov, baseTx); + StubHookContext stubCtx{.generation = 9}; + auto hookCtx = + makeStubHookContext(applyCtx, alice.id(), alice.id(), stubCtx); + hook::HookAPI api(hookCtx); + BEAST_EXPECT(api.otxn_generation() == 9); + } + + { + // No sfEmitDetails + OpenView ov{*env.current()}; + ApplyContext applyCtx = createApplyContext(env, ov, baseTx); + auto hookCtx = + makeStubHookContext(applyCtx, alice.id(), alice.id(), {}); + hook::HookAPI api(hookCtx); + BEAST_EXPECT(api.otxn_generation() == 0); + } + + { + // sfEmitDetails without sfEmitGeneration + STTx tx = STTx(ttINVOKE, [&](STObject& obj) { + obj.peekFieldObject(sfEmitDetails); + }); + OpenView ov{*env.current()}; + ApplyContext applyCtx = createApplyContext(env, ov, tx); + auto hookCtx = + makeStubHookContext(applyCtx, alice.id(), alice.id(), {}); + hook::HookAPI api(hookCtx); + BEAST_EXPECT(api.otxn_generation() == 0); + } + + { + // sfEmitGeneration present + STTx tx = STTx(ttINVOKE, [&](STObject& obj) { + obj.peekFieldObject(sfEmitDetails) + .setFieldU32(sfEmitGeneration, 4); + }); + OpenView ov{*env.current()}; + ApplyContext applyCtx = createApplyContext(env, ov, tx); + auto hookCtx = + makeStubHookContext(applyCtx, alice.id(), alice.id(), {}); + hook::HookAPI api(hookCtx); + BEAST_EXPECT(api.otxn_generation() == 4); + BEAST_EXPECT(api.otxn_generation() == 4); + } + } + + void + test_otxn_field(FeatureBitset features) + { + testcase("Test otxn_field"); + + using namespace jtx; + using namespace hook_api; + + auto const alice = Account{"alice"}; + Env env{*this, features}; + STTx tx = + STTx(ttINVOKE, [&](STObject& obj) { obj[sfAccount] = alice.id(); }); + OpenView ov{*env.current()}; + ApplyContext applyCtx = createApplyContext(env, ov, tx); + auto hookCtx = + makeStubHookContext(applyCtx, alice.id(), alice.id(), {}); + hook::HookAPI api(hookCtx); + + // TODO: INVALID_FIELD now returns DOESNT_EXIST + // BEAST_EXPECT(api.otxn_field(0).error() == INVALID_FIELD); + BEAST_EXPECT( + api.otxn_field(sfDestination.getCode()).error() == DOESNT_EXIST); + auto const result = api.otxn_field(sfAccount.getCode()); + BEAST_EXPECT(result.has_value()); + auto const* acct = dynamic_cast(result.value()); + BEAST_EXPECT(acct != nullptr); + BEAST_EXPECT(acct->value() == alice.id()); + } + + void + test_otxn_id(FeatureBitset features) + { + testcase("Test otxn_id"); + + using namespace jtx; + + auto const alice = Account{"alice"}; + Env env{*this, features}; + + { + STTx tx = STTx(ttINVOKE, [&](STObject& obj) {}); + auto const txID = tx.getTransactionID(); + OpenView ov{*env.current()}; + ApplyContext applyCtx = createApplyContext(env, ov, tx); + // Originating transaction ID + auto hookCtx = + makeStubHookContext(applyCtx, alice.id(), alice.id(), {}); + hook::HookAPI api(hookCtx); + auto const result = api.otxn_id(0); + BEAST_EXPECT(result.has_value()); + BEAST_EXPECT(result.value() == txID); + } + + { + STTx tx = STTx(ttEMIT_FAILURE, [&](STObject& obj) { + obj.setFieldH256(sfTransactionHash, uint256{5}); + }); + STTx emitFailedTx = STTx(ttINVOKE, [&](STObject& obj) {}); + auto const txID = tx.getTransactionID(); + OpenView ov{*env.current()}; + ApplyContext applyCtx = createApplyContext(env, ov, tx); + // Emit failure transaction ID + StubHookContext stubCtx{.emitFailure = emitFailedTx}; + auto hookCtx = + makeStubHookContext(applyCtx, alice.id(), alice.id(), stubCtx); + hook::HookAPI api(hookCtx); + BEAST_EXPECT( + api.otxn_id(0).value() == tx.getFieldH256(sfTransactionHash)); + // flags bypass emitFailure + BEAST_EXPECT(api.otxn_id(1).value() == txID); + } + } + + void + test_otxn_slot(FeatureBitset features) + { + testcase("Test otxn_slot"); + + using namespace jtx; + using namespace hook_api; + + auto const alice = Account{"alice"}; + Env env{*this, features}; + STTx tx = STTx(ttINVOKE, [&](STObject& obj) {}); + OpenView ov{*env.current()}; + ApplyContext applyCtx = createApplyContext(env, ov, tx); + + { + // Invalid slot argument + auto hookCtx = + makeStubHookContext(applyCtx, alice.id(), alice.id(), {}); + hook::HookAPI api(hookCtx); + auto const result = api.otxn_slot(hook_api::max_slots + 1); + BEAST_EXPECT(result.error() == INVALID_ARGUMENT); + } + + { + // No free slots + StubHookContext stubCtx{}; + for (uint32_t i = 1; i <= hook_api::max_slots; ++i) + stubCtx.slot[i] = hook::SlotEntry{}; + auto hookCtx = + makeStubHookContext(applyCtx, alice.id(), alice.id(), stubCtx); + for (uint32_t i = 1; i <= hook_api::max_slots; ++i) + hookCtx.slot[i] = hook::SlotEntry{}; + BEAST_EXPECT(stubCtx.slot.size() == hook_api::max_slots); + hook::HookAPI api(hookCtx); + BEAST_EXPECT(api.otxn_slot(0).error() == NO_FREE_SLOTS); + } + + { + // SUCCESS allocate new slot (slot = 0) + auto hookCtx = + makeStubHookContext(applyCtx, alice.id(), alice.id(), {}); + for (uint32_t i = 1; i <= 111; ++i) + hookCtx.slot[i] = hook::SlotEntry{}; + BEAST_EXPECT(!hookCtx.slot.contains(112)); + hook::HookAPI api(hookCtx); + auto const result = api.otxn_slot(0); + BEAST_EXPECT(result.has_value()); + auto const newSlot = result.value(); + BEAST_EXPECT(newSlot == 112); + BEAST_EXPECT(hookCtx.slot.contains(112)); + BEAST_EXPECT(hookCtx.slot[112].entry != nullptr); + // TODO: test slot content + } + + { + // SUCCESS allocate new slot (slot != 0) + auto hookCtx = + makeStubHookContext(applyCtx, alice.id(), alice.id(), {}); + BEAST_EXPECT(!hookCtx.slot.contains(200)); + hook::HookAPI api(hookCtx); + auto const result = api.otxn_slot(200); + BEAST_EXPECT(result.has_value()); + auto const newSlot = result.value(); + BEAST_EXPECT(newSlot == 200); + BEAST_EXPECT(hookCtx.slot.contains(200)); + BEAST_EXPECT(hookCtx.slot[newSlot].entry != nullptr); + // TODO: test slot content + } + } + + void + test_otxn_type(FeatureBitset features) + { + testcase("Test otxn_type"); + + using namespace jtx; + + auto const alice = Account{"alice"}; + Env env{*this, features}; + STTx tx = STTx(ttINVOKE, [&](STObject& obj) {}); + OpenView ov{*env.current()}; + ApplyContext applyCtx = createApplyContext(env, ov, tx); + + { + STObject failure(sfHook); + failure.setFieldU16(sfTransactionType, ttACCOUNT_SET); + StubHookContext stubCtx{.emitFailure = failure}; + auto hookCtx = + makeStubHookContext(applyCtx, alice.id(), alice.id(), stubCtx); + hook::HookAPI api(hookCtx); + BEAST_EXPECT(api.otxn_type() == ttACCOUNT_SET); + } + + { + auto hookCtx = + makeStubHookContext(applyCtx, alice.id(), alice.id(), {}); + hook::HookAPI api(hookCtx); + BEAST_EXPECT(api.otxn_type() == ttINVOKE); + } + } + + void + test_otxn_param(FeatureBitset features) + { + testcase("Test otxn_param"); + + using namespace jtx; + using namespace hook_api; + using namespace hook; + + auto const alice = Account{"alice"}; + Env env{*this, features}; + + // Build tx with hook parameters + STTx tx = STTx(ttINVOKE, [&](STObject& obj) { + STArray params{sfHookParameters, 2}; + { + STObject p{sfHookParameter}; + p.setFieldVL(sfHookParameterName, Bytes{'a'}); + p.setFieldVL(sfHookParameterValue, Bytes{1, 2}); + params.emplace_back(std::move(p)); + } + { + STObject p{sfHookParameter}; + p.setFieldVL(sfHookParameterName, Bytes{'b'}); + // missing value to test DOESNT_EXIST + params.emplace_back(std::move(p)); + } + { + STObject p{sfHookParameter}; + p.setFieldVL(sfHookParameterName, Bytes{'c'}); + // empty value to test DOESNT_EXIST + p.setFieldVL(sfHookParameterValue, Bytes{}); + params.emplace_back(std::move(p)); + } + obj.setFieldArray(sfHookParameters, params); + }); + + OpenView ov{*env.current()}; + ApplyContext applyCtx = createApplyContext(env, ov, tx); + auto hookCtx = + makeStubHookContext(applyCtx, alice.id(), alice.id(), {}); + hook::HookAPI api(hookCtx); + + BEAST_EXPECT(api.otxn_param({}).error() == TOO_SMALL); + BEAST_EXPECT(api.otxn_param(Bytes(33, 1)).error() == TOO_BIG); + auto const result = api.otxn_param(Bytes{'a'}); + BEAST_EXPECT(result.has_value()); + BEAST_EXPECT(result.value() == Bytes({1, 2})); + BEAST_EXPECT(api.otxn_param(Bytes{'b'}).error() == DOESNT_EXIST); + BEAST_EXPECT(api.otxn_param(Bytes{'c'}).error() == DOESNT_EXIST); + BEAST_EXPECT(api.otxn_param(Bytes{'d'}).error() == DOESNT_EXIST); + } + + void + test_slot(FeatureBitset features) + { + testcase("Test slot"); + + using namespace jtx; + using namespace hook_api; + + auto const alice = Account{"alice"}; + Env env{*this, features}; + STTx invokeTx = STTx(ttINVOKE, [&](STObject& obj) {}); + OpenView ov{*env.current()}; + ApplyContext applyCtx = createApplyContext(env, ov, invokeTx); + + // Missing slot + { + auto hookCtx = + makeStubHookContext(applyCtx, alice.id(), alice.id(), {}); + hook::HookAPI api(hookCtx); + BEAST_EXPECT(api.slot(1).error() == DOESNT_EXIST); + } + + // Present slot pointing to an STAmount field + { + printf("test\n"); + STObject obj(sfGeneric); + obj.setFieldAmount(sfAmount, drops(1)); + auto storage = std::make_shared(obj); + StubHookContext stubCtx{}; + stubCtx.slot[1] = hook::SlotEntry{.storage = storage, .entry = 0}; + stubCtx.slot[1].entry = &(*stubCtx.slot[1].storage); + auto hookCtx = + makeStubHookContext(applyCtx, alice.id(), alice.id(), stubCtx); + hook::HookAPI api(hookCtx); + auto const result = api.slot(1); + BEAST_EXPECT(result.has_value()); + + Serializer s; + (*result)->add(s); + + STObject resultObj{s.slice(), sfGeneric}; + printf( + "resultObj: %s\n", + resultObj.getJson(JsonOptions::none).toStyledString().c_str()); + } + } + + void + test_slot_clear(FeatureBitset features) + { + testcase("Test slot_clear"); + + using namespace jtx; + using namespace hook_api; + + auto const alice = Account{"alice"}; + Env env{*this, features}; + STTx invokeTx = STTx(ttINVOKE, [&](STObject& obj) {}); + OpenView ov{*env.current()}; + ApplyContext applyCtx = createApplyContext(env, ov, invokeTx); + + // Missing slot + { + auto hookCtx = + makeStubHookContext(applyCtx, alice.id(), alice.id(), {}); + hook::HookAPI api(hookCtx); + BEAST_EXPECT(api.slot_clear(1).error() == DOESNT_EXIST); + } + + // Clear existing slot and push to free queue + { + STObject obj(sfGeneric); + auto storage = std::make_shared(obj); + StubHookContext stubCtx{}; + stubCtx.slot[2] = {.storage = storage, .entry = &(*storage)}; + auto hookCtx = + makeStubHookContext(applyCtx, alice.id(), alice.id(), stubCtx); + hook::HookAPI api(hookCtx); + BEAST_EXPECT(api.slot_clear(2).has_value()); + BEAST_EXPECT(hookCtx.slot.find(2) == hookCtx.slot.end()); + BEAST_EXPECT(!hookCtx.slot_free.empty()); + } + } + + void + test_slot_count(FeatureBitset features) + { + testcase("Test slot_count"); + + using namespace jtx; + using namespace hook_api; + + auto const alice = Account{"alice"}; + Env env{*this, features}; + STTx invokeTx = STTx(ttINVOKE, [&](STObject& obj) {}); + OpenView ov{*env.current()}; + ApplyContext applyCtx = createApplyContext(env, ov, invokeTx); + + // Nonexistent + { + auto hookCtx = + makeStubHookContext(applyCtx, alice.id(), alice.id(), {}); + hook::HookAPI api(hookCtx); + BEAST_EXPECT(api.slot_count(1).error() == DOESNT_EXIST); + } + + // Not an array + { + STObject obj(sfGeneric); + auto storage = std::make_shared(obj); + StubHookContext stubCtx{}; + stubCtx.slot[1] = {.storage = storage, .entry = &(*storage)}; + auto hookCtx = + makeStubHookContext(applyCtx, alice.id(), alice.id(), stubCtx); + hook::HookAPI api(hookCtx); + BEAST_EXPECT(api.slot_count(1).error() == NOT_AN_ARRAY); + } + + // Array count + { + STArray arr{sfGeneric}; + arr.emplace_back(STObject(sfGeneric)); + arr.emplace_back(STObject(sfGeneric)); + auto storage = std::make_shared(arr); + StubHookContext stubCtx{}; + stubCtx.slot[3] = { + .storage = + std::reinterpret_pointer_cast(storage), + .entry = &(*storage)}; + auto hookCtx = + makeStubHookContext(applyCtx, alice.id(), alice.id(), stubCtx); + hook::HookAPI api(hookCtx); + BEAST_EXPECT(api.slot_count(3).value() == 2); + } + } + + void + test_slot_float(FeatureBitset features) + { + testcase("Test slot_float"); + + using namespace jtx; + using namespace hook_api; + + auto const alice = Account{"alice"}; + Env env{*this, features}; + STTx invokeTx = STTx(ttINVOKE, [&](STObject& obj) {}); + OpenView ov{*env.current()}; + ApplyContext applyCtx = createApplyContext(env, ov, invokeTx); + + // Missing slot + { + auto hookCtx = + makeStubHookContext(applyCtx, alice.id(), alice.id(), {}); + hook::HookAPI api(hookCtx); + BEAST_EXPECT(api.slot_float(1).error() == DOESNT_EXIST); + } + + // Not an amount + { + STObject obj(sfGeneric); + auto storage = std::make_shared(obj); + StubHookContext stubCtx{}; + stubCtx.slot[1] = {.storage = storage, .entry = &(*storage)}; + auto hookCtx = + makeStubHookContext(applyCtx, alice.id(), alice.id(), stubCtx); + hook::HookAPI api(hookCtx); + BEAST_EXPECT(api.slot_float(1).error() == NOT_AN_AMOUNT); + } + + // Native amount + { + STObject obj(sfGeneric); + obj.setFieldAmount(sfAmount, drops(10)); + auto storage = std::make_shared(obj); + auto* amtPtr = &storage->getFieldAmount(sfAmount); + StubHookContext stubCtx{}; + stubCtx.slot[2] = {.storage = storage, .entry = amtPtr}; + auto hookCtx = + makeStubHookContext(applyCtx, alice.id(), alice.id(), stubCtx); + hook::HookAPI api(hookCtx); + auto const result = api.slot_float(2); + BEAST_EXPECT(result.has_value()); + BEAST_EXPECT(result.value() != 0); + } + } + + void + test_slot_set(FeatureBitset features) + { + testcase("Test slot_set"); + + using namespace jtx; + using namespace hook; + using namespace hook_api; + + auto const alice = Account{"alice"}; + Env env{*this, features}; + STTx invokeTx = STTx(ttINVOKE, [&](STObject& obj) {}); + OpenView ov{*env.current()}; + ApplyContext applyCtx = createApplyContext(env, ov, invokeTx); + auto hookCtx = + makeStubHookContext(applyCtx, alice.id(), alice.id(), {}); + hook::HookAPI api(hookCtx); + + // Invalid argument (wrong size) + BEAST_EXPECT( + api.slot_set(Bytes{1, 2, 3}, 0).error() == INVALID_ARGUMENT); + // Invalid argument (slot_no beyond max) + BEAST_EXPECT( + api.slot_set(Bytes(32, 0), hook_api::max_slots + 1).error() == + INVALID_ARGUMENT); + } + + void + test_slot_size(FeatureBitset features) + { + testcase("Test slot_size"); + + using namespace jtx; + using namespace hook_api; + + auto const alice = Account{"alice"}; + Env env{*this, features}; + STTx invokeTx = STTx(ttINVOKE, [&](STObject& obj) {}); + OpenView ov{*env.current()}; + ApplyContext applyCtx = createApplyContext(env, ov, invokeTx); + + // Missing slot + { + auto hookCtx = + makeStubHookContext(applyCtx, alice.id(), alice.id(), {}); + hook::HookAPI api(hookCtx); + BEAST_EXPECT(api.slot_size(1).error() == DOESNT_EXIST); + } + + // Size of STObject + { + STObject obj(sfGeneric); + obj.setFieldAmount(sfAmount, drops(1)); + auto storage = std::make_shared(obj); + StubHookContext stubCtx{}; + stubCtx.slot[1] = {.storage = storage, .entry = &(*storage)}; + auto hookCtx = + makeStubHookContext(applyCtx, alice.id(), alice.id(), stubCtx); + hook::HookAPI api(hookCtx); + BEAST_EXPECT(api.slot_size(1).value() > 0); + } + } + + void + test_slot_subarray(FeatureBitset features) + { + testcase("Test slot_subarray"); + + using namespace jtx; + using namespace hook_api; + + auto const alice = Account{"alice"}; + Env env{*this, features}; + STTx invokeTx = STTx(ttINVOKE, [&](STObject& obj) {}); + OpenView ov{*env.current()}; + ApplyContext applyCtx = createApplyContext(env, ov, invokeTx); + + // Missing parent + { + auto hookCtx = + makeStubHookContext(applyCtx, alice.id(), alice.id(), {}); + hook::HookAPI api(hookCtx); + BEAST_EXPECT(api.slot_subarray(1, 0, 0).error() == DOESNT_EXIST); + } + + // Valid subarray extraction + { + STArray arr{sfGeneric}; + arr.emplace_back(STObject(sfGeneric)); + arr.emplace_back(STObject(sfGeneric)); + auto storage = std::make_shared(arr); + StubHookContext stubCtx{}; + stubCtx.slot[1] = { + .storage = + std::reinterpret_pointer_cast(storage), + .entry = &(*storage)}; + auto hookCtx = + makeStubHookContext(applyCtx, alice.id(), alice.id(), stubCtx); + hook::HookAPI api(hookCtx); + auto const result = api.slot_subarray(1, 1, 0); + BEAST_EXPECT(result.has_value()); + BEAST_EXPECT(result.value() != 0); + BEAST_EXPECT(hookCtx.slot[result.value()].entry != nullptr); + } + } + + void + test_slot_subfield(FeatureBitset features) + { + testcase("Test slot_subfield"); + + using namespace jtx; + using namespace hook_api; + + auto const alice = Account{"alice"}; + Env env{*this, features}; + STTx invokeTx = STTx(ttINVOKE, [&](STObject& obj) {}); + OpenView ov{*env.current()}; + ApplyContext applyCtx = createApplyContext(env, ov, invokeTx); + + // Missing parent + { + auto hookCtx = + makeStubHookContext(applyCtx, alice.id(), alice.id(), {}); + hook::HookAPI api(hookCtx); + BEAST_EXPECT( + api.slot_subfield(1, sfAccount.getCode(), 0).error() == + DOESNT_EXIST); + } + + // No free slots + { + auto hookCtx = + makeStubHookContext(applyCtx, alice.id(), alice.id(), {}); + for (int i = 0; i < hook_api::max_slots; i++) + hookCtx.slot[i] = hook::SlotEntry{}; + hook::HookAPI api(hookCtx); + BEAST_EXPECT( + api.slot_subfield(1, sfAccount.getCode(), 0).error() == + NO_FREE_SLOTS); + BEAST_EXPECT( + api.slot_subfield( + 1, sfAccount.getCode(), hook_api::max_slots + 1) + .error() == INVALID_ARGUMENT); + } + + // Invalid field + { + STObject obj(sfGeneric); + auto storage = std::make_shared(obj); + StubHookContext stubCtx{}; + stubCtx.slot[1] = {.storage = storage, .entry = &(*storage)}; + auto hookCtx = + makeStubHookContext(applyCtx, alice.id(), alice.id(), stubCtx); + hook::HookAPI api(hookCtx); + BEAST_EXPECT( + api.slot_subfield(1, ((99U << 16U) + 99U), 0).error() == + INVALID_FIELD); + BEAST_EXPECT( + api.slot_subfield(1, sfAccount.getCode(), 0).error() == + DOESNT_EXIST); + } + + // Valid field + { + STObject obj(sfGeneric); + obj.setFieldAmount(sfAmount, drops(1)); + auto storage = std::make_shared(obj); + StubHookContext stubCtx{}; + stubCtx.slot[1] = {.storage = storage, .entry = &(*storage)}; + auto hookCtx = + makeStubHookContext(applyCtx, alice.id(), alice.id(), stubCtx); + hook::HookAPI api(hookCtx); + auto const result = api.slot_subfield(1, sfAmount.getCode(), 0); + BEAST_EXPECT(result.has_value()); + BEAST_EXPECT(result.value() != 0); + BEAST_EXPECT(hookCtx.slot[result.value()].entry != nullptr); + // test the amount bytes + } + } + + void + test_slot_type(FeatureBitset features) + { + testcase("Test slot_type"); + + using namespace jtx; + using namespace hook_api; + + auto const alice = Account{"alice"}; + Env env{*this, features}; + STTx invokeTx = STTx(ttINVOKE, [&](STObject& obj) {}); + OpenView ov{*env.current()}; + ApplyContext applyCtx = createApplyContext(env, ov, invokeTx); + + // Missing slot + { + auto hookCtx = + makeStubHookContext(applyCtx, alice.id(), alice.id(), {}); + hook::HookAPI api(hookCtx); + BEAST_EXPECT(api.slot_type(1, 0).error() == DOESNT_EXIST); + } + + // Generic object + { + STObject obj(sfGeneric); + obj.setFieldAmount(sfAmount, drops(5)); + auto storage = std::make_shared(obj); + StubHookContext stubCtx{}; + stubCtx.slot[1] = {.storage = storage, .entry = &(*storage)}; + auto hookCtx = + makeStubHookContext(applyCtx, alice.id(), alice.id(), stubCtx); + hook::HookAPI api(hookCtx); + auto const result = api.slot_type(1, 0); + BEAST_EXPECT(result.has_value()); + } + + // Amount with flag 1 + { + STObject obj(sfGeneric); + obj.setFieldAmount(sfAmount, drops(7)); + auto storage = std::make_shared(obj); + auto* amtPtr = &storage->getFieldAmount(sfAmount); + StubHookContext stubCtx{}; + stubCtx.slot[2] = {.storage = storage, .entry = amtPtr}; + auto hookCtx = + makeStubHookContext(applyCtx, alice.id(), alice.id(), stubCtx); + hook::HookAPI api(hookCtx); + auto const result = api.slot_type(2, 1); + BEAST_EXPECT(result.has_value()); + } + } + + void + test_state(FeatureBitset features) + { + testcase("Test state"); + + BEAST_EXPECT(true); + } + + void + test_state_foreign(FeatureBitset features) + { + testcase("Test state_foreign"); + + BEAST_EXPECT(true); + } + + void + test_state_foreign_set_max(FeatureBitset features) + { + testcase("Test state_foreign_set max"); + + BEAST_EXPECT(true); + } + + void + test_state_foreign_set(FeatureBitset features) + { + testcase("Test state_foreign_set"); + + BEAST_EXPECT(true); + } + + void + test_state_set(FeatureBitset features) + { + testcase("Test state_set"); + + BEAST_EXPECT(true); + } + + void + test_sto_emplace(FeatureBitset features) + { + testcase("Test sto_emplace"); + + using namespace jtx; + using namespace hook; + using namespace hook_api; + + auto const alice = Account{"alice"}; + Env env{*this, features}; + STTx invokeTx = STTx(ttINVOKE, [&](STObject& obj) {}); + OpenView ov{*env.current()}; + ApplyContext applyCtx = createApplyContext(env, ov, invokeTx); + auto hookCtx = + makeStubHookContext(applyCtx, alice.id(), alice.id(), {}); + hook::HookAPI api(hookCtx); + + { + // Invalid argument (wrong source_object size) + auto const small = Bytes(1, 0); + auto const large = Bytes(1024 * 16 + 1, 0); + + auto const small_result = + api.sto_emplace(small, std::nullopt, sfAccount.getCode()); + BEAST_EXPECT(!small_result.has_value()); + BEAST_EXPECT(small_result.error() == TOO_SMALL); + + auto const large_result = + api.sto_emplace(large, std::nullopt, sfAccount.getCode()); + BEAST_EXPECT(!large_result.has_value()); + BEAST_EXPECT(large_result.error() == TOO_BIG); + } + + { + // Invalid argument (wrong field_object size) + auto const source = Bytes(16, 0); + auto const small = Bytes(1, 0); + auto const large = Bytes(4096 + 1, 0); + + auto const small_result = + api.sto_emplace(source, small, sfAccount.getCode()); + BEAST_EXPECT(!small_result.has_value()); + BEAST_EXPECT(small_result.error() == TOO_SMALL); + + auto const large_result = + api.sto_emplace(source, large, sfAccount.getCode()); + BEAST_EXPECT(!large_result.has_value()); + BEAST_EXPECT(large_result.error() == TOO_BIG); + } + + { + // TODO: test parse error + } + + { + // Success + auto const source_object = + Bytes({0x81U, 0x14U, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}); + // Account: "" + auto const field_object = + Bytes({0x24U, 0x00U, 0x00U, 0x00U, 0x00U}); + // Sequence: 0 + + auto const result = api.sto_emplace( + source_object, field_object, sfSequence.getCode()); + BEAST_EXPECT(result.has_value()); + BEAST_EXPECT( + result.value().size() == + source_object.size() + field_object.size()); + BEAST_EXPECT( + result.value() == + Bytes({0x24U, 0x00U, 0x00U, 0x00U, 0x00U, 0x81U, 0x14U, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0})); + } + + auto const _source_object = + Bytes({0x81U, 0x14U, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}); + { + // use UINT16 + auto source_object = _source_object; + auto field_object = Bytes({0x10U, 0x10U, 0x00U, 0x01U}); + // Version: 1 + + auto const result = api.sto_emplace( + source_object, field_object, sfVersion.getCode()); + BEAST_EXPECT(result.has_value()); + BEAST_EXPECT( + result.value().size() == + source_object.size() + field_object.size()); + field_object.insert( + field_object.end(), source_object.begin(), source_object.end()); + BEAST_EXPECT(result.value() == field_object); + } + + { + // use UINT32 + auto source_object = _source_object; + auto field_object = Bytes({0x24U, 0x00U, 0x00U, 0x00U, 0x01U}); + // Sequence: 1 + + auto const result = api.sto_emplace( + source_object, field_object, sfSequence.getCode()); + BEAST_EXPECT(result.has_value()); + BEAST_EXPECT( + result.value().size() == + source_object.size() + field_object.size()); + field_object.insert( + field_object.end(), source_object.begin(), source_object.end()); + BEAST_EXPECT(result.value() == field_object); + } + + { + // use UINT64 + auto source_object = _source_object; + auto field_object = Bytes( + {0x36U, + 0x00U, + 0x00U, + 0x00U, + 0x00U, + 0x00U, + 0x00U, + 0x00U, + 0x01U}); + // ExchangeRate: 1 + + auto const result = api.sto_emplace( + source_object, field_object, sfExchangeRate.getCode()); + BEAST_EXPECT(result.has_value()); + BEAST_EXPECT( + result.value().size() == + source_object.size() + field_object.size()); + field_object.insert( + field_object.end(), source_object.begin(), source_object.end()); + BEAST_EXPECT(result.value() == field_object); + } + + { + // use UINT128 + auto source_object = _source_object; + auto field_object = Bytes( + {0x41U, + 0x00U, + 0x00U, + 0x00U, + 0x00U, + 0x00U, + 0x00U, + 0x00U, + 0x00U, + 0x00U, + 0x00U, + 0x00U, + 0x00U, + 0x00U, + 0x00U, + 0x00U, + 0x00U}); + // EmailHash: 1 + + auto const result = api.sto_emplace( + source_object, field_object, sfEmailHash.getCode()); + BEAST_EXPECT(result.has_value()); + BEAST_EXPECT( + result.value().size() == + source_object.size() + field_object.size()); + field_object.insert( + field_object.end(), source_object.begin(), source_object.end()); + BEAST_EXPECT(result.value() == field_object); + } + + { + // use UINT256 + auto source_object = _source_object; + auto field_object = Bytes( + {0x5EU, 0x00U, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}); + // ObjectID: + // "0000000000000000000000000000000000000000000000000000000000000000" + + auto const result = api.sto_emplace( + source_object, field_object, sfObjectID.getCode()); + BEAST_EXPECT(result.has_value()); + BEAST_EXPECT( + result.value().size() == + source_object.size() + field_object.size()); + field_object.insert( + field_object.end(), source_object.begin(), source_object.end()); + BEAST_EXPECT(result.value() == field_object); + } + + { + // use AMOUNT + auto source_object = _source_object; + + auto nativeamount = Bytes( + {0x61U, 0x99U, 0x99U, 0x99U, 0x99U, 0x99U, 0x99U, 0x99U, 0x99U, + 0x99U, 0x99U, 0x99U, 0x99U, 0x99U, 0x99U, 0x99U, 0x99U, 0x99U, + 0x99U, 0x99U, 0x99U, 0x99U, 0x99U, 0x99U, 0x99U, 0x99U, 0x99U, + 0x99U, 0x99U, 0x99U, 0x99U, 0x99U, 0x99U, 0x99U, 0x99U, 0x99U, + 0x99U, 0x99U, 0x99U, 0x99U, 0x99U, 0x99U, 0x99U, 0x99U, 0x99U, + 0x99U, 0x99U, 0x99U, 0x99U, 0x99U}); + auto iouamount = Bytes( + {0x61U, + 0x99U, + 0x99U, + 0x99U, + 0x99U, + 0x99U, + 0x99U, + 0x99U, + 0x99U}); + + for (auto field_object : {nativeamount, iouamount}) + { + auto const result = api.sto_emplace( + source_object, field_object, sfAmount.getCode()); + BEAST_EXPECT(result.has_value()); + BEAST_EXPECT( + result.value().size() == + source_object.size() + field_object.size()); + field_object.insert( + field_object.end(), + source_object.begin(), + source_object.end()); + BEAST_EXPECT(result.value() == field_object); + } + } + + { + // OBJECT + auto source_object = _source_object; + auto field_object = Bytes( + {0xE0U, + 0x5BU, + 0x61U, + 0x40U, + 0x00U, + 0x00U, + 0x00U, + 0x00U, + 0x00U, + 0x00U, + 0x64U, + 0xE1U}); + // {"AmountEntry": {"Amount": "100"}} + + auto const result = api.sto_emplace( + source_object, field_object, sfAmountEntry.getCode()); + BEAST_EXPECT(result.has_value()); + BEAST_EXPECT( + result.value().size() == + source_object.size() + field_object.size()); + source_object.insert( + source_object.end(), field_object.begin(), field_object.end()); + BEAST_EXPECT(result.value() == source_object); + } + + { + // ARRAY + auto source_object = _source_object; + auto field_object = Bytes( + {0xF9U, + 0xEAU, + 0x7DU, + 0x04U, + 0xDEU, + 0xADU, + 0xBEU, + 0xEFU, + 0xE1U, + 0xF1U}); + // {"Memos": [{"Memo":{ "MemoData": "DEADBEEF" }}]} + + auto const result = + api.sto_emplace(source_object, field_object, sfMemos.getCode()); + BEAST_EXPECT(result.has_value()); + BEAST_EXPECT( + result.value().size() == + source_object.size() + field_object.size()); + source_object.insert( + source_object.end(), field_object.begin(), field_object.end()); + BEAST_EXPECT(result.value() == source_object); + } + + { + // UINT8 + auto source_object = _source_object; + auto field_object = Bytes({0x00U, 0x10U, 0x10U, 0x01U}); + // {"TickSize": 1} + + auto const result = api.sto_emplace( + source_object, field_object, sfTickSize.getCode()); + BEAST_EXPECT(result.has_value()); + BEAST_EXPECT( + result.value().size() == + source_object.size() + field_object.size()); + source_object.insert( + source_object.end(), field_object.begin(), field_object.end()); + BEAST_EXPECT(result.value() == source_object); + } + + { + // UINT160 + auto source_object = _source_object; + auto field_object = + Bytes({0x01U, 0x11U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, + 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x55U, 0x53U, + 0x44U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U}); + // {"TakerPaysCurrency": "0000000000000000000000005553440000000000"} + + auto const result = api.sto_emplace( + source_object, field_object, sfTakerPaysCurrency.getCode()); + BEAST_EXPECT(result.has_value()); + BEAST_EXPECT( + result.value().size() == + source_object.size() + field_object.size()); + source_object.insert( + source_object.end(), field_object.begin(), field_object.end()); + BEAST_EXPECT(result.value() == source_object); + } + + { + // PATHSET + auto source_object = _source_object; + auto field_object = Bytes( + {0x01U, 0x12U, 0x30U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, + 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x55U, 0x53U, 0x44U, + 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x05U, 0x4FU, 0x6FU, 0x78U, + 0x4AU, 0x58U, 0xF9U, 0xEFU, 0xB0U, 0xA9U, 0xEBU, 0x90U, 0xB8U, + 0x34U, 0x64U, 0xF9U, 0xD1U, 0x66U, 0x46U, 0x19U, 0x00U}); + // {"Paths": [[{ "currency": "USD", "issuer": + // "rVnYNK9yuxBz4uP8zC8LEFokM2nqH3poc" }]]} + + auto const result = + api.sto_emplace(source_object, field_object, sfPaths.getCode()); + BEAST_EXPECT(result.has_value()); + BEAST_EXPECT( + result.value().size() == + source_object.size() + field_object.size()); + source_object.insert( + source_object.end(), field_object.begin(), field_object.end()); + BEAST_EXPECT(result.value() == source_object); + } + + { + // VECTOR256 + auto source_object = _source_object; + auto field_object = Bytes( + {0x03U, 0x13U, 0x20U, 0x42U, 0x42U, 0x6CU, 0x4DU, 0x4FU, 0x10U, + 0x09U, 0xEEU, 0x67U, 0x08U, 0x0AU, 0x9BU, 0x79U, 0x65U, 0xB4U, + 0x46U, 0x56U, 0xD7U, 0x71U, 0x4DU, 0x10U, 0x4AU, 0x72U, 0xF9U, + 0xB4U, 0x36U, 0x9FU, 0x97U, 0xABU, 0xF0U, 0x44U, 0xEEU}); + // {"Amendments":["42426C4D4F1009EE67080A9B7965B44656D7714D104A72F9B4369F97ABF044EE"]} + + auto const result = + api.sto_emplace(source_object, field_object, sfPaths.getCode()); + BEAST_EXPECT(result.has_value()); + BEAST_EXPECT( + result.value().size() == + source_object.size() + field_object.size()); + source_object.insert( + source_object.end(), field_object.begin(), field_object.end()); + BEAST_EXPECT(result.value() == source_object); + } + + { + // UINT96 + } + + { + // UINT384 + } + + { + // UINT512 + } + } + + void + test_sto_erase(FeatureBitset features) + { + testcase("Test sto_erase"); + + BEAST_EXPECT(true); + } + void + test_sto_subarray(FeatureBitset features) + { + testcase("Test sto_subarray"); + + using namespace jtx; + using namespace hook; + using namespace hook_api; + + auto const alice = Account{"alice"}; + Env env{*this, features}; + STTx invokeTx = STTx(ttINVOKE, [&](STObject& obj) {}); + OpenView ov{*env.current()}; + ApplyContext applyCtx = createApplyContext(env, ov, invokeTx); + auto hookCtx = + makeStubHookContext(applyCtx, alice.id(), alice.id(), {}); + hook::HookAPI api(hookCtx); + + { + // Invalid data size + BEAST_EXPECT(api.sto_subarray(Bytes{}, 0).error() == TOO_SMALL); + BEAST_EXPECT(api.sto_subarray(Bytes{0x00}, 0).error() == TOO_SMALL); + } + + { + // Invalid: wrapped but size = 0 ([]) + // { Memos: [] } + BEAST_EXPECT( + api.sto_subarray(Bytes{0xF9, 0xF1}, 0).error() == PARSE_ERROR); + // { Amounts: [] } + BEAST_EXPECT( + api.sto_subarray(Bytes{0xF0, 0x5C, 0xF1}, 0).error() == + PARSE_ERROR); + } + + { + // doesn't found + // { Memos: [{Memo: {MemoData: "BEEF"}}] } + auto const memos = *strUnHex("F9EA7D02BEEFE1F1"); + BEAST_EXPECT(api.sto_subarray(memos, 2).error() == DOESNT_EXIST); + // { Amounts: [{AmountEntry: {Amount: "100"}}] } + auto const amounts = *strUnHex("F05CE05B614000000000000064E1F1"); + // TODO: fix this + // BEAST_EXPECT(api.sto_subarray(amounts, 2).error() == + // DOESNT_EXIST); + } + + { + // success + // { Memos: [{Memo: {MemoData: "BEEF"}}] } + auto const memos = *strUnHex("F9EA7D02BEEFE1F1"); + BEAST_EXPECT( + api.sto_subarray(memos, 0).value() == std::make_pair(1, 6)); + // { Amounts: [{AmountEntry: {Amount: "100"}}] } + auto const amounts = *strUnHex("F05CE05B614000000000000064E1F1"); + // TODO: fix this + // BEAST_EXPECT( + // api.sto_subarray(amounts, 0).value() == std::make_pair(2, + // 12)); + } + } + + void + test_sto_subfield(FeatureBitset features) + { + testcase("Test sto_subfield"); + + using namespace jtx; + using namespace hook; + using namespace hook_api; + + auto const alice = Account{"alice"}; + Env env{*this, features}; + STTx invokeTx = STTx(ttINVOKE, [&](STObject& obj) {}); + OpenView ov{*env.current()}; + ApplyContext applyCtx = createApplyContext(env, ov, invokeTx); + auto hookCtx = + makeStubHookContext(applyCtx, alice.id(), alice.id(), {}); + hook::HookAPI api(hookCtx); + + { + // Invalid data size + BEAST_EXPECT(api.sto_subfield(Bytes{}, 0).error() == TOO_SMALL); + BEAST_EXPECT(api.sto_subfield(Bytes{0x00}, 0).error() == TOO_SMALL); + } + + { + // Invalid data + BEAST_EXPECT( + api.sto_subfield(Bytes{0xFF, 0xFF, 0xFF, 0xFF}, 0).error() == + PARSE_ERROR); + } + + { + // doesn't found + // { Memo: {MemoData: "BEEF"} } + auto const memos = *strUnHex("EA7D02BEEFE1"); + BEAST_EXPECT( + api.sto_subfield(memos, sfMemoData.getCode()).error() == + DOESNT_EXIST); + // { AmountEntry: {Amount: "100"} } + auto const amounts = *strUnHex("E05B614000000000000064E1"); + BEAST_EXPECT( + api.sto_subfield(amounts, sfAmount.getCode()).error() == + DOESNT_EXIST); + } + + { + // success + // { Memo: {MemoData: "BEEF"} } + auto const memos = *strUnHex("EA7D02BEEFE1"); + BEAST_EXPECT( + api.sto_subfield(memos, sfMemo.getCode()).value() == + std::make_pair(1, 4)); + // { AmountEntry: {Amount: "100"} } + auto const amounts = *strUnHex("E05B614000000000000064E1"); + BEAST_EXPECT( + api.sto_subfield(amounts, sfAmountEntry.getCode()).value() == + std::make_pair(2, 9)); + } + } + + void + test_sto_validate(FeatureBitset features) + { + testcase("Test sto_validate"); + + using namespace jtx; + using namespace hook; + using namespace hook_api; + + auto const alice = Account{"alice"}; + Env env{*this, features}; + STTx invokeTx = STTx(ttINVOKE, [&](STObject& obj) {}); + OpenView ov{*env.current()}; + ApplyContext applyCtx = createApplyContext(env, ov, invokeTx); + auto hookCtx = + makeStubHookContext(applyCtx, alice.id(), alice.id(), {}); + hook::HookAPI api(hookCtx); + + BEAST_EXPECT(api.sto_validate(Bytes{}).error() == TOO_SMALL); + BEAST_EXPECT(api.sto_validate(Bytes{0x00}).error() == TOO_SMALL); + + // { Memo: {MemoData: "BEEF"} } + auto const memos = *strUnHex("EA7D02BEEFE1"); + // { AmountEntry: {Amount: "100"} } + auto const amounts = *strUnHex("E05B614000000000000064E1"); + // { Memo: {MemoData: "BEEF"} } + auto const memo = *strUnHex("EA7D02BEEFE1"); + // { AmountEntry: {Amount: "100"} } + auto const amountEntry = *strUnHex("E05B614000000000000064E1"); + + BEAST_EXPECT(api.sto_validate(memos).value() == true); + BEAST_EXPECT(api.sto_validate(amounts).value() == true); + BEAST_EXPECT(api.sto_validate(memo).value() == true); + BEAST_EXPECT(api.sto_validate(amountEntry).value() == true); + + // Invalid data + BEAST_EXPECT( + api.sto_validate(Bytes{0xFF, 0xFF, 0xFF, 0xFF}).value() == false); + + Bytes const i_memos(&memos[0], &memos[memos.size() - 1]); + Bytes const i_amounts(&amounts[0], &amounts[amounts.size() - 1]); + Bytes const i_memo(&memo[0], &memo[memo.size() - 1]); + Bytes const i_amountEntry( + &amountEntry[0], &amountEntry[amountEntry.size() - 1]); + BEAST_EXPECT(api.sto_validate(i_memos).value() == false); + BEAST_EXPECT(api.sto_validate(i_amounts).value() == false); + BEAST_EXPECT(api.sto_validate(i_memo).value() == false); + BEAST_EXPECT(api.sto_validate(i_amountEntry).value() == false); + + Bytes const i2_memos(&memos[1], &memos[memos.size()]); + Bytes const i2_amounts(&amounts[1], &amounts[amounts.size()]); + Bytes const i2_memo(&memo[1], &memo[memo.size()]); + Bytes const i2_amountEntry( + &amountEntry[1], &amountEntry[amountEntry.size()]); + BEAST_EXPECT(api.sto_validate(i_memos).value() == false); + BEAST_EXPECT(api.sto_validate(i_amounts).value() == false); + BEAST_EXPECT(api.sto_validate(i_memo).value() == false); + BEAST_EXPECT(api.sto_validate(i_amountEntry).value() == false); + } + + void + test_trace(FeatureBitset features) + { + testcase("Test trace"); + + BEAST_EXPECT(true); + } + + void + test_trace_float(FeatureBitset features) + { + testcase("Test trace_float"); + + BEAST_EXPECT(true); + } + + void + test_trace_num(FeatureBitset features) + { + testcase("Test trace_num"); + + BEAST_EXPECT(true); + } + + void + test_util_accid(FeatureBitset features) + { + testcase("Test util_accid"); + + using namespace jtx; + using namespace hook; + using namespace hook_api; + + auto const alice = Account{"alice"}; + Env env{*this, features}; + STTx invokeTx = STTx(ttINVOKE, [&](STObject& obj) {}); + OpenView ov{*env.current()}; + ApplyContext applyCtx = createApplyContext(env, ov, invokeTx); + auto hookCtx = + makeStubHookContext(applyCtx, alice.id(), alice.id(), {}); + hook::HookAPI api(hookCtx); + + // Invalid base58 string + BEAST_EXPECT(api.util_accid("invalid").error() == INVALID_ARGUMENT); + + // Valid r-address round-trip via util_raddr + auto accid = api.util_accid(alice.human()); + BEAST_EXPECT(accid.has_value()); + auto aliceid = alice.id(); + BEAST_EXPECT(accid.value() == Bytes(aliceid.begin(), aliceid.end())); + } + + void + test_util_keylet(FeatureBitset features) + { + testcase("Test util_keylet"); + + // TODO + BEAST_EXPECT(true); + } + + void + test_util_raddr(FeatureBitset features) + { + testcase("Test util_raddr"); + + using namespace jtx; + using namespace hook; + using namespace hook_api; + + auto const alice = Account{"alice"}; + Env env{*this, features}; + STTx invokeTx = STTx(ttINVOKE, [&](STObject& obj) {}); + OpenView ov{*env.current()}; + ApplyContext applyCtx = createApplyContext(env, ov, invokeTx); + auto hookCtx = + makeStubHookContext(applyCtx, alice.id(), alice.id(), {}); + hook::HookAPI api(hookCtx); + + // Wrong size + BEAST_EXPECT(api.util_raddr(Bytes(10, 0)).error() == INVALID_ARGUMENT); + + // Valid accountID + auto aliceid = alice.id(); + auto id = Bytes(aliceid.begin(), aliceid.end()); + auto addr = api.util_raddr(id); + BEAST_EXPECT(addr.has_value()); + BEAST_EXPECT(addr.value() == alice.human()); + } + + void + test_util_sha512h(FeatureBitset features) + { + testcase("Test util_sha512h"); + + using namespace jtx; + using namespace hook_api; + + auto const alice = Account{"alice"}; + Env env{*this, features}; + STTx invokeTx = STTx(ttINVOKE, [&](STObject& obj) {}); + OpenView ov{*env.current()}; + ApplyContext applyCtx = createApplyContext(env, ov, invokeTx); + auto hookCtx = + makeStubHookContext(applyCtx, alice.id(), alice.id(), {}); + hook::HookAPI api(hookCtx); + + std::string msg{"hello"}; + auto hash = api.util_sha512h(Slice(msg.data(), msg.size())); + auto expected = ripple::sha512Half(Slice(msg.data(), msg.size())); + BEAST_EXPECT(hash == expected); + } + + void + test_util_verify(FeatureBitset features) + { + testcase("Test util_verify"); + + using namespace jtx; + using namespace hook; + using namespace hook_api; + + auto const alice = Account{"alice"}; + Env env{*this, features}; + STTx invokeTx = STTx(ttINVOKE, [&](STObject& obj) {}); + OpenView ov{*env.current()}; + ApplyContext applyCtx = createApplyContext(env, ov, invokeTx); + auto hookCtx = + makeStubHookContext(applyCtx, alice.id(), alice.id(), {}); + hook::HookAPI api(hookCtx); + + // Generate keypair and signature + KeyType type = KeyType::secp256k1; + auto kp = generateKeyPair(type, generateSeed("util_verify_test")); + Serializer s; + s.add32(42); + auto const msg = s.slice(); + auto sig = ripple::sign(kp.first, kp.second, msg); + + // Invalid key size + BEAST_EXPECT( + api.util_verify(msg, sig, kp.first.slice().substr(0, 32)).error() == + INVALID_KEY); + + // Invalid data size + BEAST_EXPECT( + api.util_verify(Slice{}, sig, kp.first.slice()).error() == + TOO_SMALL); + + // Invalid sig size + BEAST_EXPECT( + api.util_verify(msg, Slice(sig.data(), 29), kp.first.slice()) + .error() == TOO_SMALL); + + // Invalid sig type + auto const& invalidKey = kp.first.slice().substr(1, 34); + BEAST_EXPECT( + api.util_verify(msg, sig, invalidKey).error() == INVALID_KEY); + return; + + // Success + auto const ok = api.util_verify(msg, sig, kp.first.slice()); + BEAST_EXPECT(ok.has_value()); + BEAST_EXPECT(ok.value()); + } + + void + testWithFeatures(FeatureBitset features) + { + using namespace test::jtx; + test_accept(features); + test_rollback(features); + testGuards(features); + + test_emit(features); + test_etxn_burden(features); + test_etxn_generation(features); + test_otxn_burden(features); + test_otxn_generation(features); + test_etxn_details(features); + test_etxn_fee_base(features - fixHookAPI20251128); + test_etxn_fee_base(features); + test_etxn_nonce(features); + test_etxn_reserve(features); + test_fee_base(features); + + test_otxn_field(features); + + test_ledger_keylet(features); + + test_float_compare(features); + test_float_divide(features); + test_float_int(features); + test_float_invert(features); + test_float_log(features); + test_float_mantissa(features); + test_float_mulratio(features); + test_float_multiply(features); + test_float_negate(features); + test_float_one(features); + test_float_root(features); + test_float_set(features); + test_float_sign(features); + test_float_sto(features); + test_float_sto_set(features); + test_float_sum(features); + + test_hook_account(features); + test_hook_again(features); + test_hook_hash(features); + test_hook_param(features); + test_hook_param_set(features); + test_hook_pos(features); + test_hook_skip(features); + + test_ledger_last_hash(features); + test_ledger_last_time(features); + test_ledger_nonce(features); + test_ledger_seq(features); + + test_meta_slot(features); + test_xpop_slot(features); + + test_otxn_id(features); + test_otxn_slot(features); + test_otxn_type(features); + test_otxn_param(features); + + test_slot(features); + test_slot_clear(features); + test_slot_count(features); + test_slot_float(features); + test_slot_set(features); + test_slot_size(features); + test_slot_subarray(features); + test_slot_subfield(features); + test_slot_type(features); + + test_state(features); + test_state_foreign(features); + test_state_foreign_set(features); + test_state_foreign_set_max(features); + test_state_set(features); + + test_sto_emplace(features); + test_sto_erase(features); + test_sto_subarray(features); + test_sto_subfield(features); + test_sto_validate(features); + + test_trace(features); + test_trace_float(features); + test_trace_num(features); + + test_util_accid(features); + test_util_keylet(features); + test_util_raddr(features); + test_util_sha512h(features); + test_util_verify(features); + } + +public: + void + run() override + { + using namespace test::jtx; + testWithFeatures(supported_amendments()); + } +}; + +BEAST_DEFINE_TESTSUITE_PRIO(HookAPI, app, ripple, 2); +} // namespace test +} // namespace ripple +#undef M diff --git a/src/test/app/SetHook_test.cpp b/src/test/app/SetHook_test.cpp index 977f5a531..0c7be29e8 100644 --- a/src/test/app/SetHook_test.cpp +++ b/src/test/app/SetHook_test.cpp @@ -3195,7 +3195,7 @@ public: { auto f = features; if (!hasFix) - f = f - fixEtxnFeeBase; + f = f - fixHookAPI20251128; Env env{*this, f}; @@ -10392,11 +10392,11 @@ public: } )[test.hook]"]; - for (auto f : {features, features - fixStoEmplaceFieldIdCheck}) + for (auto f : {features, features - fixHookAPI20251128}) { Env env{*this, f}; bool const hasFix = - env.current()->rules().enabled(fixStoEmplaceFieldIdCheck); + env.current()->rules().enabled(fixHookAPI20251128); env.fund(XRP(10000), alice); env.fund(XRP(10000), bob); @@ -10676,12 +10676,12 @@ public: } )[test.hook]"]; - for (auto isFixStoSubarray : {true, false}) + for (auto isfixHookAPI20251128 : {true, false}) { Env env{ *this, - isFixStoSubarray ? features | fixStoSubarray - : features - fixStoSubarray}; + isfixHookAPI20251128 ? features | fixHookAPI20251128 + : features - fixHookAPI20251128}; env.fund(XRP(10000), alice, bob); env.close(); @@ -10708,7 +10708,7 @@ public: BEAST_REQUIRE(hookExecution.isFieldPresent(sfHookReturnCode)); auto const returnCode = hookExecution.getFieldU64(sfHookReturnCode); - if (isFixStoSubarray) + if (isfixHookAPI20251128) { auto const doesntExistError = -5; auto const position = 2; diff --git a/src/test/app/Touch_test.cpp b/src/test/app/Touch_test.cpp index 9f1906f8e..b5df8d2b9 100644 --- a/src/test/app/Touch_test.cpp +++ b/src/test/app/Touch_test.cpp @@ -1401,8 +1401,8 @@ public: using namespace test::jtx; auto const sa = supported_amendments(); testAllTxns(sa); - testAllTxns(sa - fixEtxnFeeBase); - testAllTxns(sa - featureTouch - fixEtxnFeeBase); + testAllTxns(sa - fixHookAPI20251128); + testAllTxns(sa - featureTouch - fixHookAPI20251128); } }; diff --git a/src/test/app/TxQ_test.cpp b/src/test/app/TxQ_test.cpp index fad08fa18..a849418d3 100644 --- a/src/test/app/TxQ_test.cpp +++ b/src/test/app/TxQ_test.cpp @@ -5067,7 +5067,8 @@ public: testMultiTxnPerAccount(all); // fragile: hardcoded ordering by txID XOR parentHash // parentHash < txTree Hash < txMeta < PreviousTxnID - testTieBreaking(all - fixProvisionalDoubleThreading - fixEtxnFeeBase); + testTieBreaking( + all - fixProvisionalDoubleThreading - fixHookAPI20251128); testAcctTxnID(all); testMaximum(all); testUnexpectedBalanceChange(all); @@ -5088,7 +5089,7 @@ public: // fragile: hardcoded ordering by txID XOR parentHash // parentHash < txTree Hash < txMeta < PreviousTxnID testFullQueueGapFill( - all - fixProvisionalDoubleThreading - fixEtxnFeeBase); + all - fixProvisionalDoubleThreading - fixHookAPI20251128); testSignAndSubmitSequence(all); testAccountInfo(all); testServerInfo(all); diff --git a/src/test/rpc/LedgerRPC_test.cpp b/src/test/rpc/LedgerRPC_test.cpp index 3f298633f..0f4b1c823 100644 --- a/src/test/rpc/LedgerRPC_test.cpp +++ b/src/test/rpc/LedgerRPC_test.cpp @@ -292,7 +292,7 @@ public: using namespace test::jtx; Env env{ *this, - supported_amendments() - featureXahauGenesis - fixEtxnFeeBase}; + supported_amendments() - featureXahauGenesis - fixHookAPI20251128}; Account const alice{"alice"}; env.fund(XRP(10000), alice); env.close(); @@ -2173,7 +2173,7 @@ public: return cfg; }), supported_amendments() - featureXahauGenesis - - fixProvisionalDoubleThreading - fixEtxnFeeBase}; + fixProvisionalDoubleThreading - fixHookAPI20251128}; Json::Value jv; jv[jss::ledger_index] = "current";