mirror of
https://github.com/XRPLF/rippled.git
synced 2026-09-28 07:48:01 +00:00
Test the data-shaped and transaction-shaped contract host functions
Completes the `host_functions/` layer: reads, nested objects, arrays, nested arrays, and the two emit paths, each against a real ledger. What the shapes turn out to mean, now asserted rather than assumed: - An account has one data object, and the first write decides whether it is a map of keys or a list of elements. Mixing them is refused rather than silently replacing one with the other. - The two string arguments of a nested write are the outer key then the inner one. Reading under the swapped pair finds nothing, which is what a contract that got the order wrong would see. - Two arrays under two keys are two arrays. - A stored integer reads back big-endian, the opposite of the little-endian a parameter answers with. Two more defects are pinned rather than fixed, both the same shape as the one in the previous commit: reconstructing an `STTx` re-runs its format check, and the throw lands outside the guard that would have answered `SubmitTxnFailure`. `emit_txn` cannot emit a transaction that does not already carry `tfInnerBatchTxn`, because setting the flag means rebuilding it, and an ordinary Payment's defaulted `sfPaths` is rejected on the way back in. The contract is told `InternalFatal` and the run stops. `ContractLedger` now gives each host a transaction of its own; two identical ones share an id, which a ledger refuses to record twice.
This commit is contained in:
@@ -79,6 +79,7 @@ ContractLedger::makeContractHost(beast::Journal journal, ContractHostOptions opt
|
||||
obj.setAccountID(sfContractAccount, contractAccount);
|
||||
obj.setFieldVL(sfFunctionName, Blob{'c', 'a', 'l', 'l'});
|
||||
obj.setFieldU32(sfGas, 1'000'000);
|
||||
obj.setFieldU32(sfSequence, callSequence_++);
|
||||
});
|
||||
|
||||
auto context = std::make_unique<ApplyContext>(
|
||||
|
||||
@@ -111,6 +111,11 @@ public:
|
||||
private:
|
||||
ContractHost
|
||||
makeContractHost(beast::Journal journal, ContractHostOptions options);
|
||||
|
||||
// Every host gets a transaction of its own. Two identical ones would share an id, and a
|
||||
// ledger refuses to record the same transaction twice — which a test that finalizes more
|
||||
// than once would otherwise hit.
|
||||
std::uint32_t callSequence_{1};
|
||||
};
|
||||
|
||||
// -------------------------------------------------------------------------------------
|
||||
|
||||
109
src/tests/libxrpl/tx/wasm/host_functions/EmitTxn.cpp
Normal file
109
src/tests/libxrpl/tx/wasm/host_functions/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 <gtest/gtest.h>
|
||||
#include <tx/wasm/fixtures/ContractHostFixture.h>
|
||||
|
||||
#include <cstdint>
|
||||
#include <memory>
|
||||
|
||||
namespace xrpl::test {
|
||||
|
||||
// emit_txn — a transaction the contract serialized itself, rather than one it built through
|
||||
// `build_txn` and `add_txn_field`.
|
||||
//
|
||||
// The two paths end in the same place, so what is tested here is the difference: a
|
||||
// transaction that arrives whole, with whatever the contract chose to put in it.
|
||||
struct EmitTxnImpl : ContractHostFixture
|
||||
{
|
||||
Account const alice = fund("alice");
|
||||
Account const contract = fund("contract", XRP(2000));
|
||||
Account const carol = fund("carol");
|
||||
|
||||
ContractHost
|
||||
host()
|
||||
{
|
||||
return makeContractHost({.contractAccount = contract.id(), .caller = alice.id()});
|
||||
}
|
||||
|
||||
// A payment from the contract, assembled the way a contract's own SDK would assemble one.
|
||||
std::shared_ptr<STTx const>
|
||||
payment(STAmount amount, std::uint32_t sequence, bool innerFlag = true)
|
||||
{
|
||||
return std::make_shared<STTx const>(ttPAYMENT, [&](STObject& obj) {
|
||||
obj.setAccountID(sfAccount, contract.id());
|
||||
obj.setAccountID(sfDestination, carol.id());
|
||||
obj.setFieldAmount(sfAmount, amount);
|
||||
obj.setFieldAmount(sfFee, STAmount{0});
|
||||
obj.setFieldU32(sfSequence, sequence);
|
||||
obj.setFieldVL(sfSigningPubKey, Blob{});
|
||||
if (innerFlag)
|
||||
obj.setFieldU32(sfFlags, tfInnerBatchTxn);
|
||||
});
|
||||
}
|
||||
|
||||
// The contract account's sequence, which an emitted transaction has to carry.
|
||||
std::uint32_t
|
||||
contractSequence()
|
||||
{
|
||||
return ledger.getOpenLedger().read(keylet::account(contract.id()))->getFieldU32(sfSequence);
|
||||
}
|
||||
};
|
||||
|
||||
TEST_F(EmitTxnImpl, APaymentTheLedgerAcceptsIsQueued)
|
||||
{
|
||||
auto const contractHost = host();
|
||||
|
||||
expectValue(contractHost->emitTxn(payment(XRP(192), contractSequence())), TERtoInt(tesSUCCESS));
|
||||
EXPECT_EQ(contractHost.context().result.emittedTxns.size(), 1U);
|
||||
}
|
||||
|
||||
// A transaction the contract did not mark as an inner one cannot be emitted at all.
|
||||
//
|
||||
// The host means to set the flag for it, and rebuilds the transaction to do so — but
|
||||
// rebuilding an `STTx` from its own fields re-runs the format check, which rejects a
|
||||
// defaulted `sfPaths` that the original carried harmlessly. The contract is told
|
||||
// `InternalFatal`, so the run stops and the transaction reports `tecINTERNAL`.
|
||||
//
|
||||
// This pins what happens today. A contract's own choice reading as a node fault belongs
|
||||
// with the same fix as `EmitBuiltTxnImpl.ATransactionMissingARequiredFieldStopsTheRun`.
|
||||
TEST_F(EmitTxnImpl, ATransactionWithoutTheInnerFlagStopsTheRun)
|
||||
{
|
||||
auto const contractHost = host();
|
||||
|
||||
expectError(
|
||||
contractHost->emitTxn(payment(XRP(192), contractSequence(), false)),
|
||||
HostFunctionError::InternalFatal);
|
||||
EXPECT_TRUE(contractHost.context().result.emittedTxns.empty());
|
||||
}
|
||||
|
||||
// One the contract did mark keeps the flag, which is what the transactor reads to know it is
|
||||
// applying an inner transaction.
|
||||
TEST_F(EmitTxnImpl, AnEmittedTransactionCarriesTheInnerFlag)
|
||||
{
|
||||
auto const contractHost = host();
|
||||
|
||||
ASSERT_TRUE(contractHost->emitTxn(payment(XRP(192), contractSequence())));
|
||||
|
||||
ASSERT_EQ(contractHost.context().result.emittedTxns.size(), 1U);
|
||||
EXPECT_TRUE(contractHost.context().result.emittedTxns.front()->isFlag(tfInnerBatchTxn));
|
||||
}
|
||||
|
||||
// The sequence is the contract's own, and the ledger checks it like any other: a stale one
|
||||
// is refused with the code that says so.
|
||||
TEST_F(EmitTxnImpl, AStaleSequenceIsRefused)
|
||||
{
|
||||
auto const contractHost = host();
|
||||
|
||||
auto const result = contractHost->emitTxn(payment(XRP(1), contractSequence() - 1));
|
||||
ASSERT_TRUE(result.has_value());
|
||||
EXPECT_EQ(*result, TERtoInt(tefPAST_SEQ));
|
||||
EXPECT_TRUE(contractHost.context().result.emittedTxns.empty());
|
||||
}
|
||||
|
||||
} // namespace xrpl::test
|
||||
@@ -0,0 +1,90 @@
|
||||
#include <xrpl/protocol/STJson.h>
|
||||
#include <xrpl/tx/wasm/WasmCommon.h>
|
||||
|
||||
#include <gtest/gtest.h>
|
||||
#include <tx/wasm/fixtures/ContractHostFixture.h>
|
||||
|
||||
namespace xrpl::test {
|
||||
|
||||
// get_data_object_field — reading one key out of an account's data object.
|
||||
//
|
||||
// What it answers is the value's canonical serialization with no type byte: the contract
|
||||
// asked for a key, and the type is the one the value was stored as.
|
||||
struct GetDataObjectFieldImpl : ContractHostFixture
|
||||
{
|
||||
Account const alice = fund("alice");
|
||||
Account const contract = fund("contract");
|
||||
|
||||
ContractHost
|
||||
host()
|
||||
{
|
||||
return makeContractHost({.contractAccount = contract.id(), .caller = alice.id()});
|
||||
}
|
||||
|
||||
// An account whose data object is already on the ledger, so a read has something to find
|
||||
// without a write in the same run.
|
||||
void
|
||||
store(std::string const& key, STJson::Value const& value)
|
||||
{
|
||||
auto const contractHost = host();
|
||||
ASSERT_TRUE(contractHost->setDataObjectField(alice.id(), key, value));
|
||||
ASSERT_EQ(contractHost.finalize(), tesSUCCESS);
|
||||
}
|
||||
};
|
||||
|
||||
TEST_F(GetDataObjectFieldImpl, EveryWidthComesBackAsItsCanonicalBytes)
|
||||
{
|
||||
store("value_u8", u8(42));
|
||||
store("value_u16", u16(1234));
|
||||
store("count", u32(3));
|
||||
store("total", u64(9'876'543'210));
|
||||
|
||||
auto const contractHost = host();
|
||||
expectValue(contractHost->getDataObjectField(alice.id(), "value_u8"), serialization(u8(42)));
|
||||
expectValue(
|
||||
contractHost->getDataObjectField(alice.id(), "value_u16"), serialization(u16(1234)));
|
||||
expectValue(contractHost->getDataObjectField(alice.id(), "count"), serialization(u32(3)));
|
||||
expectValue(
|
||||
contractHost->getDataObjectField(alice.id(), "total"), serialization(u64(9'876'543'210)));
|
||||
}
|
||||
|
||||
// An integer's canonical serialization is big-endian, which is the opposite of what
|
||||
// `instance_param` answers with. The two live side by side in the ABI, so this is asserted
|
||||
// against the bytes rather than against a helper that could share the mistake.
|
||||
TEST_F(GetDataObjectFieldImpl, AnIntegerComesBackBigEndian)
|
||||
{
|
||||
store("count", u32(0x01020304));
|
||||
|
||||
expectValue(host()->getDataObjectField(alice.id(), "count"), (Bytes{0x01, 0x02, 0x03, 0x04}));
|
||||
}
|
||||
|
||||
TEST_F(GetDataObjectFieldImpl, AnAccountIsItsLengthAndItsTwentyBytes)
|
||||
{
|
||||
store("owner", acct(contract.id()));
|
||||
|
||||
auto const expected = serialization(acct(contract.id()));
|
||||
ASSERT_EQ(expected.size(), 21U) << "an account field carries its length";
|
||||
expectValue(host()->getDataObjectField(alice.id(), "owner"), expected);
|
||||
}
|
||||
|
||||
TEST_F(GetDataObjectFieldImpl, AnAccountWithNoDataObjectHasNothingToRead)
|
||||
{
|
||||
expectError(
|
||||
host()->getDataObjectField(alice.id(), "count"), HostFunctionError::LedgerObjNotFound);
|
||||
}
|
||||
|
||||
TEST_F(GetDataObjectFieldImpl, AKeyThatIsNotThereIsNotAField)
|
||||
{
|
||||
store("count", u32(3));
|
||||
|
||||
expectError(host()->getDataObjectField(alice.id(), "absent"), HostFunctionError::InvalidField);
|
||||
}
|
||||
|
||||
TEST_F(GetDataObjectFieldImpl, AnAccountThatDoesNotExistIsRefusedBeforeItsDataIsLookedFor)
|
||||
{
|
||||
expectError(
|
||||
host()->getDataObjectField(Account{"ghost"}.id(), "count"),
|
||||
HostFunctionError::InvalidAccount);
|
||||
}
|
||||
|
||||
} // namespace xrpl::test
|
||||
@@ -0,0 +1,100 @@
|
||||
#include <xrpl/protocol/STJson.h>
|
||||
#include <xrpl/tx/wasm/WasmCommon.h>
|
||||
|
||||
#include <gtest/gtest.h>
|
||||
#include <tx/wasm/fixtures/ContractHostFixture.h>
|
||||
|
||||
namespace xrpl::test {
|
||||
|
||||
// set_data_array_element_field and its reader — an account's data object *as an array*.
|
||||
//
|
||||
// An account has one data object, and it is either a map of keys or a list of elements. The
|
||||
// first array write decides which, and there is no going back inside a run.
|
||||
struct SetDataArrayElementFieldImpl : ContractHostFixture
|
||||
{
|
||||
Account const alice = fund("alice");
|
||||
Account const contract = fund("contract");
|
||||
|
||||
ContractHost
|
||||
host()
|
||||
{
|
||||
return makeContractHost({.contractAccount = contract.id(), .caller = alice.id()});
|
||||
}
|
||||
};
|
||||
|
||||
TEST_F(SetDataArrayElementFieldImpl, EachElementKeepsItsOwnValue)
|
||||
{
|
||||
auto const contractHost = host();
|
||||
|
||||
expectValue(contractHost->setDataArrayElementField(alice.id(), 0, "amount", u32(10)), 0);
|
||||
expectValue(contractHost->setDataArrayElementField(alice.id(), 1, "amount", u32(20)), 0);
|
||||
|
||||
expectValue(
|
||||
contractHost->getDataArrayElementField(alice.id(), 0, "amount"), serialization(u32(10)));
|
||||
expectValue(
|
||||
contractHost->getDataArrayElementField(alice.id(), 1, "amount"), serialization(u32(20)));
|
||||
}
|
||||
|
||||
TEST_F(SetDataArrayElementFieldImpl, OneElementHoldsSeveralKeys)
|
||||
{
|
||||
auto const contractHost = host();
|
||||
ASSERT_TRUE(contractHost->setDataArrayElementField(alice.id(), 0, "amount", u32(10)));
|
||||
ASSERT_TRUE(contractHost->setDataArrayElementField(alice.id(), 0, "kind", u8(2)));
|
||||
|
||||
expectValue(
|
||||
contractHost->getDataArrayElementField(alice.id(), 0, "amount"), serialization(u32(10)));
|
||||
expectValue(
|
||||
contractHost->getDataArrayElementField(alice.id(), 0, "kind"), serialization(u8(2)));
|
||||
}
|
||||
|
||||
TEST_F(SetDataArrayElementFieldImpl, WhatIsFinalizedIsVisibleToTheNextCall)
|
||||
{
|
||||
{
|
||||
auto const contractHost = host();
|
||||
ASSERT_TRUE(contractHost->setDataArrayElementField(alice.id(), 0, "amount", u32(10)));
|
||||
ASSERT_EQ(contractHost.finalize(), tesSUCCESS);
|
||||
}
|
||||
|
||||
expectValue(host()->getDataArrayElementField(alice.id(), 0, "amount"), serialization(u32(10)));
|
||||
}
|
||||
|
||||
// An object is not an array. A contract that has written a key cannot then write an element,
|
||||
// and is told so rather than having its object replaced by a list.
|
||||
TEST_F(SetDataArrayElementFieldImpl, AnObjectIsNotAnArray)
|
||||
{
|
||||
auto const contractHost = host();
|
||||
ASSERT_TRUE(contractHost->setDataObjectField(alice.id(), "count", u32(1)));
|
||||
|
||||
expectError(
|
||||
contractHost->setDataArrayElementField(alice.id(), 0, "amount", u32(10)),
|
||||
HostFunctionError::InvalidState);
|
||||
}
|
||||
|
||||
TEST_F(SetDataArrayElementFieldImpl, AnElementThatWasNeverWrittenIsNotThere)
|
||||
{
|
||||
auto const contractHost = host();
|
||||
ASSERT_TRUE(contractHost->setDataArrayElementField(alice.id(), 0, "amount", u32(10)));
|
||||
|
||||
expectError(
|
||||
contractHost->getDataArrayElementField(alice.id(), 1, "amount"),
|
||||
HostFunctionError::InvalidField);
|
||||
}
|
||||
|
||||
TEST_F(SetDataArrayElementFieldImpl, AKeyThatIsNotInTheElementIsNotAField)
|
||||
{
|
||||
auto const contractHost = host();
|
||||
ASSERT_TRUE(contractHost->setDataArrayElementField(alice.id(), 0, "amount", u32(10)));
|
||||
|
||||
expectError(
|
||||
contractHost->getDataArrayElementField(alice.id(), 0, "absent"),
|
||||
HostFunctionError::InvalidField);
|
||||
}
|
||||
|
||||
TEST_F(SetDataArrayElementFieldImpl, AnAccountThatDoesNotExistCannotBeWrittenTo)
|
||||
{
|
||||
expectError(
|
||||
host()->setDataArrayElementField(Account{"ghost"}.id(), 0, "amount", u32(1)),
|
||||
HostFunctionError::InvalidAccount);
|
||||
}
|
||||
|
||||
} // namespace xrpl::test
|
||||
@@ -0,0 +1,117 @@
|
||||
#include <xrpl/protocol/STJson.h>
|
||||
#include <xrpl/tx/wasm/WasmCommon.h>
|
||||
|
||||
#include <gtest/gtest.h>
|
||||
#include <tx/wasm/fixtures/ContractHostFixture.h>
|
||||
|
||||
namespace xrpl::test {
|
||||
|
||||
// set_data_nested_array_element_field and its reader — an array held under a key of an
|
||||
// account's data object, rather than the object itself being one.
|
||||
//
|
||||
// The arguments are the outer key, the element index, and the key inside that element: the
|
||||
// index sits between the two strings on the wire, which is the shape nothing else has.
|
||||
struct SetDataNestedArrayElementFieldImpl : ContractHostFixture
|
||||
{
|
||||
Account const alice = fund("alice");
|
||||
Account const contract = fund("contract");
|
||||
|
||||
ContractHost
|
||||
host()
|
||||
{
|
||||
return makeContractHost({.contractAccount = contract.id(), .caller = alice.id()});
|
||||
}
|
||||
};
|
||||
|
||||
TEST_F(SetDataNestedArrayElementFieldImpl, EachElementKeepsItsOwnValue)
|
||||
{
|
||||
auto const contractHost = host();
|
||||
|
||||
expectValue(
|
||||
contractHost->setDataNestedArrayElementField(alice.id(), "items", 0, "id", u32(55)), 0);
|
||||
expectValue(
|
||||
contractHost->setDataNestedArrayElementField(alice.id(), "items", 1, "id", u32(77)), 0);
|
||||
|
||||
expectValue(
|
||||
contractHost->getDataNestedArrayElementField(alice.id(), "items", 0, "id"),
|
||||
serialization(u32(55)));
|
||||
expectValue(
|
||||
contractHost->getDataNestedArrayElementField(alice.id(), "items", 1, "id"),
|
||||
serialization(u32(77)));
|
||||
}
|
||||
|
||||
TEST_F(SetDataNestedArrayElementFieldImpl, OneElementHoldsSeveralKeys)
|
||||
{
|
||||
auto const contractHost = host();
|
||||
ASSERT_TRUE(
|
||||
contractHost->setDataNestedArrayElementField(alice.id(), "items", 0, "id", u32(55)));
|
||||
ASSERT_TRUE(
|
||||
contractHost->setDataNestedArrayElementField(alice.id(), "items", 0, "price", u32(12)));
|
||||
|
||||
expectValue(
|
||||
contractHost->getDataNestedArrayElementField(alice.id(), "items", 0, "price"),
|
||||
serialization(u32(12)));
|
||||
}
|
||||
|
||||
// Two arrays under two keys are two arrays: an element of one is not an element of the
|
||||
// other, which is what having the outer key on the wire is for.
|
||||
TEST_F(SetDataNestedArrayElementFieldImpl, ArraysUnderDifferentKeysAreSeparate)
|
||||
{
|
||||
auto const contractHost = host();
|
||||
ASSERT_TRUE(
|
||||
contractHost->setDataNestedArrayElementField(alice.id(), "items", 0, "id", u32(55)));
|
||||
ASSERT_TRUE(
|
||||
contractHost->setDataNestedArrayElementField(alice.id(), "orders", 0, "id", u32(77)));
|
||||
|
||||
expectValue(
|
||||
contractHost->getDataNestedArrayElementField(alice.id(), "items", 0, "id"),
|
||||
serialization(u32(55)));
|
||||
expectValue(
|
||||
contractHost->getDataNestedArrayElementField(alice.id(), "orders", 0, "id"),
|
||||
serialization(u32(77)));
|
||||
}
|
||||
|
||||
TEST_F(SetDataNestedArrayElementFieldImpl, WhatIsFinalizedIsVisibleToTheNextCall)
|
||||
{
|
||||
{
|
||||
auto const contractHost = host();
|
||||
ASSERT_TRUE(
|
||||
contractHost->setDataNestedArrayElementField(alice.id(), "items", 0, "id", u32(55)));
|
||||
ASSERT_EQ(contractHost.finalize(), tesSUCCESS);
|
||||
}
|
||||
|
||||
expectValue(
|
||||
host()->getDataNestedArrayElementField(alice.id(), "items", 0, "id"),
|
||||
serialization(u32(55)));
|
||||
}
|
||||
|
||||
// The data object holds the array, so the object itself may not be one.
|
||||
TEST_F(SetDataNestedArrayElementFieldImpl, AnArrayRootHasNoKeyToHoldAnArrayUnder)
|
||||
{
|
||||
auto const contractHost = host();
|
||||
ASSERT_TRUE(contractHost->setDataArrayElementField(alice.id(), 0, "first", u8(1)));
|
||||
|
||||
expectError(
|
||||
contractHost->setDataNestedArrayElementField(alice.id(), "items", 0, "id", u32(55)),
|
||||
HostFunctionError::InvalidState);
|
||||
}
|
||||
|
||||
TEST_F(SetDataNestedArrayElementFieldImpl, AnElementThatWasNeverWrittenIsNotThere)
|
||||
{
|
||||
auto const contractHost = host();
|
||||
ASSERT_TRUE(
|
||||
contractHost->setDataNestedArrayElementField(alice.id(), "items", 0, "id", u32(55)));
|
||||
|
||||
expectError(
|
||||
contractHost->getDataNestedArrayElementField(alice.id(), "items", 1, "id"),
|
||||
HostFunctionError::InvalidField);
|
||||
}
|
||||
|
||||
TEST_F(SetDataNestedArrayElementFieldImpl, AnAccountThatDoesNotExistCannotBeWrittenTo)
|
||||
{
|
||||
expectError(
|
||||
host()->setDataNestedArrayElementField(Account{"ghost"}.id(), "items", 0, "id", u32(1)),
|
||||
HostFunctionError::InvalidAccount);
|
||||
}
|
||||
|
||||
} // namespace xrpl::test
|
||||
@@ -0,0 +1,94 @@
|
||||
#include <xrpl/protocol/STJson.h>
|
||||
#include <xrpl/tx/wasm/WasmCommon.h>
|
||||
|
||||
#include <gtest/gtest.h>
|
||||
#include <tx/wasm/fixtures/ContractHostFixture.h>
|
||||
|
||||
namespace xrpl::test {
|
||||
|
||||
// set_data_nested_object_field and its reader — one level of nesting inside an account's
|
||||
// data object.
|
||||
//
|
||||
// The two string arguments are the outer key then the inner one, in that order on the wire.
|
||||
// A contract that swaps them writes somewhere else entirely, which is why the order is
|
||||
// asserted rather than assumed.
|
||||
struct SetDataNestedObjectFieldImpl : ContractHostFixture
|
||||
{
|
||||
Account const alice = fund("alice");
|
||||
Account const contract = fund("contract");
|
||||
|
||||
ContractHost
|
||||
host()
|
||||
{
|
||||
return makeContractHost({.contractAccount = contract.id(), .caller = alice.id()});
|
||||
}
|
||||
};
|
||||
|
||||
TEST_F(SetDataNestedObjectFieldImpl, WhatIsStoredIsWhatIsReadBack)
|
||||
{
|
||||
auto const contractHost = host();
|
||||
|
||||
expectValue(contractHost->setDataNestedObjectField(alice.id(), "stats", "score", u32(9999)), 0);
|
||||
|
||||
expectValue(
|
||||
contractHost->getDataNestedObjectField(alice.id(), "stats", "score"),
|
||||
serialization(u32(9999)));
|
||||
}
|
||||
|
||||
// The outer key comes first. Read under the swapped pair and there is nothing there, which
|
||||
// is what a contract that got the order wrong would see.
|
||||
TEST_F(SetDataNestedObjectFieldImpl, TheOuterKeyIsTheFirstOne)
|
||||
{
|
||||
auto const contractHost = host();
|
||||
ASSERT_TRUE(contractHost->setDataNestedObjectField(alice.id(), "stats", "score", u32(1)));
|
||||
|
||||
expectError(
|
||||
contractHost->getDataNestedObjectField(alice.id(), "score", "stats"),
|
||||
HostFunctionError::InvalidField);
|
||||
}
|
||||
|
||||
TEST_F(SetDataNestedObjectFieldImpl, OneObjectHoldsSeveralKeys)
|
||||
{
|
||||
auto const contractHost = host();
|
||||
ASSERT_TRUE(contractHost->setDataNestedObjectField(alice.id(), "stats", "score", u32(1)));
|
||||
ASSERT_TRUE(contractHost->setDataNestedObjectField(alice.id(), "stats", "level", u32(2)));
|
||||
|
||||
expectValue(
|
||||
contractHost->getDataNestedObjectField(alice.id(), "stats", "score"),
|
||||
serialization(u32(1)));
|
||||
expectValue(
|
||||
contractHost->getDataNestedObjectField(alice.id(), "stats", "level"),
|
||||
serialization(u32(2)));
|
||||
}
|
||||
|
||||
TEST_F(SetDataNestedObjectFieldImpl, WhatIsFinalizedIsVisibleToTheNextCall)
|
||||
{
|
||||
{
|
||||
auto const contractHost = host();
|
||||
ASSERT_TRUE(
|
||||
contractHost->setDataNestedObjectField(alice.id(), "stats", "score", u32(9999)));
|
||||
ASSERT_EQ(contractHost.finalize(), tesSUCCESS);
|
||||
}
|
||||
|
||||
expectValue(
|
||||
host()->getDataNestedObjectField(alice.id(), "stats", "score"), serialization(u32(9999)));
|
||||
}
|
||||
|
||||
TEST_F(SetDataNestedObjectFieldImpl, AnArrayIsNotAnObject)
|
||||
{
|
||||
auto const contractHost = host();
|
||||
ASSERT_TRUE(contractHost->setDataArrayElementField(alice.id(), 0, "first", u8(1)));
|
||||
|
||||
expectError(
|
||||
contractHost->setDataNestedObjectField(alice.id(), "stats", "score", u32(1)),
|
||||
HostFunctionError::InvalidState);
|
||||
}
|
||||
|
||||
TEST_F(SetDataNestedObjectFieldImpl, AnAccountThatDoesNotExistCannotBeWrittenTo)
|
||||
{
|
||||
expectError(
|
||||
host()->setDataNestedObjectField(Account{"ghost"}.id(), "stats", "score", u32(1)),
|
||||
HostFunctionError::InvalidAccount);
|
||||
}
|
||||
|
||||
} // namespace xrpl::test
|
||||
Reference in New Issue
Block a user