mirror of
https://github.com/XRPLF/rippled.git
synced 2026-09-27 15:28:03 +00:00
Pin the wire contract of every contract host function
Completes the `host_calls/` layer: a mock host behind the real VM, with each module hand-writing its own imports, literal regions and literal lengths, so what is tested is the wire rather than whatever an SDK sends. The order of two same-shaped arguments is the thing worth asserting here, because nothing downstream would notice them swapped: a nested read takes the outer key first, and an array read takes the key before the index while the host takes them the other way round. Each is asserted against a value the other could not be. What the shim refuses before the host is reached, now stated: an account region that is not twenty bytes, a key that is not UTF-8, a negative index, a transaction type that does not fit in sixteen bits, a field code the protocol does not name, bytes that are not a transaction, bytes that are not an object, and an empty value region. `SliceIs` is added beside `BytesAre` for data that is not text. `BytesAre` compares against a string and so stops at the first NUL, which most serialized fields contain — an amount matched as "@" and passed.
This commit is contained in:
64
src/tests/libxrpl/tx/wasm/fixtures/ContractCallFixture.h
Normal file
64
src/tests/libxrpl/tx/wasm/fixtures/ContractCallFixture.h
Normal file
@@ -0,0 +1,64 @@
|
||||
#pragma once
|
||||
|
||||
#include <xrpl/protocol/AccountID.h>
|
||||
#include <xrpl/protocol/STJson.h>
|
||||
#include <xrpl/protocol/Serializer.h>
|
||||
#include <xrpl/tx/wasm/WasmCommon.h>
|
||||
|
||||
#include <tx/wasm/fixtures/WasmFixture.h>
|
||||
|
||||
#include <cstdint>
|
||||
#include <format>
|
||||
#include <string>
|
||||
|
||||
// What a contract's `host_calls` tests share: the guest-side bytes each call carries.
|
||||
//
|
||||
// These modules hand-write the ABI — literal regions, literal lengths — so they hold the
|
||||
// host to the wire rather than to whatever an SDK happens to send.
|
||||
|
||||
namespace xrpl::test {
|
||||
|
||||
struct ContractCallTest : HostCallTest
|
||||
{
|
||||
// A 20-byte account the guest holds as bytes. Distinctive at both ends, so a region read
|
||||
// at the wrong offset or the wrong length cannot match it.
|
||||
static AccountID
|
||||
account()
|
||||
{
|
||||
AccountID id;
|
||||
id.begin()[0] = 0xae;
|
||||
id.begin()[19] = 0xea;
|
||||
return id;
|
||||
}
|
||||
|
||||
// Bytes as a WAT data-segment string.
|
||||
static std::string
|
||||
escaped(Bytes const& bytes)
|
||||
{
|
||||
std::string out;
|
||||
for (auto const byte : bytes)
|
||||
out += std::format("\\{:02x}", byte);
|
||||
return out;
|
||||
}
|
||||
|
||||
static std::string
|
||||
escapedAccount()
|
||||
{
|
||||
auto const id = account();
|
||||
return escaped(Bytes{id.begin(), id.end()});
|
||||
}
|
||||
|
||||
// The bytes a guest writes for a `set_data_*` value: its one-byte type, then its
|
||||
// serialization.
|
||||
static Bytes
|
||||
valueBytes(STJson::Value const& value)
|
||||
{
|
||||
Bytes wire{static_cast<std::uint8_t>(value->getSType())};
|
||||
Serializer s;
|
||||
value->add(s);
|
||||
wire.insert(wire.end(), s.peekData().begin(), s.peekData().end());
|
||||
return wire;
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace xrpl::test
|
||||
@@ -545,6 +545,14 @@ MATCHER_P(BytesAre, expected, "")
|
||||
std::string_view{expected};
|
||||
}
|
||||
|
||||
// Matches a `Slice` against exact bytes. `BytesAre` compares against a string and so stops
|
||||
// at the first NUL, which most serialized fields contain.
|
||||
// NOLINTNEXTLINE(readability-identifier-naming)
|
||||
MATCHER_P(SliceIs, expected, "")
|
||||
{
|
||||
return Bytes{arg.data(), arg.data() + arg.size()} == expected;
|
||||
}
|
||||
|
||||
// Matches an `AccountID` against another, so an expectation can name *whose* data a
|
||||
// contract asked the host for.
|
||||
// NOLINTNEXTLINE(readability-identifier-naming)
|
||||
|
||||
81
src/tests/libxrpl/tx/wasm/host_calls/AddTxnField.cpp
Normal file
81
src/tests/libxrpl/tx/wasm/host_calls/AddTxnField.cpp
Normal file
@@ -0,0 +1,81 @@
|
||||
#include <xrpl/protocol/SField.h>
|
||||
#include <xrpl/tx/wasm/WasmCommon.h>
|
||||
|
||||
#include <gmock/gmock.h>
|
||||
#include <gtest/gtest.h>
|
||||
#include <tx/wasm/fixtures/ContractCallFixture.h>
|
||||
#include <tx/wasm/fixtures/MockHostFunctions.h>
|
||||
|
||||
#include <expected>
|
||||
#include <format>
|
||||
#include <string>
|
||||
|
||||
namespace xrpl::test {
|
||||
|
||||
using testing::Ref;
|
||||
using testing::Return;
|
||||
|
||||
// add_txn_field — a field code the shim translates into an `SField`, plus the bytes that
|
||||
// field is to be built from.
|
||||
struct AddTxnFieldCall : ContractCallTest
|
||||
{
|
||||
[[nodiscard]] std::string
|
||||
wat() const override
|
||||
{
|
||||
return std::format(
|
||||
R"wat(
|
||||
(module
|
||||
(import "host_lib" "add_txn_field"
|
||||
(func $add_txn_field (param i32 i32 i32 i32) (result i32)))
|
||||
(memory (export "memory") 1)
|
||||
(data (i32.const 0) "\40\00\00\00\00\00\00\c0")
|
||||
|
||||
(func (export "escrow_finish") (result i32)
|
||||
(call $add_txn_field (i32.const 1) (i32.const {}) (i32.const 0) (i32.const 8)))
|
||||
|
||||
;; A field code the protocol does not name: a 16-bit integer field nothing declares.
|
||||
(func (export "unknown_field") (result i32)
|
||||
(call $add_txn_field (i32.const 1) (i32.const {}) (i32.const 0) (i32.const 8)))
|
||||
|
||||
(func (export "negative_index") (result i32)
|
||||
(call $add_txn_field (i32.const -1) (i32.const {}) (i32.const 0) (i32.const 8))))
|
||||
)wat",
|
||||
sfAmount.getCode(),
|
||||
(static_cast<int>(STI_UINT16) << 16) | 9999,
|
||||
sfAmount.getCode());
|
||||
}
|
||||
};
|
||||
|
||||
TEST_F(AddTxnFieldCall, TheFieldCodeBecomesAnSFieldAndTheBytesArrive)
|
||||
{
|
||||
auto const amount = Bytes{0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xc0};
|
||||
EXPECT_CALL(host, addTxnField(1U, Ref(sfAmount), SliceIs(amount))).WillOnce(Return(0));
|
||||
|
||||
EXPECT_EQ(hostAnswer(), 0);
|
||||
}
|
||||
|
||||
// A code the protocol does not name has no `SField` to translate to, so the host is never
|
||||
// asked which field the contract meant.
|
||||
TEST_F(AddTxnFieldCall, AnUnknownFieldCodeIsRefusedWithoutAskingTheHost)
|
||||
{
|
||||
EXPECT_CALL(host, addTxnField).Times(0);
|
||||
|
||||
EXPECT_EQ(hostAnswer("unknown_field"), hfErrorToInt(HostFunctionError::InvalidField));
|
||||
}
|
||||
|
||||
TEST_F(AddTxnFieldCall, ANegativeIndexIsRefusedWithoutAskingTheHost)
|
||||
{
|
||||
EXPECT_CALL(host, addTxnField).Times(0);
|
||||
|
||||
EXPECT_EQ(hostAnswer("negative_index"), hfErrorToInt(HostFunctionError::IndexOutOfBounds));
|
||||
}
|
||||
|
||||
TEST_F(AddTxnFieldCall, HostErrorBecomesContractReturnValue)
|
||||
{
|
||||
EXPECT_CALL(host, addTxnField)
|
||||
.WillOnce(Return(std::unexpected(HostFunctionError::FieldNotFound)));
|
||||
|
||||
EXPECT_EQ(hostAnswer(), hfErrorToInt(HostFunctionError::FieldNotFound));
|
||||
}
|
||||
|
||||
} // namespace xrpl::test
|
||||
72
src/tests/libxrpl/tx/wasm/host_calls/BuildTxn.cpp
Normal file
72
src/tests/libxrpl/tx/wasm/host_calls/BuildTxn.cpp
Normal file
@@ -0,0 +1,72 @@
|
||||
#include <xrpl/protocol/TxFormats.h>
|
||||
#include <xrpl/tx/wasm/WasmCommon.h>
|
||||
|
||||
#include <gmock/gmock.h>
|
||||
#include <gtest/gtest.h>
|
||||
#include <tx/wasm/fixtures/ContractCallFixture.h>
|
||||
#include <tx/wasm/fixtures/MockHostFunctions.h>
|
||||
|
||||
#include <expected>
|
||||
#include <string>
|
||||
|
||||
namespace xrpl::test {
|
||||
|
||||
using testing::Return;
|
||||
|
||||
// build_txn — one scalar in, one out. The narrowest call in the ABI, and the only one whose
|
||||
// argument has to be narrowed: a transaction type is sixteen bits inside wasm's thirty-two.
|
||||
struct BuildTxnCall : ContractCallTest
|
||||
{
|
||||
[[nodiscard]] std::string
|
||||
wat() const override
|
||||
{
|
||||
return std::string{R"wat(
|
||||
(module
|
||||
(import "host_lib" "build_txn" (func $build_txn (param i32) (result i32)))
|
||||
(memory (export "memory") 1)
|
||||
|
||||
(func (export "escrow_finish") (result i32)
|
||||
(call $build_txn (i32.const 0)))
|
||||
|
||||
;; A type that does not fit in sixteen bits, which no transaction type can be.
|
||||
(func (export "type_too_large") (result i32)
|
||||
(call $build_txn (i32.const 65536)))
|
||||
|
||||
(func (export "negative_type") (result i32)
|
||||
(call $build_txn (i32.const -1))))
|
||||
)wat"};
|
||||
}
|
||||
};
|
||||
|
||||
TEST_F(BuildTxnCall, TheTypeReachesTheHostAndTheIndexComesBack)
|
||||
{
|
||||
EXPECT_CALL(host, buildTxn(static_cast<std::uint16_t>(ttPAYMENT))).WillOnce(Return(3));
|
||||
|
||||
EXPECT_EQ(hostAnswer(), 3);
|
||||
}
|
||||
|
||||
// Narrowing an out-of-range type would ask the host to build something else entirely, so it
|
||||
// is refused instead.
|
||||
TEST_F(BuildTxnCall, ATypeThatDoesNotFitIsRefusedWithoutAskingTheHost)
|
||||
{
|
||||
EXPECT_CALL(host, buildTxn).Times(0);
|
||||
|
||||
EXPECT_EQ(hostAnswer("type_too_large"), hfErrorToInt(HostFunctionError::InvalidParams));
|
||||
}
|
||||
|
||||
TEST_F(BuildTxnCall, ANegativeTypeIsRefusedWithoutAskingTheHost)
|
||||
{
|
||||
EXPECT_CALL(host, buildTxn).Times(0);
|
||||
|
||||
EXPECT_EQ(hostAnswer("negative_type"), hfErrorToInt(HostFunctionError::InvalidParams));
|
||||
}
|
||||
|
||||
TEST_F(BuildTxnCall, HostErrorBecomesContractReturnValue)
|
||||
{
|
||||
EXPECT_CALL(host, buildTxn)
|
||||
.WillOnce(Return(std::unexpected(HostFunctionError::SubmitTxnFailure)));
|
||||
|
||||
EXPECT_EQ(hostAnswer(), hfErrorToInt(HostFunctionError::SubmitTxnFailure));
|
||||
}
|
||||
|
||||
} // namespace xrpl::test
|
||||
84
src/tests/libxrpl/tx/wasm/host_calls/EmitEvent.cpp
Normal file
84
src/tests/libxrpl/tx/wasm/host_calls/EmitEvent.cpp
Normal file
@@ -0,0 +1,84 @@
|
||||
#include <xrpl/protocol/STJson.h>
|
||||
#include <xrpl/tx/wasm/WasmCommon.h>
|
||||
|
||||
#include <gmock/gmock.h>
|
||||
#include <gtest/gtest.h>
|
||||
#include <tx/wasm/fixtures/ContractCallFixture.h>
|
||||
#include <tx/wasm/fixtures/MockHostFunctions.h>
|
||||
|
||||
#include <expected>
|
||||
#include <format>
|
||||
#include <string>
|
||||
|
||||
namespace xrpl::test {
|
||||
|
||||
using testing::Return;
|
||||
|
||||
// emit_event — a name and a serialized object in, a scalar out. The second of the two wire
|
||||
// formats a guest writes itself, and a different parser from the one a value goes through.
|
||||
struct EmitEventCall : ContractCallTest
|
||||
{
|
||||
static STJson
|
||||
event()
|
||||
{
|
||||
return STJson{STJson::Map{
|
||||
{"count",
|
||||
std::static_pointer_cast<STBase>(std::make_shared<STUInt32>(sfSequence, 32U))}}};
|
||||
}
|
||||
|
||||
[[nodiscard]] std::string
|
||||
wat() const override
|
||||
{
|
||||
auto const blob = event().toBlob();
|
||||
return std::format(
|
||||
R"wat(
|
||||
(module
|
||||
(import "host_lib" "emit_event" (func $emit_event (param i32 i32 i32 i32) (result i32)))
|
||||
(memory (export "memory") 1)
|
||||
(data (i32.const 0) "transferred")
|
||||
(data (i32.const 32) "{}")
|
||||
(data (i32.const 2048) "not an object")
|
||||
|
||||
(func (export "escrow_finish") (result i32)
|
||||
(call $emit_event (i32.const 0) (i32.const 11) (i32.const 32) (i32.const {})))
|
||||
|
||||
(func (export "not_an_object") (result i32)
|
||||
(call $emit_event (i32.const 0) (i32.const 11) (i32.const 2048) (i32.const 13)))
|
||||
|
||||
(func (export "empty_data") (result i32)
|
||||
(call $emit_event (i32.const 0) (i32.const 11) (i32.const 32) (i32.const 0))))
|
||||
)wat",
|
||||
escaped(Bytes{blob.begin(), blob.end()}),
|
||||
blob.size());
|
||||
}
|
||||
};
|
||||
|
||||
TEST_F(EmitEventCall, TheNameAndTheDecodedObjectReachTheHost)
|
||||
{
|
||||
EXPECT_CALL(host, emitEvent(BytesAre("transferred"), EventJsonEq(event()))).WillOnce(Return(0));
|
||||
|
||||
EXPECT_EQ(hostAnswer(), 0);
|
||||
}
|
||||
|
||||
TEST_F(EmitEventCall, BytesThatAreNotAnObjectAreRefusedWithoutAskingTheHost)
|
||||
{
|
||||
EXPECT_CALL(host, emitEvent).Times(0);
|
||||
|
||||
EXPECT_EQ(hostAnswer("not_an_object"), hfErrorToInt(HostFunctionError::InvalidParams));
|
||||
}
|
||||
|
||||
TEST_F(EmitEventCall, AnEmptyRegionIsNotAnObject)
|
||||
{
|
||||
EXPECT_CALL(host, emitEvent).Times(0);
|
||||
|
||||
EXPECT_EQ(hostAnswer("empty_data"), hfErrorToInt(HostFunctionError::InvalidParams));
|
||||
}
|
||||
|
||||
TEST_F(EmitEventCall, HostErrorBecomesContractReturnValue)
|
||||
{
|
||||
EXPECT_CALL(host, emitEvent).WillOnce(Return(std::unexpected(HostFunctionError::InvalidState)));
|
||||
|
||||
EXPECT_EQ(hostAnswer(), hfErrorToInt(HostFunctionError::InvalidState));
|
||||
}
|
||||
|
||||
} // namespace xrpl::test
|
||||
109
src/tests/libxrpl/tx/wasm/host_calls/EmitTxn.cpp
Normal file
109
src/tests/libxrpl/tx/wasm/host_calls/EmitTxn.cpp
Normal file
@@ -0,0 +1,109 @@
|
||||
#include <xrpl/protocol/SField.h>
|
||||
#include <xrpl/protocol/STAmount.h>
|
||||
#include <xrpl/protocol/STTx.h>
|
||||
#include <xrpl/protocol/TER.h>
|
||||
#include <xrpl/protocol/TxFlags.h>
|
||||
#include <xrpl/protocol/TxFormats.h>
|
||||
#include <xrpl/tx/wasm/WasmCommon.h>
|
||||
|
||||
#include <gmock/gmock.h>
|
||||
#include <gtest/gtest.h>
|
||||
#include <tx/wasm/fixtures/ContractCallFixture.h>
|
||||
#include <tx/wasm/fixtures/MockHostFunctions.h>
|
||||
|
||||
#include <expected>
|
||||
#include <format>
|
||||
#include <string>
|
||||
|
||||
namespace xrpl::test {
|
||||
|
||||
using testing::Return;
|
||||
|
||||
// emit_txn — a serialized transaction in, a TER out through the guest's buffer.
|
||||
//
|
||||
// The shim deserializes, so the host is handed a transaction rather than bytes: a guest that
|
||||
// writes something that is not one never reaches it.
|
||||
struct EmitTxnCall : ContractCallTest
|
||||
{
|
||||
// A transaction as a contract would have serialized it.
|
||||
static STTx
|
||||
transaction()
|
||||
{
|
||||
return STTx{ttPAYMENT, [](STObject& obj) {
|
||||
obj.setAccountID(sfAccount, account());
|
||||
obj.setAccountID(sfDestination, account());
|
||||
obj.setFieldAmount(sfAmount, STAmount{192});
|
||||
obj.setFieldAmount(sfFee, STAmount{0});
|
||||
obj.setFieldU32(sfSequence, 1);
|
||||
obj.setFieldU32(sfFlags, tfInnerBatchTxn);
|
||||
obj.setFieldVL(sfSigningPubKey, Blob{});
|
||||
}};
|
||||
}
|
||||
|
||||
static Bytes
|
||||
serialized()
|
||||
{
|
||||
auto const s = transaction().getSerializer();
|
||||
return Bytes{s.peekData().begin(), s.peekData().end()};
|
||||
}
|
||||
|
||||
[[nodiscard]] std::string
|
||||
wat() const override
|
||||
{
|
||||
auto const bytes = serialized();
|
||||
return std::format(
|
||||
R"wat(
|
||||
(module
|
||||
(import "host_lib" "emit_txn" (func $emit_txn (param i32 i32 i32 i32) (result i32)))
|
||||
(memory (export "memory") 1)
|
||||
(data (i32.const 0) "{}")
|
||||
(data (i32.const 2048) "not a transaction")
|
||||
|
||||
;; Emits the serialized transaction and returns the TER it wrote.
|
||||
(func (export "escrow_finish") (result i32)
|
||||
(local $n i32)
|
||||
(local.set $n
|
||||
(call $emit_txn (i32.const 0) (i32.const {}) (i32.const 3072) (i32.const 4)))
|
||||
(select (local.get $n) (i32.load (i32.const 3072)) (i32.lt_s (local.get $n) (i32.const 0))))
|
||||
|
||||
(func (export "not_a_transaction") (result i32)
|
||||
(call $emit_txn (i32.const 2048) (i32.const 17) (i32.const 3072) (i32.const 4))))
|
||||
)wat",
|
||||
escaped(bytes),
|
||||
bytes.size());
|
||||
}
|
||||
};
|
||||
|
||||
TEST_F(EmitTxnCall, TheGuestsBytesReachTheHostAsATransaction)
|
||||
{
|
||||
EXPECT_CALL(host, emitTxn(StTxIdIs(transaction().getTransactionID())))
|
||||
.WillOnce(Return(TERtoInt(tesSUCCESS)));
|
||||
|
||||
EXPECT_EQ(hostAnswer(), TERtoInt(tesSUCCESS));
|
||||
}
|
||||
|
||||
// The TER travels in the buffer because it may be negative, and a negative return is a host
|
||||
// error. This is the case that would otherwise have stopped the run.
|
||||
TEST_F(EmitTxnCall, ANegativeTerArrivesIntact)
|
||||
{
|
||||
EXPECT_CALL(host, emitTxn).WillOnce(Return(TERtoInt(tefPAST_SEQ)));
|
||||
|
||||
EXPECT_EQ(hostAnswer(), TERtoInt(tefPAST_SEQ));
|
||||
}
|
||||
|
||||
TEST_F(EmitTxnCall, BytesThatAreNotATransactionAreRefusedWithoutAskingTheHost)
|
||||
{
|
||||
EXPECT_CALL(host, emitTxn).Times(0);
|
||||
|
||||
EXPECT_EQ(hostAnswer("not_a_transaction"), hfErrorToInt(HostFunctionError::InvalidParams));
|
||||
}
|
||||
|
||||
TEST_F(EmitTxnCall, HostErrorBecomesContractReturnValue)
|
||||
{
|
||||
EXPECT_CALL(host, emitTxn)
|
||||
.WillOnce(Return(std::unexpected(HostFunctionError::SubmitTxnFailure)));
|
||||
|
||||
EXPECT_EQ(hostAnswer(), hfErrorToInt(HostFunctionError::SubmitTxnFailure));
|
||||
}
|
||||
|
||||
} // namespace xrpl::test
|
||||
80
src/tests/libxrpl/tx/wasm/host_calls/FunctionParam.cpp
Normal file
80
src/tests/libxrpl/tx/wasm/host_calls/FunctionParam.cpp
Normal file
@@ -0,0 +1,80 @@
|
||||
#include <xrpl/protocol/SField.h>
|
||||
#include <xrpl/tx/wasm/WasmCommon.h>
|
||||
|
||||
#include <gmock/gmock.h>
|
||||
#include <gtest/gtest.h>
|
||||
#include <tx/wasm/fixtures/ContractCallFixture.h>
|
||||
#include <tx/wasm/fixtures/MockHostFunctions.h>
|
||||
|
||||
#include <expected>
|
||||
#include <string>
|
||||
|
||||
namespace xrpl::test {
|
||||
|
||||
using testing::Return;
|
||||
|
||||
// function_param — the same shape as `instance_param`, over the call's own parameters.
|
||||
// Which of the two tables the host is asked about is the whole of the difference.
|
||||
struct FunctionParamCall : ContractCallTest
|
||||
{
|
||||
[[nodiscard]] std::string
|
||||
wat() const override
|
||||
{
|
||||
return std::string{R"wat(
|
||||
(module
|
||||
(import "host_lib" "function_param"
|
||||
(func $function_param (param i32 i32 i32 i32) (result i32)))
|
||||
(memory (export "memory") 1)
|
||||
|
||||
;; Asks for parameter 2 as an STI_UINT32 (2) and returns the four bytes that landed.
|
||||
(func (export "escrow_finish") (result i32)
|
||||
(local $n i32)
|
||||
(local.set $n
|
||||
(call $function_param (i32.const 2) (i32.const 2) (i32.const 0) (i32.const 32)))
|
||||
(select (local.get $n) (i32.load (i32.const 0)) (i32.lt_s (local.get $n) (i32.const 0))))
|
||||
|
||||
;; Reports the length, for the cases where the value itself is not the point.
|
||||
(func (export "value_length") (result i32)
|
||||
(call $function_param (i32.const 2) (i32.const 2) (i32.const 0) (i32.const 32)))
|
||||
|
||||
;; A negative index, which the shim refuses rather than casting.
|
||||
(func (export "negative_index") (result i32)
|
||||
(call $function_param (i32.const -1) (i32.const 2) (i32.const 0) (i32.const 32))))
|
||||
)wat"};
|
||||
}
|
||||
};
|
||||
|
||||
TEST_F(FunctionParamCall, TheIndexAndTypeReachTheHostAndTheBytesComeBack)
|
||||
{
|
||||
EXPECT_CALL(host, functionParam(2U, STI_UINT32))
|
||||
.WillOnce(Return(Bytes{0x0d, 0x0c, 0x0b, 0x0a}));
|
||||
EXPECT_CALL(host, instanceParam).Times(0);
|
||||
|
||||
EXPECT_EQ(hostAnswer(), 0x0a0b0c0d) << "the value's bytes, read back little-endian";
|
||||
}
|
||||
|
||||
TEST_F(FunctionParamCall, TheAnswerIsTheValuesLength)
|
||||
{
|
||||
EXPECT_CALL(host, functionParam).WillOnce(Return(Bytes{1, 2, 3, 4}));
|
||||
|
||||
EXPECT_EQ(hostAnswer("value_length"), 4);
|
||||
}
|
||||
|
||||
// A negative index is out of range, not a very large one: casting it would ask the host
|
||||
// about a parameter no contract could have meant.
|
||||
TEST_F(FunctionParamCall, ANegativeIndexIsRefusedWithoutAskingTheHost)
|
||||
{
|
||||
EXPECT_CALL(host, functionParam).Times(0);
|
||||
|
||||
EXPECT_EQ(hostAnswer("negative_index"), hfErrorToInt(HostFunctionError::InvalidParams));
|
||||
}
|
||||
|
||||
TEST_F(FunctionParamCall, HostErrorBecomesContractReturnValue)
|
||||
{
|
||||
EXPECT_CALL(host, functionParam)
|
||||
.WillOnce(Return(std::unexpected(HostFunctionError::IndexOutOfBounds)));
|
||||
|
||||
EXPECT_EQ(hostAnswer(), hfErrorToInt(HostFunctionError::IndexOutOfBounds));
|
||||
}
|
||||
|
||||
} // namespace xrpl::test
|
||||
@@ -0,0 +1,76 @@
|
||||
#include <xrpl/tx/wasm/WasmCommon.h>
|
||||
|
||||
#include <gmock/gmock.h>
|
||||
#include <gtest/gtest.h>
|
||||
#include <tx/wasm/fixtures/ContractCallFixture.h>
|
||||
#include <tx/wasm/fixtures/MockHostFunctions.h>
|
||||
|
||||
#include <expected>
|
||||
#include <format>
|
||||
#include <string>
|
||||
|
||||
namespace xrpl::test {
|
||||
|
||||
using testing::Return;
|
||||
|
||||
// get_data_array_element_field — the shape with a scalar *between* two regions: account,
|
||||
// key, index, out.
|
||||
//
|
||||
// The key comes before the index on the wire, and the host takes them the other way round.
|
||||
// A test that only checked both arrived would pass with them swapped, so this asserts each
|
||||
// against a value the other could not be.
|
||||
struct GetDataArrayElementFieldCall : ContractCallTest
|
||||
{
|
||||
[[nodiscard]] std::string
|
||||
wat() const override
|
||||
{
|
||||
return std::format(
|
||||
R"wat(
|
||||
(module
|
||||
(import "host_lib" "get_data_array_element_field"
|
||||
(func $get_data_array_element_field (param i32 i32 i32 i32 i32 i32 i32) (result i32)))
|
||||
(memory (export "memory") 1)
|
||||
(data (i32.const 0) "{}")
|
||||
(data (i32.const 32) "amount")
|
||||
|
||||
(func (export "escrow_finish") (result i32)
|
||||
(local $n i32)
|
||||
(local.set $n (call $get_data_array_element_field
|
||||
(i32.const 0) (i32.const 20) (i32.const 32) (i32.const 6) (i32.const 7)
|
||||
(i32.const 64) (i32.const 32)))
|
||||
(select (local.get $n) (i32.load (i32.const 64)) (i32.lt_s (local.get $n) (i32.const 0))))
|
||||
|
||||
;; A negative element index, which the shim refuses rather than casting to a huge one.
|
||||
(func (export "negative_index") (result i32)
|
||||
(call $get_data_array_element_field
|
||||
(i32.const 0) (i32.const 20) (i32.const 32) (i32.const 6) (i32.const -1)
|
||||
(i32.const 64) (i32.const 32))))
|
||||
)wat",
|
||||
escapedAccount());
|
||||
}
|
||||
};
|
||||
|
||||
TEST_F(GetDataArrayElementFieldCall, TheKeyIsTheRegionAndTheIndexIsTheScalar)
|
||||
{
|
||||
EXPECT_CALL(host, getDataArrayElementField(AccountIs(account()), 7U, BytesAre("amount")))
|
||||
.WillOnce(Return(Bytes{0x0d, 0x0c, 0x0b, 0x0a}));
|
||||
|
||||
EXPECT_EQ(hostAnswer(), 0x0a0b0c0d);
|
||||
}
|
||||
|
||||
TEST_F(GetDataArrayElementFieldCall, ANegativeIndexIsRefusedWithoutAskingTheHost)
|
||||
{
|
||||
EXPECT_CALL(host, getDataArrayElementField).Times(0);
|
||||
|
||||
EXPECT_EQ(hostAnswer("negative_index"), hfErrorToInt(HostFunctionError::IndexOutOfBounds));
|
||||
}
|
||||
|
||||
TEST_F(GetDataArrayElementFieldCall, HostErrorBecomesContractReturnValue)
|
||||
{
|
||||
EXPECT_CALL(host, getDataArrayElementField)
|
||||
.WillOnce(Return(std::unexpected(HostFunctionError::InvalidState)));
|
||||
|
||||
EXPECT_EQ(hostAnswer(), hfErrorToInt(HostFunctionError::InvalidState));
|
||||
}
|
||||
|
||||
} // namespace xrpl::test
|
||||
@@ -0,0 +1,76 @@
|
||||
#include <xrpl/tx/wasm/WasmCommon.h>
|
||||
|
||||
#include <gmock/gmock.h>
|
||||
#include <gtest/gtest.h>
|
||||
#include <tx/wasm/fixtures/ContractCallFixture.h>
|
||||
#include <tx/wasm/fixtures/MockHostFunctions.h>
|
||||
|
||||
#include <expected>
|
||||
#include <format>
|
||||
#include <string>
|
||||
|
||||
namespace xrpl::test {
|
||||
|
||||
using testing::Return;
|
||||
|
||||
// get_data_nested_array_element_field — the widest shape in the ABI: two string keys with a
|
||||
// scalar index between them, then the output region.
|
||||
struct GetDataNestedArrayElementFieldCall : ContractCallTest
|
||||
{
|
||||
[[nodiscard]] std::string
|
||||
wat() const override
|
||||
{
|
||||
return std::format(
|
||||
R"wat(
|
||||
(module
|
||||
(import "host_lib" "get_data_nested_array_element_field"
|
||||
(func $get_data_nested_array_element_field
|
||||
(param i32 i32 i32 i32 i32 i32 i32 i32 i32) (result i32)))
|
||||
(memory (export "memory") 1)
|
||||
(data (i32.const 0) "{}")
|
||||
(data (i32.const 32) "items")
|
||||
(data (i32.const 48) "price")
|
||||
|
||||
(func (export "escrow_finish") (result i32)
|
||||
(local $n i32)
|
||||
(local.set $n (call $get_data_nested_array_element_field
|
||||
(i32.const 0) (i32.const 20) (i32.const 32) (i32.const 5) (i32.const 7)
|
||||
(i32.const 48) (i32.const 5) (i32.const 64) (i32.const 32)))
|
||||
(select (local.get $n) (i32.load (i32.const 64)) (i32.lt_s (local.get $n) (i32.const 0))))
|
||||
|
||||
(func (export "negative_index") (result i32)
|
||||
(call $get_data_nested_array_element_field
|
||||
(i32.const 0) (i32.const 20) (i32.const 32) (i32.const 5) (i32.const -1)
|
||||
(i32.const 48) (i32.const 5) (i32.const 64) (i32.const 32))))
|
||||
)wat",
|
||||
escapedAccount());
|
||||
}
|
||||
};
|
||||
|
||||
TEST_F(GetDataNestedArrayElementFieldCall, EachRegionAndTheIndexArriveInOrder)
|
||||
{
|
||||
EXPECT_CALL(
|
||||
host,
|
||||
getDataNestedArrayElementField(
|
||||
AccountIs(account()), BytesAre("items"), 7U, BytesAre("price")))
|
||||
.WillOnce(Return(Bytes{0x0d, 0x0c, 0x0b, 0x0a}));
|
||||
|
||||
EXPECT_EQ(hostAnswer(), 0x0a0b0c0d);
|
||||
}
|
||||
|
||||
TEST_F(GetDataNestedArrayElementFieldCall, ANegativeIndexIsRefusedWithoutAskingTheHost)
|
||||
{
|
||||
EXPECT_CALL(host, getDataNestedArrayElementField).Times(0);
|
||||
|
||||
EXPECT_EQ(hostAnswer("negative_index"), hfErrorToInt(HostFunctionError::IndexOutOfBounds));
|
||||
}
|
||||
|
||||
TEST_F(GetDataNestedArrayElementFieldCall, HostErrorBecomesContractReturnValue)
|
||||
{
|
||||
EXPECT_CALL(host, getDataNestedArrayElementField)
|
||||
.WillOnce(Return(std::unexpected(HostFunctionError::InvalidField)));
|
||||
|
||||
EXPECT_EQ(hostAnswer(), hfErrorToInt(HostFunctionError::InvalidField));
|
||||
}
|
||||
|
||||
} // namespace xrpl::test
|
||||
@@ -0,0 +1,64 @@
|
||||
#include <xrpl/tx/wasm/WasmCommon.h>
|
||||
|
||||
#include <gmock/gmock.h>
|
||||
#include <gtest/gtest.h>
|
||||
#include <tx/wasm/fixtures/ContractCallFixture.h>
|
||||
#include <tx/wasm/fixtures/MockHostFunctions.h>
|
||||
|
||||
#include <expected>
|
||||
#include <format>
|
||||
#include <string>
|
||||
|
||||
namespace xrpl::test {
|
||||
|
||||
using testing::Return;
|
||||
|
||||
// get_data_nested_object_field — two string keys in, bytes out.
|
||||
//
|
||||
// The outer key comes first. Two regions of the same kind are the easiest pair to hand over
|
||||
// backwards, and nothing downstream would notice, so the order is asserted with keys that
|
||||
// could not be mistaken for one another.
|
||||
struct GetDataNestedObjectFieldCall : ContractCallTest
|
||||
{
|
||||
[[nodiscard]] std::string
|
||||
wat() const override
|
||||
{
|
||||
return std::format(
|
||||
R"wat(
|
||||
(module
|
||||
(import "host_lib" "get_data_nested_object_field"
|
||||
(func $get_data_nested_object_field (param i32 i32 i32 i32 i32 i32 i32 i32) (result i32)))
|
||||
(memory (export "memory") 1)
|
||||
(data (i32.const 0) "{}")
|
||||
(data (i32.const 32) "stats")
|
||||
(data (i32.const 48) "score")
|
||||
|
||||
(func (export "escrow_finish") (result i32)
|
||||
(local $n i32)
|
||||
(local.set $n (call $get_data_nested_object_field
|
||||
(i32.const 0) (i32.const 20) (i32.const 32) (i32.const 5) (i32.const 48) (i32.const 5)
|
||||
(i32.const 64) (i32.const 32)))
|
||||
(select (local.get $n) (i32.load (i32.const 64)) (i32.lt_s (local.get $n) (i32.const 0)))))
|
||||
)wat",
|
||||
escapedAccount());
|
||||
}
|
||||
};
|
||||
|
||||
TEST_F(GetDataNestedObjectFieldCall, TheOuterKeyIsTheFirstRegion)
|
||||
{
|
||||
EXPECT_CALL(
|
||||
host, getDataNestedObjectField(AccountIs(account()), BytesAre("stats"), BytesAre("score")))
|
||||
.WillOnce(Return(Bytes{0x0d, 0x0c, 0x0b, 0x0a}));
|
||||
|
||||
EXPECT_EQ(hostAnswer(), 0x0a0b0c0d);
|
||||
}
|
||||
|
||||
TEST_F(GetDataNestedObjectFieldCall, HostErrorBecomesContractReturnValue)
|
||||
{
|
||||
EXPECT_CALL(host, getDataNestedObjectField)
|
||||
.WillOnce(Return(std::unexpected(HostFunctionError::InvalidField)));
|
||||
|
||||
EXPECT_EQ(hostAnswer(), hfErrorToInt(HostFunctionError::InvalidField));
|
||||
}
|
||||
|
||||
} // namespace xrpl::test
|
||||
85
src/tests/libxrpl/tx/wasm/host_calls/GetDataObjectField.cpp
Normal file
85
src/tests/libxrpl/tx/wasm/host_calls/GetDataObjectField.cpp
Normal file
@@ -0,0 +1,85 @@
|
||||
#include <xrpl/tx/wasm/WasmCommon.h>
|
||||
|
||||
#include <gmock/gmock.h>
|
||||
#include <gtest/gtest.h>
|
||||
#include <tx/wasm/fixtures/ContractCallFixture.h>
|
||||
#include <tx/wasm/fixtures/MockHostFunctions.h>
|
||||
|
||||
#include <expected>
|
||||
#include <format>
|
||||
#include <string>
|
||||
|
||||
namespace xrpl::test {
|
||||
|
||||
using testing::Return;
|
||||
|
||||
// get_data_object_field — an account region and a string key in, bytes out.
|
||||
//
|
||||
// The key is declared `&str`, so the engine checks it is UTF-8 before the host sees it: a
|
||||
// region that is not becomes `InvalidParams` without a call.
|
||||
struct GetDataObjectFieldCall : ContractCallTest
|
||||
{
|
||||
[[nodiscard]] std::string
|
||||
wat() const override
|
||||
{
|
||||
return std::format(
|
||||
R"wat(
|
||||
(module
|
||||
(import "host_lib" "get_data_object_field"
|
||||
(func $get_data_object_field (param i32 i32 i32 i32 i32 i32) (result i32)))
|
||||
(memory (export "memory") 1)
|
||||
(data (i32.const 0) "{}")
|
||||
(data (i32.const 32) "count")
|
||||
(data (i32.const 48) "\ff\fe")
|
||||
|
||||
(func (export "escrow_finish") (result i32)
|
||||
(local $n i32)
|
||||
(local.set $n (call $get_data_object_field
|
||||
(i32.const 0) (i32.const 20) (i32.const 32) (i32.const 5) (i32.const 64) (i32.const 32)))
|
||||
(select (local.get $n) (i32.load (i32.const 64)) (i32.lt_s (local.get $n) (i32.const 0))))
|
||||
|
||||
;; A nineteen-byte account, which is not an account id.
|
||||
(func (export "short_account") (result i32)
|
||||
(call $get_data_object_field
|
||||
(i32.const 0) (i32.const 19) (i32.const 32) (i32.const 5) (i32.const 64) (i32.const 32)))
|
||||
|
||||
;; A key region that is not UTF-8.
|
||||
(func (export "invalid_key") (result i32)
|
||||
(call $get_data_object_field
|
||||
(i32.const 0) (i32.const 20) (i32.const 48) (i32.const 2) (i32.const 64) (i32.const 32))))
|
||||
)wat",
|
||||
escapedAccount());
|
||||
}
|
||||
};
|
||||
|
||||
TEST_F(GetDataObjectFieldCall, TheAccountAndKeyReachTheHostAndTheValueComesBack)
|
||||
{
|
||||
EXPECT_CALL(host, getDataObjectField(AccountIs(account()), BytesAre("count")))
|
||||
.WillOnce(Return(Bytes{0x0d, 0x0c, 0x0b, 0x0a}));
|
||||
|
||||
EXPECT_EQ(hostAnswer(), 0x0a0b0c0d);
|
||||
}
|
||||
|
||||
TEST_F(GetDataObjectFieldCall, AnAccountRegionOfTheWrongLengthIsRefusedWithoutAskingTheHost)
|
||||
{
|
||||
EXPECT_CALL(host, getDataObjectField).Times(0);
|
||||
|
||||
EXPECT_EQ(hostAnswer("short_account"), hfErrorToInt(HostFunctionError::InvalidParams));
|
||||
}
|
||||
|
||||
TEST_F(GetDataObjectFieldCall, AKeyThatIsNotTextIsRefusedWithoutAskingTheHost)
|
||||
{
|
||||
EXPECT_CALL(host, getDataObjectField).Times(0);
|
||||
|
||||
EXPECT_EQ(hostAnswer("invalid_key"), hfErrorToInt(HostFunctionError::InvalidParams));
|
||||
}
|
||||
|
||||
TEST_F(GetDataObjectFieldCall, HostErrorBecomesContractReturnValue)
|
||||
{
|
||||
EXPECT_CALL(host, getDataObjectField)
|
||||
.WillOnce(Return(std::unexpected(HostFunctionError::LedgerObjNotFound)));
|
||||
|
||||
EXPECT_EQ(hostAnswer(), hfErrorToInt(HostFunctionError::LedgerObjNotFound));
|
||||
}
|
||||
|
||||
} // namespace xrpl::test
|
||||
79
src/tests/libxrpl/tx/wasm/host_calls/InstanceParam.cpp
Normal file
79
src/tests/libxrpl/tx/wasm/host_calls/InstanceParam.cpp
Normal file
@@ -0,0 +1,79 @@
|
||||
#include <xrpl/protocol/SField.h>
|
||||
#include <xrpl/tx/wasm/WasmCommon.h>
|
||||
|
||||
#include <gmock/gmock.h>
|
||||
#include <gtest/gtest.h>
|
||||
#include <tx/wasm/fixtures/ContractCallFixture.h>
|
||||
#include <tx/wasm/fixtures/MockHostFunctions.h>
|
||||
|
||||
#include <expected>
|
||||
#include <string>
|
||||
|
||||
namespace xrpl::test {
|
||||
|
||||
using testing::Return;
|
||||
|
||||
// instance_param — two scalars in, bytes out. The scalars are wasm's own `i32`, so nothing
|
||||
// is marshalled on the way in and the index reaches the host as it was written.
|
||||
struct InstanceParamCall : ContractCallTest
|
||||
{
|
||||
[[nodiscard]] std::string
|
||||
wat() const override
|
||||
{
|
||||
return std::string{R"wat(
|
||||
(module
|
||||
(import "host_lib" "instance_param"
|
||||
(func $instance_param (param i32 i32 i32 i32) (result i32)))
|
||||
(memory (export "memory") 1)
|
||||
|
||||
;; Asks for parameter 2 as an STI_UINT32 (2) and returns the four bytes that landed.
|
||||
(func (export "escrow_finish") (result i32)
|
||||
(local $n i32)
|
||||
(local.set $n
|
||||
(call $instance_param (i32.const 2) (i32.const 2) (i32.const 0) (i32.const 32)))
|
||||
(select (local.get $n) (i32.load (i32.const 0)) (i32.lt_s (local.get $n) (i32.const 0))))
|
||||
|
||||
;; Reports the length, for the cases where the value itself is not the point.
|
||||
(func (export "value_length") (result i32)
|
||||
(call $instance_param (i32.const 2) (i32.const 2) (i32.const 0) (i32.const 32)))
|
||||
|
||||
;; A negative index, which the shim refuses rather than casting.
|
||||
(func (export "negative_index") (result i32)
|
||||
(call $instance_param (i32.const -1) (i32.const 2) (i32.const 0) (i32.const 32))))
|
||||
)wat"};
|
||||
}
|
||||
};
|
||||
|
||||
TEST_F(InstanceParamCall, TheIndexAndTypeReachTheHostAndTheBytesComeBack)
|
||||
{
|
||||
EXPECT_CALL(host, instanceParam(2U, STI_UINT32))
|
||||
.WillOnce(Return(Bytes{0x0d, 0x0c, 0x0b, 0x0a}));
|
||||
|
||||
EXPECT_EQ(hostAnswer(), 0x0a0b0c0d) << "the value's bytes, read back little-endian";
|
||||
}
|
||||
|
||||
TEST_F(InstanceParamCall, TheAnswerIsTheValuesLength)
|
||||
{
|
||||
EXPECT_CALL(host, instanceParam).WillOnce(Return(Bytes{1, 2, 3, 4}));
|
||||
|
||||
EXPECT_EQ(hostAnswer("value_length"), 4);
|
||||
}
|
||||
|
||||
// A negative index is out of range, not a very large one: casting it would ask the host
|
||||
// about a parameter no contract could have meant.
|
||||
TEST_F(InstanceParamCall, ANegativeIndexIsRefusedWithoutAskingTheHost)
|
||||
{
|
||||
EXPECT_CALL(host, instanceParam).Times(0);
|
||||
|
||||
EXPECT_EQ(hostAnswer("negative_index"), hfErrorToInt(HostFunctionError::InvalidParams));
|
||||
}
|
||||
|
||||
TEST_F(InstanceParamCall, HostErrorBecomesContractReturnValue)
|
||||
{
|
||||
EXPECT_CALL(host, instanceParam)
|
||||
.WillOnce(Return(std::unexpected(HostFunctionError::IndexOutOfBounds)));
|
||||
|
||||
EXPECT_EQ(hostAnswer(), hfErrorToInt(HostFunctionError::IndexOutOfBounds));
|
||||
}
|
||||
|
||||
} // namespace xrpl::test
|
||||
@@ -0,0 +1,73 @@
|
||||
#include <xrpl/protocol/SField.h>
|
||||
#include <xrpl/tx/wasm/WasmCommon.h>
|
||||
|
||||
#include <gmock/gmock.h>
|
||||
#include <gtest/gtest.h>
|
||||
#include <tx/wasm/fixtures/ContractCallFixture.h>
|
||||
#include <tx/wasm/fixtures/MockHostFunctions.h>
|
||||
|
||||
#include <expected>
|
||||
#include <format>
|
||||
#include <string>
|
||||
|
||||
namespace xrpl::test {
|
||||
|
||||
using testing::Return;
|
||||
|
||||
// set_data_array_element_field — a key, a scalar index between the regions, and a value.
|
||||
struct SetDataArrayElementFieldCall : ContractCallTest
|
||||
{
|
||||
[[nodiscard]] std::string
|
||||
wat() const override
|
||||
{
|
||||
return std::format(
|
||||
R"wat(
|
||||
(module
|
||||
(import "host_lib" "set_data_array_element_field"
|
||||
(func $set_data_array_element_field (param i32 i32 i32 i32 i32 i32 i32) (result i32)))
|
||||
(memory (export "memory") 1)
|
||||
(data (i32.const 0) "{}")
|
||||
(data (i32.const 32) "amount")
|
||||
(data (i32.const 64) "\10\2a")
|
||||
|
||||
(func (export "escrow_finish") (result i32)
|
||||
(call $set_data_array_element_field
|
||||
(i32.const 0) (i32.const 20) (i32.const 32) (i32.const 6) (i32.const 7)
|
||||
(i32.const 64) (i32.const 2)))
|
||||
|
||||
(func (export "negative_index") (result i32)
|
||||
(call $set_data_array_element_field
|
||||
(i32.const 0) (i32.const 20) (i32.const 32) (i32.const 6) (i32.const -1)
|
||||
(i32.const 64) (i32.const 2))))
|
||||
)wat",
|
||||
escapedAccount());
|
||||
}
|
||||
};
|
||||
|
||||
TEST_F(SetDataArrayElementFieldCall, TheKeyTheIndexAndTheValueAllArrive)
|
||||
{
|
||||
EXPECT_CALL(
|
||||
host,
|
||||
setDataArrayElementField(
|
||||
AccountIs(account()), 7U, BytesAre("amount"), JsonValueIs(STI_UINT8, Bytes{0x2a})))
|
||||
.WillOnce(Return(0));
|
||||
|
||||
EXPECT_EQ(hostAnswer(), 0);
|
||||
}
|
||||
|
||||
TEST_F(SetDataArrayElementFieldCall, ANegativeIndexIsRefusedWithoutAskingTheHost)
|
||||
{
|
||||
EXPECT_CALL(host, setDataArrayElementField).Times(0);
|
||||
|
||||
EXPECT_EQ(hostAnswer("negative_index"), hfErrorToInt(HostFunctionError::IndexOutOfBounds));
|
||||
}
|
||||
|
||||
TEST_F(SetDataArrayElementFieldCall, HostErrorBecomesContractReturnValue)
|
||||
{
|
||||
EXPECT_CALL(host, setDataArrayElementField)
|
||||
.WillOnce(Return(std::unexpected(HostFunctionError::InvalidState)));
|
||||
|
||||
EXPECT_EQ(hostAnswer(), hfErrorToInt(HostFunctionError::InvalidState));
|
||||
}
|
||||
|
||||
} // namespace xrpl::test
|
||||
@@ -0,0 +1,68 @@
|
||||
#include <xrpl/protocol/SField.h>
|
||||
#include <xrpl/tx/wasm/WasmCommon.h>
|
||||
|
||||
#include <gmock/gmock.h>
|
||||
#include <gtest/gtest.h>
|
||||
#include <tx/wasm/fixtures/ContractCallFixture.h>
|
||||
#include <tx/wasm/fixtures/MockHostFunctions.h>
|
||||
|
||||
#include <expected>
|
||||
#include <format>
|
||||
#include <string>
|
||||
|
||||
namespace xrpl::test {
|
||||
|
||||
using testing::Return;
|
||||
|
||||
// set_data_nested_array_element_field — every kind of argument the ABI has in one call: an
|
||||
// account, two string keys, a scalar index between them, and a typed value.
|
||||
struct SetDataNestedArrayElementFieldCall : ContractCallTest
|
||||
{
|
||||
[[nodiscard]] std::string
|
||||
wat() const override
|
||||
{
|
||||
return std::format(
|
||||
R"wat(
|
||||
(module
|
||||
(import "host_lib" "set_data_nested_array_element_field"
|
||||
(func $set_data_nested_array_element_field
|
||||
(param i32 i32 i32 i32 i32 i32 i32 i32 i32) (result i32)))
|
||||
(memory (export "memory") 1)
|
||||
(data (i32.const 0) "{}")
|
||||
(data (i32.const 32) "items")
|
||||
(data (i32.const 48) "price")
|
||||
(data (i32.const 64) "\10\2a")
|
||||
|
||||
(func (export "escrow_finish") (result i32)
|
||||
(call $set_data_nested_array_element_field
|
||||
(i32.const 0) (i32.const 20) (i32.const 32) (i32.const 5) (i32.const 7)
|
||||
(i32.const 48) (i32.const 5) (i32.const 64) (i32.const 2))))
|
||||
)wat",
|
||||
escapedAccount());
|
||||
}
|
||||
};
|
||||
|
||||
TEST_F(SetDataNestedArrayElementFieldCall, EveryArgumentArrivesInItsOwnPlace)
|
||||
{
|
||||
EXPECT_CALL(
|
||||
host,
|
||||
setDataNestedArrayElementField(
|
||||
AccountIs(account()),
|
||||
BytesAre("items"),
|
||||
7U,
|
||||
BytesAre("price"),
|
||||
JsonValueIs(STI_UINT8, Bytes{0x2a})))
|
||||
.WillOnce(Return(0));
|
||||
|
||||
EXPECT_EQ(hostAnswer(), 0);
|
||||
}
|
||||
|
||||
TEST_F(SetDataNestedArrayElementFieldCall, HostErrorBecomesContractReturnValue)
|
||||
{
|
||||
EXPECT_CALL(host, setDataNestedArrayElementField)
|
||||
.WillOnce(Return(std::unexpected(HostFunctionError::InvalidState)));
|
||||
|
||||
EXPECT_EQ(hostAnswer(), hfErrorToInt(HostFunctionError::InvalidState));
|
||||
}
|
||||
|
||||
} // namespace xrpl::test
|
||||
@@ -0,0 +1,68 @@
|
||||
#include <xrpl/protocol/SField.h>
|
||||
#include <xrpl/tx/wasm/WasmCommon.h>
|
||||
|
||||
#include <gmock/gmock.h>
|
||||
#include <gtest/gtest.h>
|
||||
#include <tx/wasm/fixtures/ContractCallFixture.h>
|
||||
#include <tx/wasm/fixtures/MockHostFunctions.h>
|
||||
|
||||
#include <expected>
|
||||
#include <format>
|
||||
#include <string>
|
||||
|
||||
namespace xrpl::test {
|
||||
|
||||
using testing::Return;
|
||||
|
||||
// set_data_nested_object_field — two string keys and a typed value.
|
||||
//
|
||||
// The value's type byte decides what the shim builds, and the outer key comes first, so
|
||||
// both are asserted against something the other could not be.
|
||||
struct SetDataNestedObjectFieldCall : ContractCallTest
|
||||
{
|
||||
[[nodiscard]] std::string
|
||||
wat() const override
|
||||
{
|
||||
return std::format(
|
||||
R"wat(
|
||||
(module
|
||||
(import "host_lib" "set_data_nested_object_field"
|
||||
(func $set_data_nested_object_field (param i32 i32 i32 i32 i32 i32 i32 i32) (result i32)))
|
||||
(memory (export "memory") 1)
|
||||
(data (i32.const 0) "{}")
|
||||
(data (i32.const 32) "stats")
|
||||
(data (i32.const 48) "score")
|
||||
(data (i32.const 64) "\02\00\00\27\0f")
|
||||
|
||||
(func (export "escrow_finish") (result i32)
|
||||
(call $set_data_nested_object_field
|
||||
(i32.const 0) (i32.const 20) (i32.const 32) (i32.const 5) (i32.const 48) (i32.const 5)
|
||||
(i32.const 64) (i32.const 5))))
|
||||
)wat",
|
||||
escapedAccount());
|
||||
}
|
||||
};
|
||||
|
||||
TEST_F(SetDataNestedObjectFieldCall, TheOuterKeyComesFirstAndTheValueIsDecoded)
|
||||
{
|
||||
EXPECT_CALL(
|
||||
host,
|
||||
setDataNestedObjectField(
|
||||
AccountIs(account()),
|
||||
BytesAre("stats"),
|
||||
BytesAre("score"),
|
||||
JsonValueIs(STI_UINT32, (Bytes{0x00, 0x00, 0x27, 0x0f}))))
|
||||
.WillOnce(Return(0));
|
||||
|
||||
EXPECT_EQ(hostAnswer(), 0);
|
||||
}
|
||||
|
||||
TEST_F(SetDataNestedObjectFieldCall, HostErrorBecomesContractReturnValue)
|
||||
{
|
||||
EXPECT_CALL(host, setDataNestedObjectField)
|
||||
.WillOnce(Return(std::unexpected(HostFunctionError::InvalidState)));
|
||||
|
||||
EXPECT_EQ(hostAnswer(), hfErrorToInt(HostFunctionError::InvalidState));
|
||||
}
|
||||
|
||||
} // namespace xrpl::test
|
||||
Reference in New Issue
Block a user