//------------------------------------------------------------------------------ /* 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 #include #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_prepare(FeatureBitset features) { testcase("Test prepare"); 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) {}); { // PREREQUISITE_NOT_MET auto hookCtx = makeStubHookContext(applyCtx, alice.id(), alice.id(), {}); auto& api = hookCtx.api(); auto tx = emitInvokeTx; Serializer s = tx.getSerializer(); s.add8(0); // invalid value auto const result = api.prepare(s.slice()); BEAST_EXPECT(result.error() == PREREQUISITE_NOT_MET); } { // Invalid txn auto hookCtx = makeStubHookContext( applyCtx, alice.id(), alice.id(), { .expected_etxn_count = 1, }); auto& api = hookCtx.api(); auto tx = emitInvokeTx; Serializer s = tx.getSerializer(); s.add8(0); // invalid value auto const result = api.prepare(s.slice()); BEAST_EXPECT(result.error() == INVALID_ARGUMENT); } { // success auto hookCtx = makeStubHookContext( applyCtx, alice.id(), alice.id(), { .expected_etxn_count = 1, }); auto& api = hookCtx.api(); auto tx = emitInvokeTx; Serializer s = tx.getSerializer(); auto const result = api.prepare(s.slice()); BEAST_EXPECT(result.has_value()); SerialIter sit(Slice(result.value().data(), result.value().size())); STObject st(sit, sfGeneric); BEAST_EXPECT(st.getFieldAmount(sfFee) > XRPAmount(0)); BEAST_EXPECT(st.getFieldU32(sfSequence) == 0); BEAST_EXPECT(st.getAccountID(sfAccount) == alice.id()); auto const seq = applyCtx.view().info().seq; BEAST_EXPECT(st.getFieldU32(sfFirstLedgerSequence) == seq + 1); BEAST_EXPECT(st.getFieldU32(sfLastLedgerSequence) == seq + 5); BEAST_EXPECT(st.isFieldPresent(sfEmitDetails)); auto const result2 = api.emit(Slice(result.value().data(), result.value().size())); BEAST_EXPECT(result2.has_value()); } } 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] = Slice{}; 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] = Slice{}; 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}); auto& api = hookCtx.api(); 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}, }); auto& api = hookCtx.api(); 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}}, }); auto& api = hookCtx.api(); 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}}, }); auto& api = hookCtx.api(); 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], }}); auto& api = hookCtx.api(); 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()}, }); auto& api = hookCtx.api(); 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]}, }); auto& api = hookCtx.api(); 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()}, }); auto& api = hookCtx.api(); { // 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(), {}); auto& api = hookCtx.api(); 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); auto& api = hookCtx.api(); 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); auto& api = hookCtx.api(); 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); auto& api = hookCtx.api(); 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()}, }); auto& api = hookCtx.api(); // 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(), {}); auto& api = hookCtx.api(); 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); auto& api = hookCtx.api(); 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); auto& api = hookCtx.api(); 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); auto& api = hookCtx.api(); 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(), {}); auto& api = hookCtx.api(); 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(), {}); auto& api = hookCtx.api(); 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; auto& api = hookCtx.api(); auto const result = api.etxn_nonce(); BEAST_EXPECT(result.error() == TOO_MANY_NONCES); } // SUCCESS { hookCtx.emit_nonce_counter = hook_api::max_nonce; auto& api = hookCtx.api(); 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); auto& api = hookCtx.api(); auto const result = api.etxn_reserve(2); BEAST_EXPECT(result.error() == ALREADY_SET); } { // TOO_SMALL auto hookCtx = makeStubHookContext(applyCtx, alice.id(), alice.id(), {}); auto& api = hookCtx.api(); auto const result = api.etxn_reserve(0); BEAST_EXPECT(result.error() == TOO_SMALL); } { // TOO_BIG auto hookCtx = makeStubHookContext(applyCtx, alice.id(), alice.id(), {}); auto& api = hookCtx.api(); 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(), {}); auto& api = hookCtx.api(); 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(), {}); auto& api = hookCtx.api(); 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(), {}); auto& api = hookCtx.api(); 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(), {}); auto& api = hookCtx.api(); 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(), {}); auto& api = hookCtx.api(); 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(), {}); auto& api = hookCtx.api(); // 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(), {}); auto& api = hookCtx.api(); // 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(), {}); auto& api = hookCtx.api(); // 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(), {}); auto& api = hookCtx.api(); 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(), {}); auto& api = hookCtx.api(); 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(), {}); auto& api = hookCtx.api(); 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(), {}); auto& api = hookCtx.api(); 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(), {}); auto& api = hookCtx.api(); 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(), {}); auto& api = hookCtx.api(); // 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(), {}); auto& api = hookCtx.api(); // 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(), {}); auto& api = hookCtx.api(); 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(), {}); auto& api = hookCtx.api(); // 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(), {}); auto& api = hookCtx.api(); 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); auto& api = hookCtx.api(); 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); auto& api = hookCtx.api(); 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); auto& api = hookCtx.api(); 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); auto& api = hookCtx.api(); 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); auto& api = hookCtx.api(); { // 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(), {}); auto& api = hookCtx.api(); 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); auto& api = hookCtx.api(); 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); auto& api = hookCtx.api(); 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(), {}); auto& api = hookCtx.api(); 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); auto& api = hookCtx.api(); 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(), {}); auto& api = hookCtx.api(); 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); auto& api = hookCtx.api(); 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}}}, }); auto& api = hookCtx.api(); { // 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(), {}); auto& api = hookCtx.api(); { // 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(), {}); auto& api = hookCtx.api(); 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(), {}); auto& api = hookCtx.api(); 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); auto& api = hookCtx.api(); auto const result = api.ledger_nonce(); BEAST_EXPECT(result.error() == TOO_MANY_NONCES); } { // SUCCESS auto hookCtx = makeStubHookContext(applyCtx, alice.id(), alice.id(), {}); auto& api = hookCtx.api(); 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(), {}); auto& api = hookCtx.api(); 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()}}); auto& api = hookCtx.api(); { auto hookCtx = makeStubHookContext(applyCtx, alice.id(), alice.id(), {}); auto& api = hookCtx.api(); // 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 auto& api = hookCtx.api(); auto const result = api.meta_slot(0); BEAST_EXPECT(!result.has_value()); BEAST_EXPECT(result.error() == NO_FREE_SLOTS); } { auto& api = hookCtx.api(); 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(), {}); auto& api = hookCtx.api(); { STTx invokeTx = STTx(ttINVOKE, [&](STObject& obj) {}); OpenView ov{*env.current()}; ApplyContext applyCtx = createApplyContext(env, ov, invokeTx); auto hookCtx = makeStubHookContext(applyCtx, alice.id(), alice.id(), {}); auto& api = hookCtx.api(); // 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{}; auto& api = hookCtx.api(); 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(), {}); auto& api = hookCtx.api(); 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); auto& api = hookCtx.api(); 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(), {}); auto& api = hookCtx.api(); 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(), {}); auto& api = hookCtx.api(); 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(), {}); auto& api = hookCtx.api(); 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); auto& api = hookCtx.api(); 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(), {}); auto& api = hookCtx.api(); 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(), {}); auto& api = hookCtx.api(); 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(), {}); auto& api = hookCtx.api(); 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(), {}); auto& api = hookCtx.api(); // 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(), {}); auto& api = hookCtx.api(); 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); auto& api = hookCtx.api(); 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(), {}); auto& api = hookCtx.api(); 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); auto& api = hookCtx.api(); 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)); auto& api = hookCtx.api(); 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)); auto& api = hookCtx.api(); 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); auto& api = hookCtx.api(); BEAST_EXPECT(api.otxn_type() == ttACCOUNT_SET); } { auto hookCtx = makeStubHookContext(applyCtx, alice.id(), alice.id(), {}); auto& api = hookCtx.api(); 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(), {}); auto& api = hookCtx.api(); 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(), {}); auto& api = hookCtx.api(); BEAST_EXPECT(api.slot(1).error() == DOESNT_EXIST); } // Present slot pointing to an STAmount field { 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); auto& api = hookCtx.api(); auto const result = api.slot(1); BEAST_EXPECT(result.has_value()); } } 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(), {}); auto& api = hookCtx.api(); 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); auto& api = hookCtx.api(); 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(), {}); auto& api = hookCtx.api(); 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); auto& api = hookCtx.api(); 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); auto& api = hookCtx.api(); 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(), {}); auto& api = hookCtx.api(); 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); auto& api = hookCtx.api(); 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); auto& api = hookCtx.api(); 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(), {}); auto& api = hookCtx.api(); // 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(), {}); auto& api = hookCtx.api(); 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); auto& api = hookCtx.api(); 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(), {}); auto& api = hookCtx.api(); 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); auto& api = hookCtx.api(); 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(), {}); auto& api = hookCtx.api(); 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{}; auto& api = hookCtx.api(); 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); auto& api = hookCtx.api(); 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); auto& api = hookCtx.api(); 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(), {}); auto& api = hookCtx.api(); 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); auto& api = hookCtx.api(); 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); auto& api = hookCtx.api(); auto const result = api.slot_type(2, 1); BEAST_EXPECT(result.has_value()); } } void test_state(FeatureBitset features) { testcase("Test state"); // state() is same as state_foreign with hook's namespace and account // tested in test_state_foreign BEAST_EXPECT(true); } void test_state_foreign(FeatureBitset features) { testcase("Test state_foreign"); using namespace jtx; using namespace hook_api; using namespace hook; // Note: Full state API testing requires proper hook execution // environment which is tested in SetHook_test.cpp integration tests. // Here we test cache behavior and error conditions that can be // simulated without actual ledger state access. auto const alice = Account{"alice"}; Env env{*this, features}; env.fund(XRP(10000), alice); env.close(); STTx invokeTx = STTx(ttINVOKE, [&](STObject& obj) {}); OpenView ov{*env.current()}; ApplyContext applyCtx = createApplyContext(env, ov, invokeTx); uint256 const testKey{1}; uint256 const testNs{2}; { // Test cache read path: when data is already in stateMap cache // Use external stateMap to avoid dangling reference HookStateMap stateMap; auto hookCtx = makeStubHookContext( applyCtx, alice.id(), alice.id(), {}, stateMap); // Pre-populate the stateMap cache directly Bytes expectedData{0x01, 0x02, 0x03}; stateMap[alice.id()] = { 100, // availableForReserves 1, // namespaceCount 1, // hookStateScale {{testNs, {{testKey, {false, expectedData}}}}}}; auto& api = hookCtx.api(); auto const result = api.state_foreign(testKey, testNs, alice.id()); BEAST_EXPECT(result.has_value()); BEAST_EXPECT(result.value() == expectedData); } { // Test cache miss path: key not in cache // Since view().peek() will be called for cache miss and will not // find state in ledger, it should return DOESNT_EXIST HookStateMap stateMap; auto hookCtx = makeStubHookContext( applyCtx, alice.id(), alice.id(), {}, stateMap); // Pre-populate with different key uint256 const otherKey{99}; Bytes data{0xFF}; stateMap[alice.id()] = { 100, 1, 1, {{testNs, {{otherKey, {false, data}}}}}}; auto& api = hookCtx.api(); // testKey is not in cache, so it will try to read from ledger // which should return DOESNT_EXIST since there's no actual state auto const result = api.state_foreign(testKey, testNs, alice.id()); BEAST_EXPECT(!result.has_value()); BEAST_EXPECT(result.error() == DOESNT_EXIST); } } void test_state_foreign_set_max(FeatureBitset features) { testcase("Test state_foreign_set max"); using namespace jtx; using namespace hook_api; using namespace hook; auto const alice = Account{"alice"}; Env env{*this, features}; env.fund(XRP(100000), alice); env.close(); STTx invokeTx = STTx(ttINVOKE, [&](STObject& obj) {}); OpenView ov{*env.current()}; ApplyContext applyCtx = createApplyContext(env, ov, invokeTx); { // TOO_MANY_STATE_MODIFICATIONS // This check only applies when creating a NEW key, not updating // existing uint256 const testKey{1}; uint256 const existingKey{2}; uint256 const testNs{3}; // Use external stateMap to avoid dangling reference HookStateMap stateMap; auto hookCtx = makeStubHookContext( applyCtx, alice.id(), alice.id(), {}, stateMap); // Pre-populate stateMap with a DIFFERENT key (existingKey, not // testKey) so that testKey will be a new entry stateMap[alice.id()] = { 100, // availableForReserves 1, // namespaceCount 1, // hookStateScale {{testNs, {{existingKey, {false, Bytes{}}}}}}}; // Set modified_entry_count to max stateMap.modified_entry_count = max_state_modifications; auto& api = hookCtx.api(); Bytes data{0x01}; // This should fail because testKey is new and we're at max // modifications auto const result = api.state_foreign_set(testKey, testNs, alice.id(), data); BEAST_EXPECT(!result.has_value()); BEAST_EXPECT(result.error() == TOO_MANY_STATE_MODIFICATIONS); } { // Updating existing key also fails at max modifications // (TOO_MANY_STATE_MODIFICATIONS limits total modifications, not // just new keys) uint256 const testKey{1}; uint256 const testNs{2}; HookStateMap stateMap; auto hookCtx = makeStubHookContext( applyCtx, alice.id(), alice.id(), {}, stateMap); // Pre-populate with the SAME key we'll update stateMap[alice.id()] = { 100, 1, 1, {{testNs, {{testKey, {false, Bytes{0xFF}}}}}}}; stateMap.modified_entry_count = max_state_modifications; auto& api = hookCtx.api(); Bytes data{0x01}; // This should also fail because TOO_MANY_STATE_MODIFICATIONS // applies to all modifications, not just new keys auto const result = api.state_foreign_set(testKey, testNs, alice.id(), data); BEAST_EXPECT(!result.has_value()); BEAST_EXPECT(result.error() == TOO_MANY_STATE_MODIFICATIONS); } } void test_state_foreign_set(FeatureBitset features) { testcase("Test state_foreign_set"); using namespace jtx; using namespace hook_api; using namespace hook; // Note: Full state_foreign_set testing requires proper hook execution // environment which is tested in SetHook_test.cpp integration tests. // Here we test what can be verified through cache manipulation. auto const alice = Account{"alice"}; auto const bob = Account{"bob"}; Env env{*this, features}; env.fund(XRP(100000), alice, bob); env.close(); // Compute hook hash for the accept hook auto const wasm = genesis::AcceptHook; auto const hookHash = ripple::sha512Half_s(ripple::Slice(wasm.data(), wasm.size())); // Set up a hook on bob (no grants) env(hook(bob, {{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); uint256 const testKey{1}; uint256 const testNs{2}; Bytes testData{0x01, 0x02, 0x03}; { // Local account modification with pre-populated stateMap // (equivalent to state_set test) HookStateMap stateMap; auto hookCtx = makeStubHookContext( applyCtx, alice.id(), alice.id(), {}, stateMap); // Pre-populate stateMap to avoid reserve calculation issues stateMap[alice.id()] = { 100, // availableForReserves 1, // namespaceCount 1, // hookStateScale {}}; auto& api = hookCtx.api(); auto const result = api.state_foreign_set(testKey, testNs, alice.id(), testData); BEAST_EXPECT(result.has_value()); BEAST_EXPECT(result.value() == testData.size()); auto const stateMapAcc = std::get<3>(hookCtx.result.stateMap[alice.id()]); auto const stateMapNs = stateMapAcc.find(testNs); BEAST_EXPECT(stateMapNs != stateMapAcc.end()); auto const stateMapKey = stateMapNs->second.find(testKey); BEAST_EXPECT(stateMapKey != stateMapNs->second.end()); BEAST_EXPECT(std::get<0>(stateMapKey->second) == true); } { // Delete operation (empty data) HookStateMap stateMap; auto hookCtx = makeStubHookContext( applyCtx, alice.id(), alice.id(), {}, stateMap); // Pre-populate stateMap with existing data stateMap[alice.id()] = { 100, // availableForReserves 1, // namespaceCount 1, // hookStateScale {{testNs, {{testKey, {false, testData}}}}}}; auto& api = hookCtx.api(); // Delete (empty data) Bytes emptyData{}; auto deleteResult = api.state_foreign_set(testKey, testNs, alice.id(), emptyData); BEAST_EXPECT(deleteResult.has_value()); BEAST_EXPECT(deleteResult.value() == 0); auto const stateMapAcc = std::get<3>(hookCtx.result.stateMap[alice.id()]); auto const stateMapNs = stateMapAcc.find(testNs); BEAST_EXPECT(stateMapNs != stateMapAcc.end()); auto const stateMapKey = stateMapNs->second.find(testKey); BEAST_EXPECT(stateMapKey != stateMapNs->second.end()); BEAST_EXPECT(std::get<0>(stateMapKey->second) == true); BEAST_EXPECT(std::get<1>(stateMapKey->second) == emptyData); } { // PREVIOUS_FAILURE_PREVENTS_RETRY StubHookContext stubCtx{}; stubCtx.result.foreignStateSetDisabled = true; HookStateMap stateMap; auto hookCtx = makeStubHookContext( applyCtx, alice.id(), alice.id(), stubCtx, stateMap); auto& api = hookCtx.api(); auto const result = api.state_foreign_set(testKey, testNs, bob.id(), testData); BEAST_EXPECT(!result.has_value()); BEAST_EXPECT(result.error() == PREVIOUS_FAILURE_PREVENTS_RETRY); } { // NOT_AUTHORIZED: foreign state set without grant StubHookContext stubCtx{ .result = {.hookHash = hookHash}, }; HookStateMap stateMap; auto hookCtx = makeStubHookContext( applyCtx, alice.id(), alice.id(), stubCtx, stateMap); auto& api = hookCtx.api(); auto const result = api.state_foreign_set(testKey, testNs, bob.id(), testData); BEAST_EXPECT(!result.has_value()); BEAST_EXPECT(result.error() == NOT_AUTHORIZED); // After NOT_AUTHORIZED, foreignStateSetDisabled should be set BEAST_EXPECT(hookCtx.result.foreignStateSetDisabled); } { // Cache hit: modify same state twice without re-checking grants HookStateMap stateMap; auto hookCtx = makeStubHookContext( applyCtx, alice.id(), alice.id(), {}, stateMap); // Pre-populate stateMap stateMap[alice.id()] = { 100, // availableForReserves 1, // namespaceCount 1, // hookStateScale {}}; auto& api = hookCtx.api(); // First modification auto result1 = api.state_foreign_set(testKey, testNs, alice.id(), testData); BEAST_EXPECT(result1.has_value()); // Second modification (should hit cache) Bytes newData{0x04, 0x05}; auto result2 = api.state_foreign_set(testKey, testNs, alice.id(), newData); BEAST_EXPECT(result2.has_value()); BEAST_EXPECT(result2.value() == newData.size()); } } void test_state_set(FeatureBitset features) { testcase("Test state_set"); // state_set() is same as state_foreign_set with hook's namespace and // account tested in test_state_foreign_set BEAST_EXPECT(true); } void test_sto_emplace(FeatureBitset features) { testcase("Test sto_emplace/sto_erase"); 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(), {}); auto& api = hookCtx.api(); { // 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 = *strUnHex("81140000000000000000000000000000000000000000"); // Account: "" auto const field_object = *strUnHex("2400000000"); // 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() == *strUnHex( "240000000081140000000000000000000000000000000000000000")); } auto const _source_object = *strUnHex("81140000000000000000000000000000000000000000"); { // use UINT16 auto source_object = _source_object; auto field_object = *strUnHex("10100001"); // 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); auto const erase_result = api.sto_emplace( result.value(), std::nullopt, sfVersion.getCode()); BEAST_EXPECT(erase_result.has_value()); BEAST_EXPECT(erase_result.value().size() == _source_object.size()); BEAST_EXPECT(erase_result.value() == _source_object); } { // use UINT32 auto source_object = _source_object; auto field_object = *strUnHex("2400000001"); // 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); auto const erase_result = api.sto_emplace( result.value(), std::nullopt, sfSequence.getCode()); BEAST_EXPECT(erase_result.has_value()); BEAST_EXPECT(erase_result.value().size() == _source_object.size()); BEAST_EXPECT(erase_result.value() == _source_object); } { // use UINT64 auto source_object = _source_object; auto field_object = *strUnHex("360000000000000001"); // 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); auto const erase_result = api.sto_emplace( result.value(), std::nullopt, sfExchangeRate.getCode()); BEAST_EXPECT(erase_result.has_value()); BEAST_EXPECT(erase_result.value().size() == _source_object.size()); BEAST_EXPECT(erase_result.value() == _source_object); } { // use UINT128 auto source_object = _source_object; auto field_object = *strUnHex("4100000000000000000000000000000000"); // 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); auto const erase_result = api.sto_emplace( result.value(), std::nullopt, sfEmailHash.getCode()); BEAST_EXPECT(erase_result.has_value()); BEAST_EXPECT(erase_result.value().size() == _source_object.size()); BEAST_EXPECT(erase_result.value() == _source_object); } { // use UINT256 auto source_object = _source_object; auto field_object = *strUnHex( "5E000000000000000000000000000000000000000000000000000000000000" "0000"); // 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); auto const erase_result = api.sto_emplace( result.value(), std::nullopt, sfObjectID.getCode()); BEAST_EXPECT(erase_result.has_value()); BEAST_EXPECT(erase_result.value().size() == _source_object.size()); BEAST_EXPECT(erase_result.value() == _source_object); } { // use AMOUNT auto source_object = _source_object; auto nativeamount = *strUnHex( "61999999999999999999999999999999999999999999999999999999999999" "999999999999999999999999999999999999"); auto iouamount = *strUnHex("614999999999999999"); 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); auto const erase_result = api.sto_emplace( result.value(), std::nullopt, sfAmount.getCode()); BEAST_EXPECT(erase_result.has_value()); BEAST_EXPECT( erase_result.value().size() == _source_object.size()); BEAST_EXPECT(erase_result.value() == _source_object); } } { // OBJECT auto source_object = _source_object; auto field_object = *strUnHex("E05B614000000000000064E1"); // {"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); auto const erase_result = api.sto_emplace( result.value(), std::nullopt, sfAmountEntry.getCode()); BEAST_EXPECT(erase_result.has_value()); BEAST_EXPECT(erase_result.value().size() == _source_object.size()); BEAST_EXPECT(erase_result.value() == _source_object); } { // ARRAY auto source_object = _source_object; auto field_object = *strUnHex("F9EA7D04DEADBEEFE1F1"); // {"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); auto const erase_result = api.sto_emplace( result.value(), std::nullopt, sfMemos.getCode()); BEAST_EXPECT(erase_result.has_value()); BEAST_EXPECT(erase_result.value().size() == _source_object.size()); BEAST_EXPECT(erase_result.value() == _source_object); } { // UINT8 auto source_object = _source_object; auto field_object = *strUnHex("00101001"); // {"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); auto const erase_result = api.sto_emplace( result.value(), std::nullopt, sfTickSize.getCode()); BEAST_EXPECT(erase_result.has_value()); BEAST_EXPECT(erase_result.value().size() == _source_object.size()); BEAST_EXPECT(erase_result.value() == _source_object); } { // UINT160 auto source_object = _source_object; auto field_object = *strUnHex("01110000000000000000000000005553440000000000"); // {"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); auto const erase_result = api.sto_emplace( result.value(), std::nullopt, sfTakerPaysCurrency.getCode()); BEAST_EXPECT(erase_result.has_value()); BEAST_EXPECT(erase_result.value().size() == _source_object.size()); BEAST_EXPECT(erase_result.value() == _source_object); } { // PATHSET auto source_object = _source_object; auto field_object = *strUnHex( "0112300000000000000000000000005553440000000000054F6F784A58F9EF" "B0A9EB90B83464F9D166461900"); // {"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); auto const erase_result = api.sto_emplace( result.value(), std::nullopt, sfPaths.getCode()); BEAST_EXPECT(erase_result.has_value()); BEAST_EXPECT(erase_result.value().size() == _source_object.size()); BEAST_EXPECT(erase_result.value() == _source_object); } { // VECTOR256 auto source_object = _source_object; auto field_object = *strUnHex( "03132042426C4D4F1009EE67080A9B7965B44656D7714D104A72F9B4369F97" "ABF044EE"); // {"Amendments":["42426C4D4F1009EE67080A9B7965B44656D7714D104A72F9B4369F97ABF044EE"]} auto const result = api.sto_emplace( source_object, field_object, sfAmendments.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); auto const erase_result = api.sto_emplace( result.value(), std::nullopt, sfAmendments.getCode()); BEAST_EXPECT(erase_result.has_value()); BEAST_EXPECT(erase_result.value().size() == _source_object.size()); BEAST_EXPECT(erase_result.value() == _source_object); } { // UINT96 } { // UINT384 } { // UINT512 } } 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(), {}); auto& api = hookCtx.api(); { // 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"); 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(1u, 6u)); // { Amounts: [{AmountEntry: {Amount: "100"}}] } auto const amounts = *strUnHex("F05CE05B614000000000000064E1F1"); BEAST_EXPECT( api.sto_subarray(amounts, 0).value() == std::make_pair(2u, 12u)); } } 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(), {}); auto& api = hookCtx.api(); { // 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(1u, 4u)); // { AmountEntry: {Amount: "100"} } auto const amounts = *strUnHex("E05B614000000000000064E1"); BEAST_EXPECT( api.sto_subfield(amounts, sfAmountEntry.getCode()).value() == std::make_pair(2u, 9u)); } } 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(), {}); auto& api = hookCtx.api(); 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(), {}); auto& api = hookCtx.api(); // 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(), {}); auto& api = hookCtx.api(); // 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(), {}); auto& api = hookCtx.api(); 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(), {}); auto& api = hookCtx.api(); // 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_prepare(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); // tested in test_sto_emplace 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(testable_amendments()); } }; BEAST_DEFINE_TESTSUITE_PRIO(HookAPI, app, ripple, 2); } // namespace test } // namespace ripple #undef M