diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 0000000000..24620f8d7c --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1,2 @@ +# Allow anyone to review any change by default. +* diff --git a/cspell.config.yaml b/cspell.config.yaml index 6eef313efb..dfe6f1be61 100644 --- a/cspell.config.yaml +++ b/cspell.config.yaml @@ -99,6 +99,7 @@ words: - distro - doxyfile - dxrpl + - emittance - endmacro - exceptioned - Falco @@ -248,6 +249,7 @@ words: - statsd - STATSDCOLLECTOR - stissue + - stjson - stnum - stobj - stobject diff --git a/include/xrpl/ledger/OpenViewSandbox.h b/include/xrpl/ledger/OpenViewSandbox.h new file mode 100644 index 0000000000..3fd4073e12 --- /dev/null +++ b/include/xrpl/ledger/OpenViewSandbox.h @@ -0,0 +1,82 @@ +#pragma once + +#include + +#include + +namespace xrpl { + +class OpenViewSandbox +{ +private: + OpenView& parent_; + std::unique_ptr sandbox_; + +public: + using key_type = ReadView::key_type; + + OpenViewSandbox(OpenView& parent) + : parent_(parent), sandbox_(std::make_unique(batch_view, parent)) + { + } + + void + rawErase(std::shared_ptr const& sle) + { + sandbox_->rawErase(sle); + } + + void + rawInsert(std::shared_ptr const& sle) + { + sandbox_->rawInsert(sle); + } + + void + rawReplace(std::shared_ptr const& sle) + { + sandbox_->rawReplace(sle); + } + + void + rawDestroyXRP(XRPAmount const& fee) + { + sandbox_->rawDestroyXRP(fee); + } + + void + rawTxInsert( + key_type const& key, + std::shared_ptr const& txn, + std::shared_ptr const& metaData) + { + sandbox_->rawTxInsert(key, txn, metaData); + } + + void + commit() + { + sandbox_->apply(parent_); + sandbox_ = std::make_unique(batch_view, parent_); + } + + void + discard() + { + sandbox_ = std::make_unique(batch_view, parent_); + } + + OpenView const& + view() const + { + return *sandbox_; + } + + OpenView& + view() + { + return *sandbox_; + } +}; + +} // namespace xrpl diff --git a/include/xrpl/ledger/View.h b/include/xrpl/ledger/View.h index 55be01d677..d3996e2956 100644 --- a/include/xrpl/ledger/View.h +++ b/include/xrpl/ledger/View.h @@ -3,6 +3,10 @@ #include #include #include +#include +#include +#include +#include #include #include #include @@ -211,6 +215,27 @@ doWithdraw( STAmount const& amount, beast::Journal j); +enum class SendIssuerHandling { ihSENDER_NOT_ALLOWED, ihRECEIVER_NOT_ALLOWED, ihIGNORE }; +enum class SendEscrowHandling { ehIGNORE, ehCHECK }; +enum class SendAuthHandling { ahCHECK_SENDER, ahCHECK_RECEIVER, ahBOTH, ahNEITHER }; +enum class SendFreezeHandling { fhCHECK_SENDER, fhCHECK_RECEIVER, fhBOTH, fhNEITHER }; +enum class SendTransferHandling { thIGNORE, thCHECK }; +enum class SendBalanceHandling { bhIGNORE, bhCHECK }; + +TER +canTransferFT( + ReadView const& view, + AccountID const& sender, + AccountID const& receiver, + STAmount const& amount, + beast::Journal j, + SendIssuerHandling issuerHandling, + SendEscrowHandling escrowHandling, + SendAuthHandling authHandling, + SendFreezeHandling freezeHandling, + SendTransferHandling transferHandling, + SendBalanceHandling balanceHandling); + /** Deleter function prototype. Returns the status of the entry deletion * (if should not be skipped) and if the entry should be skipped. The status * is always tesSUCCESS if the entry should be skipped. diff --git a/include/xrpl/ledger/helpers/ContractUtils.h b/include/xrpl/ledger/helpers/ContractUtils.h new file mode 100644 index 0000000000..3e478ad43d --- /dev/null +++ b/include/xrpl/ledger/helpers/ContractUtils.h @@ -0,0 +1,102 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +namespace xrpl { + +class ContractDataMap : public std::map> +{ +public: + uint32_t modifiedCount = 0; +}; + +class ContractEventMap : public std::map +{ +}; + +namespace contract { + +/** The maximum number of data modifications in a single function. */ +int64_t constexpr maxDataModifications = 1000; + +/** The maximum number of bytes the data can occupy. */ +int64_t constexpr maxContractDataSize = 1024; + +/** The multiplier for contract data size calculations. */ +int64_t constexpr dataByteMultiplier = 512; + +/** The cost multiplier of creating a contract in bytes. */ +int64_t constexpr createByteMultiplier = 500ULL; + +/** The value to return when the fee calculation failed. */ +int64_t constexpr feeCalculationFailed = 0x7FFFFFFFFFFFFFFFLL; + +/** The maximum number of contract parameters that can be in a transaction. */ +std::size_t constexpr maxContractParams = 8; + +/** The maximum number of contract functions that can be in a transaction. */ +std::size_t constexpr maxContractFunctions = 8; + +int64_t +contractCreateFee(uint64_t byteCount); + +NotTEC +preflightFunctions(STTx const& tx, beast::Journal j); + +NotTEC +preflightInstanceParameters(STTx const& tx, beast::Journal j); + +bool +validateParameterMapping(STArray const& params, STArray const& values, beast::Journal j); + +NotTEC +preflightInstanceParameterValues(STTx const& tx, beast::Journal j); + +NotTEC +preflightFlagParameters(STArray const& parameters, beast::Journal j); + +bool +isValidParameterFlag(std::uint32_t flags); + +TER +preclaimFlagParameters( + ReadView const& view, + AccountID const& sourceAccount, + AccountID const& contractAccount, + STArray const& parameters, + beast::Journal j); + +TER +doApplyFlagParameters( + ApplyView& view, + STTx const& tx, + AccountID const& sourceAccount, + AccountID const& contractAccount, + STArray const& parameters, + XRPAmount const& priorBalance, + beast::Journal j); + +TER +finalizeContractData( + ServiceRegistry& registry, + ApplyView& view, + AccountID const& contractAccount, + ContractDataMap const& dataMap, + ContractEventMap const& eventMap, + uint256 const& txnID); + +} // namespace contract +} // namespace xrpl diff --git a/include/xrpl/tx/transactors/nft/NFTokenUtils.h b/include/xrpl/ledger/helpers/NFTokenUtils.h similarity index 95% rename from include/xrpl/tx/transactors/nft/NFTokenUtils.h rename to include/xrpl/ledger/helpers/NFTokenUtils.h index 33aab068c6..bd7ef7760a 100644 --- a/include/xrpl/tx/transactors/nft/NFTokenUtils.h +++ b/include/xrpl/ledger/helpers/NFTokenUtils.h @@ -1,12 +1,12 @@ #pragma once #include +#include #include #include #include #include #include -#include namespace xrpl { @@ -140,6 +140,13 @@ checkTrustlineDeepFrozen( beast::Journal const j, Issue const& issue); +TER +transferNFToken( + ApplyView& view, + AccountID const& buyer, + AccountID const& seller, + uint256 const& nftokenID); + } // namespace nft } // namespace xrpl diff --git a/include/xrpl/protocol/Emitable.h b/include/xrpl/protocol/Emitable.h new file mode 100644 index 0000000000..6acce0d102 --- /dev/null +++ b/include/xrpl/protocol/Emitable.h @@ -0,0 +1,78 @@ +#pragma once + +#include +#include +#include + +#include +#include +#include + +namespace xrpl { +/** + * We have both transaction type emitables and granular type emitables. + * Since we will reuse the TransactionFormats to parse the Transaction + * Emitables, only the GranularEmitableType is defined here. To prevent + * conflicts with TxType, the GranularEmitableType is always set to a value + * greater than the maximum value of uint16. + */ +enum GranularEmitableType : std::uint32_t { +#pragma push_macro("EMITABLE") +#undef EMITABLE + +#define EMITABLE(type, txType, value) type = value, + +#include + +#undef EMITABLE +#pragma pop_macro("EMITABLE") +}; + +enum Emittance { emitable, notEmitable }; + +class Emitable +{ +private: + Emitable(); + + std::unordered_map emitableTx_; + + std::unordered_map granularEmitableMap_; + + std::unordered_map granularNameMap_; + + std::unordered_map granularTxTypeMap_; + +public: + static Emitable const& + getInstance(); + + Emitable(Emitable const&) = delete; + Emitable& + operator=(Emitable const&) = delete; + + std::optional + getEmitableName(std::uint32_t const value) const; + + std::optional + getGranularValue(std::string const& name) const; + + std::optional + getGranularName(GranularEmitableType const& value) const; + + std::optional + getGranularTxType(GranularEmitableType const& gpType) const; + + bool + isEmitable(std::uint32_t const& emitableValue) const; + + // for tx level emitable, emitable value is equal to tx type plus one + uint32_t + txToEmitableType(TxType const& type) const; + + // tx type value is emitable value minus one + TxType + emitableToTxType(uint32_t const& value) const; +}; + +} // namespace xrpl diff --git a/include/xrpl/protocol/Indexes.h b/include/xrpl/protocol/Indexes.h index a438a8a77b..4811460455 100644 --- a/include/xrpl/protocol/Indexes.h +++ b/include/xrpl/protocol/Indexes.h @@ -348,6 +348,22 @@ permissionedDomain(AccountID const& account, std::uint32_t seq) noexcept; Keylet permissionedDomain(uint256 const& domainID) noexcept; + +Keylet +contractSource(uint256 const& contractHash) noexcept; + +Keylet +contract(uint256 const& contractHash, AccountID const& owner, std::uint32_t seq) noexcept; + +inline Keylet +contract(uint256 const& contractID) +{ + return {ltCONTRACT, contractID}; +} + +Keylet +contractData(AccountID const& owner, AccountID const& contractAccount) noexcept; + } // namespace keylet // Everything below is deprecated and should be removed in favor of keylets: diff --git a/include/xrpl/protocol/LedgerFormats.h b/include/xrpl/protocol/LedgerFormats.h index a43f6a7134..6b61f4d33a 100644 --- a/include/xrpl/protocol/LedgerFormats.h +++ b/include/xrpl/protocol/LedgerFormats.h @@ -74,14 +74,6 @@ enum LedgerEntryType : std::uint16_t { */ ltNICKNAME [[deprecated("This object type is not supported and should not be used.")]] = 0x006e, - /** A legacy, deprecated type. - - \deprecated **This object type is not supported and should not be used.** - Support for this type of object was never implemented. - No objects of this type were ever created. - */ - ltCONTRACT [[deprecated("This object type is not supported and should not be used.")]] = 0x0063, - /** A legacy, deprecated type. \deprecated **This object type is not supported and should not be used.** diff --git a/include/xrpl/protocol/SField.h b/include/xrpl/protocol/SField.h index 7a864b1b58..a0de720ff9 100644 --- a/include/xrpl/protocol/SField.h +++ b/include/xrpl/protocol/SField.h @@ -33,6 +33,9 @@ class STNumber; class STXChainBridge; class STVector256; class STCurrency; +class STData; +class STDataType; +class STJson; #pragma push_macro("XMACRO") #undef XMACRO @@ -71,6 +74,9 @@ class STCurrency; STYPE(STI_ISSUE, 24) \ STYPE(STI_XCHAIN_BRIDGE, 25) \ STYPE(STI_CURRENCY, 26) \ + STYPE(STI_DATA, 27) \ + STYPE(STI_DATATYPE, 28) \ + STYPE(STI_JSON, 29) \ \ /* high-level types */ \ /* cannot be serialized inside other types */ \ @@ -350,6 +356,9 @@ using SF_NUMBER = TypedField; using SF_VL = TypedField; using SF_VECTOR256 = TypedField; using SF_XCHAIN_BRIDGE = TypedField; +using SF_DATA = TypedField; +using SF_DATATYPE = TypedField; +using SF_JSON = TypedField; //------------------------------------------------------------------------------ diff --git a/include/xrpl/protocol/STData.h b/include/xrpl/protocol/STData.h new file mode 100644 index 0000000000..78e3a6657f --- /dev/null +++ b/include/xrpl/protocol/STData.h @@ -0,0 +1,289 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +namespace xrpl { + +class STData final : public STBase +{ +private: + using data_type = detail::STVar; + std::uint16_t inner_type_; + data_type data_; + bool default_{true}; + +public: + using value_type = STData; // Although not directly holding a single value + + STData(SField const& n); + STData(SField const& n, unsigned char); + STData(SField const& n, std::uint16_t); + STData(SField const& n, std::uint32_t); + STData(SField const& n, std::uint64_t); + STData(SField const& n, uint128 const&); + STData(SField const& n, uint160 const&); + STData(SField const& n, uint192 const&); + STData(SField const& n, uint256 const&); + STData(SField const& n, Blob const&); + STData(SField const& n, Slice const&); + STData(SField const& n, AccountID const&); + STData(SField const& n, STAmount const&); + STData(SField const& n, STIssue const&); + STData(SField const& n, STCurrency const&); + STData(SField const& n, STNumber const&); + + STData(SerialIter& sit, SField const& name); + + std::size_t + size() const; + + SerializedTypeID + getSType() const override; + + std::string + getInnerTypeString() const; + + std::string + getText() const override; + + Json::Value getJson(JsonOptions) const override; + + void + add(Serializer& s) const override; + + bool + isEquivalent(STBase const& t) const override; + + bool + isDefault() const override; + + SerializedTypeID + getInnerSType() const noexcept; + + STBase* + makeFieldPresent(); + + void + setFieldU8(unsigned char); + void + setFieldU16(std::uint16_t); + void + setFieldU32(std::uint32_t); + void + setFieldU64(std::uint64_t); + void + setFieldH128(uint128 const&); + void + setFieldH160(uint160 const&); + void + setFieldH192(uint192 const&); + void + setFieldH256(uint256 const&); + void + setFieldVL(Blob const&); + void + setFieldVL(Slice const&); + void + setAccountID(AccountID const&); + void + setFieldAmount(STAmount const&); + void + setIssue(STIssue const&); + void + setCurrency(STCurrency const&); + void + setFieldNumber(STNumber const&); + + unsigned char + getFieldU8() const; + std::uint16_t + getFieldU16() const; + std::uint32_t + getFieldU32() const; + std::uint64_t + getFieldU64() const; + uint128 + getFieldH128() const; + uint160 + getFieldH160() const; + uint192 + getFieldH192() const; + uint256 + getFieldH256() const; + Blob + getFieldVL() const; + AccountID + getAccountID() const; + STAmount const& + getFieldAmount() const; + STIssue + getFieldIssue() const; + STCurrency + getFieldCurrency() const; + STNumber + getFieldNumber() const; + +private: + STBase* + copy(std::size_t n, void* buf) const override; + STBase* + move(std::size_t n, void* buf) override; + + friend class detail::STVar; + + // Implementation for getting (most) fields that return by value. + // + // The remove_cv and remove_reference are necessitated by the STBitString + // types. Their value() returns by const ref. We return those types + // by value. + template < + typename T, + typename V = typename std::remove_cv< + typename std::remove_reference().value())>::type>::type> + V + getFieldByValue() const; + + // Implementations for getting (most) fields that return by const reference. + // + // If an absent optional field is deserialized we don't have anything + // obvious to return. So we insist on having the call provide an + // 'empty' value we return in that circumstance. + template + V const& + getFieldByConstRef(V const& empty) const; + + // Implementation for setting most fields with a setValue() method. + template + void + setFieldUsingSetValue(V value); + + // Implementation for setting fields using assignment + template + void + setFieldUsingAssignment(T const& value); +}; + +//------------------------------------------------------------------------------ +// Implementation +//------------------------------------------------------------------------------ + +inline SerializedTypeID +STData::getInnerSType() const noexcept +{ + return static_cast(inner_type_); +} + +template +V +STData::getFieldByValue() const +{ + STBase const* rf = &data_.get(); + + // if (!rf) + // throwFieldNotFound(getFName()); + + SerializedTypeID id = rf->getSType(); + + if (id == STI_NOTPRESENT) + Throw("Field not present"); + + T const* cf = dynamic_cast(rf); + + if (!cf) + Throw("Wrong field type"); + + return cf->value(); +} + +// Implementations for getting (most) fields that return by const reference. +// +// If an absent optional field is deserialized we don't have anything +// obvious to return. So we insist on having the call provide an +// 'empty' value we return in that circumstance. +template +V const& +STData::getFieldByConstRef(V const& empty) const +{ + STBase const* rf = &data_.get(); + + // if (!rf) + // throwFieldNotFound(field); + + SerializedTypeID id = rf->getSType(); + + if (id == STI_NOTPRESENT) + return empty; // optional field not present + + T const* cf = dynamic_cast(rf); + + if (!cf) + Throw("Wrong field type"); + + return *cf; +} + +// Implementation for setting most fields with a setValue() method. +template +void +STData::setFieldUsingSetValue(V value) +{ + static_assert(!std::is_lvalue_reference::value, ""); + + STBase* rf = &data_.get(); + + // if (!rf) + // throwFieldNotFound(field); + + if (rf->getSType() == STI_NOTPRESENT) + rf = makeFieldPresent(); + + T* cf = dynamic_cast(rf); + + if (!cf) + Throw("Wrong field type"); + + cf->setValue(std::move(value)); +} + +// Implementation for setting fields using assignment +template +void +STData::setFieldUsingAssignment(T const& value) +{ + STBase* rf = &data_.get(); + + // if (!rf) + // throwFieldNotFound(field); + + // if (rf->getSType() == STI_NOTPRESENT) + // rf = makeFieldPresent(field); + + T* cf = dynamic_cast(rf); + + if (!cf) + Throw("Wrong field type"); + + (*cf) = value; +} + +//------------------------------------------------------------------------------ +// +// Creation +// +//------------------------------------------------------------------------------ + +STData +dataFromJson(SField const& field, Json::Value const& value); + +} // namespace xrpl diff --git a/include/xrpl/protocol/STDataType.h b/include/xrpl/protocol/STDataType.h new file mode 100644 index 0000000000..cae5907a02 --- /dev/null +++ b/include/xrpl/protocol/STDataType.h @@ -0,0 +1,87 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +namespace xrpl { + +class STDataType final : public STBase +{ +private: + std::uint16_t inner_type_; + bool default_{true}; + +public: + using value_type = STDataType; // Although not directly holding a single value + + STDataType(SField const& n); + STDataType(SField const& n, SerializedTypeID); + + STDataType(SerialIter& sit, SField const& name); + + SerializedTypeID + getSType() const override; + + std::string + getInnerTypeString() const; + + std::string + getText() const override; + + Json::Value getJson(JsonOptions) const override; + + void + add(Serializer& s) const override; + + bool + isEquivalent(STBase const& t) const override; + + bool + isDefault() const override; + + void setInnerSType(SerializedTypeID); + + SerializedTypeID + getInnerSType() const noexcept; + + STBase* + makeFieldPresent(); + + STBase* + copy(std::size_t n, void* buf) const override; + STBase* + move(std::size_t n, void* buf) override; + + friend class detail::STVar; +}; + +//------------------------------------------------------------------------------ +// Implementation +//------------------------------------------------------------------------------ + +inline SerializedTypeID +STDataType::getInnerSType() const noexcept +{ + return static_cast(inner_type_); +} + +//------------------------------------------------------------------------------ +// +// Creation +// +//------------------------------------------------------------------------------ + +STDataType +dataTypeFromJson(SField const& field, Json::Value const& value); + +} // namespace xrpl diff --git a/include/xrpl/protocol/STJson.h b/include/xrpl/protocol/STJson.h new file mode 100644 index 0000000000..5d7c2f7d9e --- /dev/null +++ b/include/xrpl/protocol/STJson.h @@ -0,0 +1,193 @@ +#pragma once + +#include +#include +#include + +#include +#include +#include +#include +#include + +namespace xrpl { + +/** + * STJson: Serialized Type for JSON-like structures (objects or arrays). + * + * Supports two modes: + * - Object: Key-value pairs where keys are VL-encoded strings + * - Array: Ordered list of values + * + * Values are [SType marker][VL-encoded SType serialization]. + * Values can be any SType, including nested STJson. + * + * Serialization format: [type_byte][VL_length][data...] + * - type_byte: 0x00 = Object, 0x01 = Array + */ +class STJson : public STBase +{ +public: + enum class JsonType : uint8_t { Object = 0x00, Array = 0x01 }; + + using value_type = STJson; + value_type + value() const + { + return *this; + } + + using Key = std::string; + using Value = std::shared_ptr; + using Map = std::map; + using Array = std::vector; + + STJson() = default; + + explicit STJson(Map&& map); + explicit STJson(Array&& array); + explicit STJson(SField const& name); + explicit STJson(SerialIter& sit, SField const& name); + + SerializedTypeID + getSType() const override; + + // Type checking + bool + isArray() const; + + bool + isObject() const; + + JsonType + getType() const; + + // Depth checking (0 = no nesting, 1 = one level of nesting) + int + getDepth() const; + + // Parse from binary blob + static std::shared_ptr + fromBlob(void const* data, std::size_t size); + + // Parse from SerialIter + static std::shared_ptr + fromSerialIter(SerialIter& sit); + + // Serialize to binary + void + add(Serializer& s) const override; + + // JSON representation + Json::Value + getJson(JsonOptions options) const override; + + bool + isEquivalent(STBase const& t) const override; + + bool + isDefault() const override; + + // Blob representation + Blob + toBlob() const; + + // STJson size + std::size_t + size() const; + + // Object accessors (only valid when isObject() == true) + Map const& + getMap() const; + + void + setObjectField(Key const& key, Value const& value); + + std::optional + getObjectField(Key const& key) const; + + void + setNestedObjectField(Key const& key, Key const& nestedKey, Value const& value); + + std::optional + getNestedObjectField(Key const& key, Key const& nestedKey) const; + + // Array accessors (only valid when isArray() == true) + Array const& + getArray() const; + + void + pushArrayElement(Value const& value); + + std::optional + getArrayElement(size_t index) const; + + void + setArrayElement(size_t index, Value const& value); + + void + setArrayElementField(size_t index, Key const& key, Value const& value); + + std::optional + getArrayElementField(size_t index, Key const& key) const; + + size_t + arraySize() const; + + // Nested array accessors (for arrays stored in object fields) + void + setNestedArrayElement(Key const& key, size_t index, Value const& value); + + void + setNestedArrayElementField( + Key const& key, + size_t index, + Key const& nestedKey, + Value const& value); + + std::optional + getNestedArrayElement(Key const& key, size_t index) const; + + std::optional + getNestedArrayElementField(Key const& key, size_t index, Key const& nestedKey) const; + + // Factory for SType value from blob (with SType marker) + static Value + makeValueFromVLWithType(SerialIter& sit); + + void + setValue(STJson const& v); + +private: + std::variant data_{Map{}}; + bool default_{false}; + + // Helper: validate nesting depth (max 1 level) + void + validateDepth(Value const& value, int currentDepth) const; + + // Helper: parse a single key-value pair from SerialIter + static std::pair + parsePair(SerialIter& sit); + + // Helper: parse array elements from SerialIter + static Array + parseArray(SerialIter& sit, int length); + + // Helper: encode a key as VL + static void + addVLKey(Serializer& s, std::string const& str); + + // Helper: encode a value as [SType marker][VL] + static void + addVLValue(Serializer& s, std::shared_ptr const& value); + + STBase* + copy(std::size_t n, void* buf) const override; + STBase* + move(std::size_t n, void* buf) override; + + friend class detail::STVar; +}; + +} // namespace xrpl diff --git a/include/xrpl/protocol/STObject.h b/include/xrpl/protocol/STObject.h index 61a1cce05e..4cc5634f3f 100644 --- a/include/xrpl/protocol/STObject.h +++ b/include/xrpl/protocol/STObject.h @@ -11,6 +11,7 @@ #include #include #include +#include #include #include #include @@ -216,6 +217,10 @@ public: getFieldI32(SField const& field) const; AccountID getAccountID(SField const& field) const; + STData + getFieldData(SField const& field) const; + STDataType + getFieldDataType(SField const& field) const; Blob getFieldVL(SField const& field) const; @@ -234,6 +239,8 @@ public: getFieldCurrency(SField const& field) const; STNumber const& getFieldNumber(SField const& field) const; + STJson const& + getFieldJson(SField const& field) const; /** Get the value of a field. @param A TypedField built from an SField value representing the desired @@ -338,6 +345,9 @@ public: void set(STBase&& v); + void + addFieldFromSlice(SField const& sfield, Slice const& data); + void setFieldU8(SField const& field, unsigned char); void @@ -376,6 +386,8 @@ public: setFieldArray(SField const& field, STArray const& v); void setFieldObject(SField const& field, STObject const& v); + void + setFieldJson(SField const& field, STJson const& v); template void diff --git a/include/xrpl/protocol/TER.h b/include/xrpl/protocol/TER.h index bc7c0a7739..7296ad0c8c 100644 --- a/include/xrpl/protocol/TER.h +++ b/include/xrpl/protocol/TER.h @@ -352,6 +352,7 @@ enum TECcodes : TERUnderlyingType { // reclaimed after those networks reset. tecNO_DELEGATE_PERMISSION = 198, tecWASM_REJECTED = 199, + tecINVALID_PARAMETERS = 200, }; //------------------------------------------------------------------------------ diff --git a/include/xrpl/protocol/TxFlags.h b/include/xrpl/protocol/TxFlags.h index 7c2085109f..aea17af0f7 100644 --- a/include/xrpl/protocol/TxFlags.h +++ b/include/xrpl/protocol/TxFlags.h @@ -212,8 +212,21 @@ inline constexpr FlagValue tfUniversalMask = ~tfUniversal; TF_FLAG(tfLoanDefault, 0x00010000) \ TF_FLAG(tfLoanImpair, 0x00020000) \ TF_FLAG(tfLoanUnimpair, 0x00040000), \ + MASK_ADJ(0)) \ + \ + TRANSACTION(Contract, \ + TF_FLAG(tfImmutable, 0x00010000) \ + TF_FLAG(tfCodeImmutable, 0x00020000) \ + TF_FLAG(tfABIImmutable, 0x00040000) \ + TF_FLAG(tfUndeletable, 0x00080000), \ MASK_ADJ(0)) +constexpr std::uint32_t tfSendAmount = 0x00010000; +constexpr std::uint32_t tfSendNFToken = 0x00020000; +constexpr std::uint32_t tfAuthorizeToken = 0x00040000; +constexpr std::uint32_t tfContractParameterMask = + ~(tfSendAmount | tfSendNFToken | tfAuthorizeToken); + // clang-format on // Create all the flag values. diff --git a/include/xrpl/protocol/detail/emitable.macro b/include/xrpl/protocol/detail/emitable.macro new file mode 100644 index 0000000000..be410bbb7c --- /dev/null +++ b/include/xrpl/protocol/detail/emitable.macro @@ -0,0 +1,19 @@ +#if !defined(EMITABLE) +#error "undefined macro: EMITABLE" +#endif + +/** + * EMITABLE(name, type, txType, value) + * + * This macro defines a permission: + * name: the name of the permission. + * type: the GranularPermissionType enum. + * txType: the corresponding TxType for this permission. + * value: the uint32 numeric value for the enum type. + */ + +/** This removes the contract account the ability to set or remove deposit auth. */ +EMITABLE(AccountDepositAuth, ttACCOUNT_SET, 65537) + +// ** This removes the contract account the ability to set or remove disable master key. */ +EMITABLE(AccountDisableMaster, ttACCOUNT_SET, 65538) diff --git a/include/xrpl/protocol/detail/features.macro b/include/xrpl/protocol/detail/features.macro index 6688592d07..9179ec9558 100644 --- a/include/xrpl/protocol/detail/features.macro +++ b/include/xrpl/protocol/detail/features.macro @@ -15,6 +15,7 @@ // Add new amendments to the top of this list. // Keep it sorted in reverse chronological order. +XRPL_FEATURE(SmartContract, Supported::no, VoteBehavior::DefaultNo) XRPL_FEATURE(SmartEscrow, Supported::no, VoteBehavior::DefaultNo) XRPL_FIX (Security3_1_3, Supported::no, VoteBehavior::DefaultNo) XRPL_FIX (PermissionedDomainInvariant, Supported::yes, VoteBehavior::DefaultNo) diff --git a/include/xrpl/protocol/detail/ledger_entries.macro b/include/xrpl/protocol/detail/ledger_entries.macro index d5b082e539..8751b64ce0 100644 --- a/include/xrpl/protocol/detail/ledger_entries.macro +++ b/include/xrpl/protocol/detail/ledger_entries.macro @@ -150,6 +150,7 @@ LEDGER_ENTRY(ltACCOUNT_ROOT, 0x0061, AccountRoot, account, ({ {sfAMMID, soeOPTIONAL}, // pseudo-account designator {sfVaultID, soeOPTIONAL}, // pseudo-account designator {sfLoanBrokerID, soeOPTIONAL}, // pseudo-account designator + {sfContractID, soeOPTIONAL}, // pseudo-account designator })) /** A ledger object which contains a list of object identifiers. @@ -610,5 +611,45 @@ LEDGER_ENTRY(ltLOAN, 0x0089, Loan, loan, ({ {sfLoanScale, soeDEFAULT}, })) +/** A ledger object representing a contract source. + \sa keylet::contractSource + */ +LEDGER_ENTRY(ltCONTRACT_SOURCE, 0x0085, ContractSource, contract_source, ({ + {sfPreviousTxnID, soeREQUIRED}, + {sfPreviousTxnLgrSeq, soeREQUIRED}, + {sfContractHash, soeREQUIRED}, + {sfContractCode, soeREQUIRED}, + {sfFunctions, soeREQUIRED}, + {sfInstanceParameters, soeOPTIONAL}, + {sfReferenceCount, soeREQUIRED}, +})) + +/** A ledger object representing a contract. + \sa keylet::contract + */ +LEDGER_ENTRY(ltCONTRACT, 0x0086, Contract, contract, ({ + {sfPreviousTxnID, soeREQUIRED}, + {sfPreviousTxnLgrSeq, soeREQUIRED}, + {sfSequence, soeREQUIRED}, + {sfOwnerNode, soeREQUIRED}, + {sfOwner, soeREQUIRED}, + {sfContractAccount, soeREQUIRED}, + {sfContractHash, soeREQUIRED}, + {sfInstanceParameterValues, soeOPTIONAL}, + {sfURI, soeOPTIONAL}, +})) + +/** A ledger object representing a contract data. + \sa keylet::contractData + */ +LEDGER_ENTRY(ltCONTRACT_DATA, 0x0087, ContractData, contract_data, ({ + {sfPreviousTxnID, soeREQUIRED}, + {sfPreviousTxnLgrSeq, soeREQUIRED}, + {sfOwnerNode, soeREQUIRED}, + {sfOwner, soeREQUIRED}, + {sfContractAccount, soeREQUIRED}, + {sfContractJson, soeREQUIRED}, +})) + #undef EXPAND #undef LEDGER_ENTRY_DUPLICATE diff --git a/include/xrpl/protocol/detail/sfields.macro b/include/xrpl/protocol/detail/sfields.macro index 78a3fc51ad..a790b2b0df 100644 --- a/include/xrpl/protocol/detail/sfields.macro +++ b/include/xrpl/protocol/detail/sfields.macro @@ -22,9 +22,10 @@ TYPED_SFIELD(sfAssetScale, UINT8, 5) // 8-bit integers (uncommon) TYPED_SFIELD(sfTickSize, UINT8, 16) TYPED_SFIELD(sfUNLModifyDisabling, UINT8, 17) -TYPED_SFIELD(sfHookResult, UINT8, 18) +// 18 unused TYPED_SFIELD(sfWasLockingChainSend, UINT8, 19) TYPED_SFIELD(sfWithdrawalPolicy, UINT8, 20) +TYPED_SFIELD(sfContractResult, UINT8, 21) // 16-bit integers (common) TYPED_SFIELD(sfLedgerEntryType, UINT16, 1, SField::sMD_Never) @@ -36,10 +37,7 @@ TYPED_SFIELD(sfDiscountedFee, UINT16, 6) // 16-bit integers (uncommon) TYPED_SFIELD(sfVersion, UINT16, 16) -TYPED_SFIELD(sfHookStateChangeCount, UINT16, 17) -TYPED_SFIELD(sfHookEmitCount, UINT16, 18) -TYPED_SFIELD(sfHookExecutionIndex, UINT16, 19) -TYPED_SFIELD(sfHookApiVersion, UINT16, 20) +// 17 to 20 unused TYPED_SFIELD(sfLedgerFixType, UINT16, 21) TYPED_SFIELD(sfManagementFeeRate, UINT16, 22) // 1/10 basis points (bips) @@ -90,9 +88,7 @@ TYPED_SFIELD(sfTicketSequence, UINT32, 41) TYPED_SFIELD(sfNFTokenTaxon, UINT32, 42) TYPED_SFIELD(sfMintedNFTokens, UINT32, 43) TYPED_SFIELD(sfBurnedNFTokens, UINT32, 44) -TYPED_SFIELD(sfHookStateCount, UINT32, 45) -TYPED_SFIELD(sfEmitGeneration, UINT32, 46) -// 47 reserved for Hooks +// 45 to 47 unused TYPED_SFIELD(sfVoteWeight, UINT32, 48) TYPED_SFIELD(sfFirstNFTokenSequence, UINT32, 50) TYPED_SFIELD(sfOracleDocumentID, UINT32, 51) @@ -118,6 +114,7 @@ TYPED_SFIELD(sfExtensionSizeLimit, UINT32, 70) TYPED_SFIELD(sfGasPrice, UINT32, 71) TYPED_SFIELD(sfComputationAllowance, UINT32, 72) TYPED_SFIELD(sfGasUsed, UINT32, 73) +TYPED_SFIELD(sfParameterFlag, UINT32, 74) // 64-bit integers (common) TYPED_SFIELD(sfIndexNext, UINT64, 1) @@ -135,9 +132,7 @@ TYPED_SFIELD(sfNFTokenOfferNode, UINT64, 12) TYPED_SFIELD(sfEmitBurden, UINT64, 13) // 64-bit integers (uncommon) -TYPED_SFIELD(sfHookOn, UINT64, 16) -TYPED_SFIELD(sfHookInstructionCount, UINT64, 17) -TYPED_SFIELD(sfHookReturnCode, UINT64, 18) +// 16 to 18 unused TYPED_SFIELD(sfReferenceCount, UINT64, 19) TYPED_SFIELD(sfXChainClaimID, UINT64, 20) TYPED_SFIELD(sfXChainAccountCreateCount, UINT64, 21) @@ -197,10 +192,7 @@ TYPED_SFIELD(sfPreviousPageMin, UINT256, 26) TYPED_SFIELD(sfNextPageMin, UINT256, 27) TYPED_SFIELD(sfNFTokenBuyOffer, UINT256, 28) TYPED_SFIELD(sfNFTokenSellOffer, UINT256, 29) -TYPED_SFIELD(sfHookStateKey, UINT256, 30) -TYPED_SFIELD(sfHookHash, UINT256, 31) -TYPED_SFIELD(sfHookNamespace, UINT256, 32) -TYPED_SFIELD(sfHookSetTxnID, UINT256, 33) +// 30 to 33 unused TYPED_SFIELD(sfDomainID, UINT256, 34) TYPED_SFIELD(sfVaultID, UINT256, 35, SField::sMD_PseudoAccount | SField::sMD_Default) @@ -208,6 +200,9 @@ TYPED_SFIELD(sfParentBatchID, UINT256, 36) TYPED_SFIELD(sfLoanBrokerID, UINT256, 37, SField::sMD_PseudoAccount | SField::sMD_Default) TYPED_SFIELD(sfLoanID, UINT256, 38) +TYPED_SFIELD(sfContractHash, UINT256, 39) +TYPED_SFIELD(sfContractID, UINT256, 40, + SField::sMD_PseudoAccount | SField::sMD_Default) // number (common) TYPED_SFIELD(sfNumber, NUMBER, 1) @@ -252,9 +247,7 @@ TYPED_SFIELD(sfMinimumOffer, AMOUNT, 16) TYPED_SFIELD(sfRippleEscrow, AMOUNT, 17) TYPED_SFIELD(sfDeliveredAmount, AMOUNT, 18) TYPED_SFIELD(sfNFTokenBrokerFee, AMOUNT, 19) - -// Reserve 20 & 21 for Hooks. - +// 20 to 21 unused // currency amount (fees) TYPED_SFIELD(sfBaseFeeDrops, AMOUNT, 22) TYPED_SFIELD(sfReserveBaseDrops, AMOUNT, 23) @@ -292,10 +285,7 @@ TYPED_SFIELD(sfMasterSignature, VL, 18, SField::sMD_Default, SFi TYPED_SFIELD(sfUNLModifyValidator, VL, 19) TYPED_SFIELD(sfValidatorToDisable, VL, 20) TYPED_SFIELD(sfValidatorToReEnable, VL, 21) -TYPED_SFIELD(sfHookStateData, VL, 22) -TYPED_SFIELD(sfHookReturnString, VL, 23) -TYPED_SFIELD(sfHookParameterName, VL, 24) -TYPED_SFIELD(sfHookParameterValue, VL, 25) +// 22 to 25 unused TYPED_SFIELD(sfDIDDocument, VL, 26) TYPED_SFIELD(sfData, VL, 27) TYPED_SFIELD(sfAssetClass, VL, 28) @@ -303,6 +293,8 @@ TYPED_SFIELD(sfProvider, VL, 29) TYPED_SFIELD(sfMPTokenMetadata, VL, 30) TYPED_SFIELD(sfCredentialType, VL, 31) TYPED_SFIELD(sfFinishFunction, VL, 32) +TYPED_SFIELD(sfContractCode, VL, 33) +TYPED_SFIELD(sfFunctionName, VL, 34) // account (common) TYPED_SFIELD(sfAccount, ACCOUNT, 1) @@ -319,7 +311,7 @@ TYPED_SFIELD(sfHolder, ACCOUNT, 11) TYPED_SFIELD(sfDelegate, ACCOUNT, 12) // account (uncommon) -TYPED_SFIELD(sfHookAccount, ACCOUNT, 16) +// 16 unused TYPED_SFIELD(sfOtherChainSource, ACCOUNT, 18) TYPED_SFIELD(sfOtherChainDestination, ACCOUNT, 19) TYPED_SFIELD(sfAttestationSignerAccount, ACCOUNT, 20) @@ -329,6 +321,7 @@ TYPED_SFIELD(sfIssuingChainDoor, ACCOUNT, 23) TYPED_SFIELD(sfSubject, ACCOUNT, 24) TYPED_SFIELD(sfBorrower, ACCOUNT, 25) TYPED_SFIELD(sfCounterparty, ACCOUNT, 26) +TYPED_SFIELD(sfContractAccount, ACCOUNT, 27) // vector of 256-bit TYPED_SFIELD(sfIndexes, VECTOR256, 1, SField::sMD_Never) @@ -367,7 +360,7 @@ UNTYPED_SFIELD(sfMemo, OBJECT, 10) UNTYPED_SFIELD(sfSignerEntry, OBJECT, 11) UNTYPED_SFIELD(sfNFToken, OBJECT, 12) UNTYPED_SFIELD(sfEmitDetails, OBJECT, 13) -UNTYPED_SFIELD(sfHook, OBJECT, 14) +// 14 unused UNTYPED_SFIELD(sfPermission, OBJECT, 15) // inner object (uncommon) @@ -375,11 +368,7 @@ UNTYPED_SFIELD(sfSigner, OBJECT, 16) // 17 unused UNTYPED_SFIELD(sfMajority, OBJECT, 18) UNTYPED_SFIELD(sfDisabledValidator, OBJECT, 19) -UNTYPED_SFIELD(sfEmittedTxn, OBJECT, 20) -UNTYPED_SFIELD(sfHookExecution, OBJECT, 21) -UNTYPED_SFIELD(sfHookDefinition, OBJECT, 22) -UNTYPED_SFIELD(sfHookParameter, OBJECT, 23) -UNTYPED_SFIELD(sfHookGrant, OBJECT, 24) +// 20 to 24 unused UNTYPED_SFIELD(sfVoteEntry, OBJECT, 25) UNTYPED_SFIELD(sfAuctionSlot, OBJECT, 26) UNTYPED_SFIELD(sfAuthAccount, OBJECT, 27) @@ -393,6 +382,10 @@ UNTYPED_SFIELD(sfRawTransaction, OBJECT, 34) UNTYPED_SFIELD(sfBatchSigner, OBJECT, 35) UNTYPED_SFIELD(sfBook, OBJECT, 36) UNTYPED_SFIELD(sfCounterpartySignature, OBJECT, 37, SField::sMD_Default, SField::notSigning) +UNTYPED_SFIELD(sfFunction, OBJECT, 38) +UNTYPED_SFIELD(sfInstanceParameter, OBJECT, 39) +UNTYPED_SFIELD(sfInstanceParameterValue, OBJECT, 40) +UNTYPED_SFIELD(sfParameter, OBJECT, 41) // array of objects (common) // ARRAY/1 is reserved for end of array @@ -406,16 +399,14 @@ UNTYPED_SFIELD(sfSufficient, ARRAY, 7) UNTYPED_SFIELD(sfAffectedNodes, ARRAY, 8) UNTYPED_SFIELD(sfMemos, ARRAY, 9) UNTYPED_SFIELD(sfNFTokens, ARRAY, 10) -UNTYPED_SFIELD(sfHooks, ARRAY, 11) +// 11 unused UNTYPED_SFIELD(sfVoteSlots, ARRAY, 12) UNTYPED_SFIELD(sfAdditionalBooks, ARRAY, 13) // array of objects (uncommon) UNTYPED_SFIELD(sfMajorities, ARRAY, 16) UNTYPED_SFIELD(sfDisabledValidators, ARRAY, 17) -UNTYPED_SFIELD(sfHookExecutions, ARRAY, 18) -UNTYPED_SFIELD(sfHookParameters, ARRAY, 19) -UNTYPED_SFIELD(sfHookGrants, ARRAY, 20) +// 18 to 20 unused UNTYPED_SFIELD(sfXChainClaimAttestations, ARRAY, 21) UNTYPED_SFIELD(sfXChainCreateAccountAttestations, ARRAY, 22) // 23 unused @@ -427,3 +418,16 @@ UNTYPED_SFIELD(sfAcceptedCredentials, ARRAY, 28) UNTYPED_SFIELD(sfPermissions, ARRAY, 29) UNTYPED_SFIELD(sfRawTransactions, ARRAY, 30) UNTYPED_SFIELD(sfBatchSigners, ARRAY, 31, SField::sMD_Default, SField::notSigning) +UNTYPED_SFIELD(sfFunctions, ARRAY, 32) +UNTYPED_SFIELD(sfInstanceParameters, ARRAY, 33) +UNTYPED_SFIELD(sfInstanceParameterValues, ARRAY, 34) +UNTYPED_SFIELD(sfParameters, ARRAY, 35) + +// data +TYPED_SFIELD(sfParameterValue, DATA, 1, SField::sMD_Default) + +// data type +TYPED_SFIELD(sfParameterType, DATATYPE, 1) + +// json +TYPED_SFIELD(sfContractJson, JSON, 1) diff --git a/include/xrpl/protocol/detail/transactions.macro b/include/xrpl/protocol/detail/transactions.macro index a8ce412bb3..8b715b0584 100644 --- a/include/xrpl/protocol/detail/transactions.macro +++ b/include/xrpl/protocol/detail/transactions.macro @@ -3,7 +3,7 @@ #endif /** - * TRANSACTION(tag, value, name, delegable, amendments, privileges, fields) + * TRANSACTION(tag, value, name, delegable, amendments, privileges, emitable, fields) * * To ease maintenance, you may replace any unneeded values with "..." * e.g. #define TRANSACTION(tag, value, name, ...) @@ -28,6 +28,7 @@ TRANSACTION(ttPAYMENT, 0, Payment, Delegation::delegable, uint256{}, createAcct, + Emittance::emitable, ({ {sfDestination, soeREQUIRED}, {sfAmount, soeREQUIRED, soeMPTSupported}, @@ -48,6 +49,7 @@ TRANSACTION(ttESCROW_CREATE, 1, EscrowCreate, Delegation::delegable, uint256{}, noPriv, + Emittance::emitable, ({ {sfDestination, soeREQUIRED}, {sfDestinationTag, soeOPTIONAL}, @@ -67,6 +69,7 @@ TRANSACTION(ttESCROW_FINISH, 2, EscrowFinish, Delegation::delegable, uint256{}, noPriv, + Emittance::emitable, ({ {sfOwner, soeREQUIRED}, {sfOfferSequence, soeREQUIRED}, @@ -85,6 +88,7 @@ TRANSACTION(ttACCOUNT_SET, 3, AccountSet, Delegation::notDelegable, uint256{}, noPriv, + Emittance::emitable, ({ {sfEmailHash, soeOPTIONAL}, {sfWalletLocator, soeOPTIONAL}, @@ -106,6 +110,7 @@ TRANSACTION(ttESCROW_CANCEL, 4, EscrowCancel, Delegation::delegable, uint256{}, noPriv, + Emittance::emitable, ({ {sfOwner, soeREQUIRED}, {sfOfferSequence, soeREQUIRED}, @@ -119,6 +124,7 @@ TRANSACTION(ttREGULAR_KEY_SET, 5, SetRegularKey, Delegation::notDelegable, uint256{}, noPriv, + Emittance::notEmitable, ({ {sfRegularKey, soeOPTIONAL}, })) @@ -133,6 +139,7 @@ TRANSACTION(ttOFFER_CREATE, 7, OfferCreate, Delegation::delegable, uint256{}, noPriv, + Emittance::emitable, ({ {sfTakerPays, soeREQUIRED}, {sfTakerGets, soeREQUIRED}, @@ -149,6 +156,7 @@ TRANSACTION(ttOFFER_CANCEL, 8, OfferCancel, Delegation::delegable, uint256{}, noPriv, + Emittance::emitable, ({ {sfOfferSequence, soeREQUIRED}, })) @@ -163,6 +171,7 @@ TRANSACTION(ttTICKET_CREATE, 10, TicketCreate, Delegation::delegable, uint256{}, noPriv, + Emittance::emitable, ({ {sfTicketCount, soeREQUIRED}, })) @@ -179,6 +188,7 @@ TRANSACTION(ttSIGNER_LIST_SET, 12, SignerListSet, Delegation::notDelegable, uint256{}, noPriv, + Emittance::notEmitable, ({ {sfSignerQuorum, soeREQUIRED}, {sfSignerEntries, soeOPTIONAL}, @@ -192,6 +202,7 @@ TRANSACTION(ttPAYCHAN_CREATE, 13, PaymentChannelCreate, Delegation::delegable, uint256{}, noPriv, + Emittance::emitable, ({ {sfDestination, soeREQUIRED}, {sfAmount, soeREQUIRED}, @@ -209,6 +220,7 @@ TRANSACTION(ttPAYCHAN_FUND, 14, PaymentChannelFund, Delegation::delegable, uint256{}, noPriv, + Emittance::emitable, ({ {sfChannel, soeREQUIRED}, {sfAmount, soeREQUIRED}, @@ -223,6 +235,7 @@ TRANSACTION(ttPAYCHAN_CLAIM, 15, PaymentChannelClaim, Delegation::delegable, uint256{}, noPriv, + Emittance::emitable, ({ {sfChannel, soeREQUIRED}, {sfAmount, soeOPTIONAL}, @@ -240,6 +253,7 @@ TRANSACTION(ttCHECK_CREATE, 16, CheckCreate, Delegation::delegable, uint256{}, noPriv, + Emittance::emitable, ({ {sfDestination, soeREQUIRED}, {sfSendMax, soeREQUIRED}, @@ -256,6 +270,7 @@ TRANSACTION(ttCHECK_CASH, 17, CheckCash, Delegation::delegable, uint256{}, noPriv, + Emittance::emitable, ({ {sfCheckID, soeREQUIRED}, {sfAmount, soeOPTIONAL}, @@ -270,6 +285,7 @@ TRANSACTION(ttCHECK_CANCEL, 18, CheckCancel, Delegation::delegable, uint256{}, noPriv, + Emittance::emitable, ({ {sfCheckID, soeREQUIRED}, })) @@ -282,6 +298,7 @@ TRANSACTION(ttDEPOSIT_PREAUTH, 19, DepositPreauth, Delegation::delegable, uint256{}, noPriv, + Emittance::notEmitable, ({ {sfAuthorize, soeOPTIONAL}, {sfUnauthorize, soeOPTIONAL}, @@ -297,6 +314,7 @@ TRANSACTION(ttTRUST_SET, 20, TrustSet, Delegation::delegable, uint256{}, noPriv, + Emittance::emitable, ({ {sfLimitAmount, soeOPTIONAL}, {sfQualityIn, soeOPTIONAL}, @@ -311,6 +329,7 @@ TRANSACTION(ttACCOUNT_DELETE, 21, AccountDelete, Delegation::notDelegable, uint256{}, mustDeleteAcct, + Emittance::notEmitable, ({ {sfDestination, soeREQUIRED}, {sfDestinationTag, soeOPTIONAL}, @@ -327,6 +346,7 @@ TRANSACTION(ttNFTOKEN_MINT, 25, NFTokenMint, Delegation::delegable, uint256{}, changeNFTCounts, + Emittance::emitable, ({ {sfNFTokenTaxon, soeREQUIRED}, {sfTransferFee, soeOPTIONAL}, @@ -345,6 +365,7 @@ TRANSACTION(ttNFTOKEN_BURN, 26, NFTokenBurn, Delegation::delegable, uint256{}, changeNFTCounts, + Emittance::emitable, ({ {sfNFTokenID, soeREQUIRED}, {sfOwner, soeOPTIONAL}, @@ -358,6 +379,7 @@ TRANSACTION(ttNFTOKEN_CREATE_OFFER, 27, NFTokenCreateOffer, Delegation::delegable, uint256{}, noPriv, + Emittance::emitable, ({ {sfNFTokenID, soeREQUIRED}, {sfAmount, soeREQUIRED}, @@ -374,6 +396,7 @@ TRANSACTION(ttNFTOKEN_CANCEL_OFFER, 28, NFTokenCancelOffer, Delegation::delegable, uint256{}, noPriv, + Emittance::emitable, ({ {sfNFTokenOffers, soeREQUIRED}, })) @@ -386,6 +409,7 @@ TRANSACTION(ttNFTOKEN_ACCEPT_OFFER, 29, NFTokenAcceptOffer, Delegation::delegable, uint256{}, noPriv, + Emittance::emitable, ({ {sfNFTokenBuyOffer, soeOPTIONAL}, {sfNFTokenSellOffer, soeOPTIONAL}, @@ -400,6 +424,7 @@ TRANSACTION(ttCLAWBACK, 30, Clawback, Delegation::delegable, featureClawback, noPriv, + Emittance::emitable, ({ {sfAmount, soeREQUIRED, soeMPTSupported}, {sfHolder, soeOPTIONAL}, @@ -413,6 +438,7 @@ TRANSACTION(ttAMM_CLAWBACK, 31, AMMClawback, Delegation::delegable, featureAMMClawback, mayDeleteAcct | overrideFreeze, + Emittance::emitable, ({ {sfHolder, soeREQUIRED}, {sfAsset, soeREQUIRED}, @@ -428,6 +454,7 @@ TRANSACTION(ttAMM_CREATE, 35, AMMCreate, Delegation::delegable, featureAMM, createPseudoAcct, + Emittance::emitable, ({ {sfAmount, soeREQUIRED}, {sfAmount2, soeREQUIRED}, @@ -442,6 +469,7 @@ TRANSACTION(ttAMM_DEPOSIT, 36, AMMDeposit, Delegation::delegable, featureAMM, noPriv, + Emittance::emitable, ({ {sfAsset, soeREQUIRED}, {sfAsset2, soeREQUIRED}, @@ -460,6 +488,7 @@ TRANSACTION(ttAMM_WITHDRAW, 37, AMMWithdraw, Delegation::delegable, featureAMM, mayDeleteAcct, + Emittance::emitable, ({ {sfAsset, soeREQUIRED}, {sfAsset2, soeREQUIRED}, @@ -477,6 +506,7 @@ TRANSACTION(ttAMM_VOTE, 38, AMMVote, Delegation::delegable, featureAMM, noPriv, + Emittance::emitable, ({ {sfAsset, soeREQUIRED}, {sfAsset2, soeREQUIRED}, @@ -491,6 +521,7 @@ TRANSACTION(ttAMM_BID, 39, AMMBid, Delegation::delegable, featureAMM, noPriv, + Emittance::emitable, ({ {sfAsset, soeREQUIRED}, {sfAsset2, soeREQUIRED}, @@ -507,6 +538,7 @@ TRANSACTION(ttAMM_DELETE, 40, AMMDelete, Delegation::delegable, featureAMM, mustDeleteAcct, + Emittance::emitable, ({ {sfAsset, soeREQUIRED}, {sfAsset2, soeREQUIRED}, @@ -520,6 +552,7 @@ TRANSACTION(ttXCHAIN_CREATE_CLAIM_ID, 41, XChainCreateClaimID, Delegation::delegable, featureXChainBridge, noPriv, + Emittance::emitable, ({ {sfXChainBridge, soeREQUIRED}, {sfSignatureReward, soeREQUIRED}, @@ -531,6 +564,7 @@ TRANSACTION(ttXCHAIN_COMMIT, 42, XChainCommit, Delegation::delegable, featureXChainBridge, noPriv, + Emittance::emitable, ({ {sfXChainBridge, soeREQUIRED}, {sfXChainClaimID, soeREQUIRED}, @@ -543,6 +577,7 @@ TRANSACTION(ttXCHAIN_CLAIM, 43, XChainClaim, Delegation::delegable, featureXChainBridge, noPriv, + Emittance::emitable, ({ {sfXChainBridge, soeREQUIRED}, {sfXChainClaimID, soeREQUIRED}, @@ -556,6 +591,7 @@ TRANSACTION(ttXCHAIN_ACCOUNT_CREATE_COMMIT, 44, XChainAccountCreateCommit, Delegation::delegable, featureXChainBridge, noPriv, + Emittance::emitable, ({ {sfXChainBridge, soeREQUIRED}, {sfDestination, soeREQUIRED}, @@ -568,6 +604,7 @@ TRANSACTION(ttXCHAIN_ADD_CLAIM_ATTESTATION, 45, XChainAddClaimAttestation, Delegation::delegable, featureXChainBridge, createAcct, + Emittance::emitable, ({ {sfXChainBridge, soeREQUIRED}, @@ -589,6 +626,7 @@ TRANSACTION(ttXCHAIN_ADD_ACCOUNT_CREATE_ATTESTATION, 46, Delegation::delegable, featureXChainBridge, createAcct, + Emittance::emitable, ({ {sfXChainBridge, soeREQUIRED}, @@ -610,6 +648,7 @@ TRANSACTION(ttXCHAIN_MODIFY_BRIDGE, 47, XChainModifyBridge, Delegation::delegable, featureXChainBridge, noPriv, + Emittance::emitable, ({ {sfXChainBridge, soeREQUIRED}, {sfSignatureReward, soeOPTIONAL}, @@ -621,6 +660,7 @@ TRANSACTION(ttXCHAIN_CREATE_BRIDGE, 48, XChainCreateBridge, Delegation::delegable, featureXChainBridge, noPriv, + Emittance::emitable, ({ {sfXChainBridge, soeREQUIRED}, {sfSignatureReward, soeREQUIRED}, @@ -635,6 +675,7 @@ TRANSACTION(ttDID_SET, 49, DIDSet, Delegation::delegable, featureDID, noPriv, + Emittance::emitable, ({ {sfDIDDocument, soeOPTIONAL}, {sfURI, soeOPTIONAL}, @@ -649,6 +690,7 @@ TRANSACTION(ttDID_DELETE, 50, DIDDelete, Delegation::delegable, featureDID, noPriv, + Emittance::emitable, ({})) /** This transaction type creates an Oracle instance */ @@ -659,6 +701,7 @@ TRANSACTION(ttORACLE_SET, 51, OracleSet, Delegation::delegable, featurePriceOracle, noPriv, + Emittance::emitable, ({ {sfOracleDocumentID, soeREQUIRED}, {sfProvider, soeOPTIONAL}, @@ -676,6 +719,7 @@ TRANSACTION(ttORACLE_DELETE, 52, OracleDelete, Delegation::delegable, featurePriceOracle, noPriv, + Emittance::emitable, ({ {sfOracleDocumentID, soeREQUIRED}, })) @@ -688,6 +732,7 @@ TRANSACTION(ttLEDGER_STATE_FIX, 53, LedgerStateFix, Delegation::delegable, fixNFTokenPageLinks, noPriv, + Emittance::emitable, ({ {sfLedgerFixType, soeREQUIRED}, {sfOwner, soeOPTIONAL}, @@ -701,6 +746,7 @@ TRANSACTION(ttMPTOKEN_ISSUANCE_CREATE, 54, MPTokenIssuanceCreate, Delegation::delegable, featureMPTokensV1, createMPTIssuance, + Emittance::emitable, ({ {sfAssetScale, soeOPTIONAL}, {sfTransferFee, soeOPTIONAL}, @@ -718,6 +764,7 @@ TRANSACTION(ttMPTOKEN_ISSUANCE_DESTROY, 55, MPTokenIssuanceDestroy, Delegation::delegable, featureMPTokensV1, destroyMPTIssuance, + Emittance::emitable, ({ {sfMPTokenIssuanceID, soeREQUIRED}, })) @@ -730,6 +777,7 @@ TRANSACTION(ttMPTOKEN_ISSUANCE_SET, 56, MPTokenIssuanceSet, Delegation::delegable, featureMPTokensV1, noPriv, + Emittance::emitable, ({ {sfMPTokenIssuanceID, soeREQUIRED}, {sfHolder, soeOPTIONAL}, @@ -747,6 +795,7 @@ TRANSACTION(ttMPTOKEN_AUTHORIZE, 57, MPTokenAuthorize, Delegation::delegable, featureMPTokensV1, mustAuthorizeMPT, + Emittance::emitable, ({ {sfMPTokenIssuanceID, soeREQUIRED}, {sfHolder, soeOPTIONAL}, @@ -760,6 +809,7 @@ TRANSACTION(ttCREDENTIAL_CREATE, 58, CredentialCreate, Delegation::delegable, featureCredentials, noPriv, + Emittance::emitable, ({ {sfSubject, soeREQUIRED}, {sfCredentialType, soeREQUIRED}, @@ -775,6 +825,7 @@ TRANSACTION(ttCREDENTIAL_ACCEPT, 59, CredentialAccept, Delegation::delegable, featureCredentials, noPriv, + Emittance::emitable, ({ {sfIssuer, soeREQUIRED}, {sfCredentialType, soeREQUIRED}, @@ -788,6 +839,7 @@ TRANSACTION(ttCREDENTIAL_DELETE, 60, CredentialDelete, Delegation::delegable, featureCredentials, noPriv, + Emittance::emitable, ({ {sfSubject, soeOPTIONAL}, {sfIssuer, soeOPTIONAL}, @@ -802,6 +854,7 @@ TRANSACTION(ttNFTOKEN_MODIFY, 61, NFTokenModify, Delegation::delegable, featureDynamicNFT, noPriv, + Emittance::emitable, ({ {sfNFTokenID, soeREQUIRED}, {sfOwner, soeOPTIONAL}, @@ -816,6 +869,7 @@ TRANSACTION(ttPERMISSIONED_DOMAIN_SET, 62, PermissionedDomainSet, Delegation::delegable, featurePermissionedDomains, noPriv, + Emittance::emitable, ({ {sfDomainID, soeOPTIONAL}, {sfAcceptedCredentials, soeREQUIRED}, @@ -829,6 +883,7 @@ TRANSACTION(ttPERMISSIONED_DOMAIN_DELETE, 63, PermissionedDomainDelete, Delegation::delegable, featurePermissionedDomains, noPriv, + Emittance::emitable, ({ {sfDomainID, soeREQUIRED}, })) @@ -841,6 +896,7 @@ TRANSACTION(ttDELEGATE_SET, 64, DelegateSet, Delegation::notDelegable, featurePermissionDelegationV1_1, noPriv, + Emittance::notEmitable, ({ {sfAuthorize, soeREQUIRED}, {sfPermissions, soeREQUIRED}, @@ -854,6 +910,7 @@ TRANSACTION(ttVAULT_CREATE, 65, VaultCreate, Delegation::notDelegable, featureSingleAssetVault, createPseudoAcct | createMPTIssuance | mustModifyVault, + Emittance::emitable, ({ {sfAsset, soeREQUIRED, soeMPTSupported}, {sfAssetsMaximum, soeOPTIONAL}, @@ -872,6 +929,7 @@ TRANSACTION(ttVAULT_SET, 66, VaultSet, Delegation::notDelegable, featureSingleAssetVault, mustModifyVault, + Emittance::emitable, ({ {sfVaultID, soeREQUIRED}, {sfAssetsMaximum, soeOPTIONAL}, @@ -887,6 +945,7 @@ TRANSACTION(ttVAULT_DELETE, 67, VaultDelete, Delegation::notDelegable, featureSingleAssetVault, mustDeleteAcct | destroyMPTIssuance | mustModifyVault, + Emittance::emitable, ({ {sfVaultID, soeREQUIRED}, })) @@ -899,6 +958,7 @@ TRANSACTION(ttVAULT_DEPOSIT, 68, VaultDeposit, Delegation::notDelegable, featureSingleAssetVault, mayAuthorizeMPT | mustModifyVault, + Emittance::emitable, ({ {sfVaultID, soeREQUIRED}, {sfAmount, soeREQUIRED, soeMPTSupported}, @@ -912,6 +972,7 @@ TRANSACTION(ttVAULT_WITHDRAW, 69, VaultWithdraw, Delegation::notDelegable, featureSingleAssetVault, mayDeleteMPT | mayAuthorizeMPT | mustModifyVault, + Emittance::emitable, ({ {sfVaultID, soeREQUIRED}, {sfAmount, soeREQUIRED, soeMPTSupported}, @@ -927,6 +988,7 @@ TRANSACTION(ttVAULT_CLAWBACK, 70, VaultClawback, Delegation::notDelegable, featureSingleAssetVault, mayDeleteMPT | mustModifyVault, + Emittance::emitable, ({ {sfVaultID, soeREQUIRED}, {sfHolder, soeREQUIRED}, @@ -941,6 +1003,7 @@ TRANSACTION(ttBATCH, 71, Batch, Delegation::notDelegable, featureBatch, noPriv, + Emittance::notEmitable, ({ {sfRawTransactions, soeREQUIRED}, {sfBatchSigners, soeOPTIONAL}, @@ -955,7 +1018,9 @@ TRANSACTION(ttBATCH, 71, Batch, TRANSACTION(ttLOAN_BROKER_SET, 74, LoanBrokerSet, Delegation::notDelegable, featureLendingProtocol, - createPseudoAcct | mayAuthorizeMPT, ({ + createPseudoAcct | mayAuthorizeMPT, + Emittance::emitable, + ({ {sfVaultID, soeREQUIRED}, {sfLoanBrokerID, soeOPTIONAL}, {sfData, soeOPTIONAL}, @@ -972,7 +1037,9 @@ TRANSACTION(ttLOAN_BROKER_SET, 74, LoanBrokerSet, TRANSACTION(ttLOAN_BROKER_DELETE, 75, LoanBrokerDelete, Delegation::notDelegable, featureLendingProtocol, - mustDeleteAcct | mayAuthorizeMPT, ({ + mustDeleteAcct | mayAuthorizeMPT, + Emittance::emitable, + ({ {sfLoanBrokerID, soeREQUIRED}, })) @@ -983,7 +1050,9 @@ TRANSACTION(ttLOAN_BROKER_DELETE, 75, LoanBrokerDelete, TRANSACTION(ttLOAN_BROKER_COVER_DEPOSIT, 76, LoanBrokerCoverDeposit, Delegation::notDelegable, featureLendingProtocol, - noPriv, ({ + noPriv, + Emittance::emitable, + ({ {sfLoanBrokerID, soeREQUIRED}, {sfAmount, soeREQUIRED, soeMPTSupported}, })) @@ -995,7 +1064,9 @@ TRANSACTION(ttLOAN_BROKER_COVER_DEPOSIT, 76, LoanBrokerCoverDeposit, TRANSACTION(ttLOAN_BROKER_COVER_WITHDRAW, 77, LoanBrokerCoverWithdraw, Delegation::notDelegable, featureLendingProtocol, - mayAuthorizeMPT, ({ + mayAuthorizeMPT, + Emittance::emitable, + ({ {sfLoanBrokerID, soeREQUIRED}, {sfAmount, soeREQUIRED, soeMPTSupported}, {sfDestination, soeOPTIONAL}, @@ -1010,7 +1081,9 @@ TRANSACTION(ttLOAN_BROKER_COVER_WITHDRAW, 77, LoanBrokerCoverWithdraw, TRANSACTION(ttLOAN_BROKER_COVER_CLAWBACK, 78, LoanBrokerCoverClawback, Delegation::notDelegable, featureLendingProtocol, - noPriv, ({ + noPriv, + Emittance::emitable, + ({ {sfLoanBrokerID, soeOPTIONAL}, {sfAmount, soeOPTIONAL, soeMPTSupported}, })) @@ -1022,7 +1095,9 @@ TRANSACTION(ttLOAN_BROKER_COVER_CLAWBACK, 78, LoanBrokerCoverClawback, TRANSACTION(ttLOAN_SET, 80, LoanSet, Delegation::notDelegable, featureLendingProtocol, - mayAuthorizeMPT | mustModifyVault, ({ + mayAuthorizeMPT | mustModifyVault, + Emittance::emitable, + ({ {sfLoanBrokerID, soeREQUIRED}, {sfData, soeOPTIONAL}, {sfCounterparty, soeOPTIONAL}, @@ -1049,7 +1124,9 @@ TRANSACTION(ttLOAN_SET, 80, LoanSet, TRANSACTION(ttLOAN_DELETE, 81, LoanDelete, Delegation::notDelegable, featureLendingProtocol, - noPriv, ({ + noPriv, + Emittance::emitable, +({ {sfLoanID, soeREQUIRED}, })) @@ -1063,7 +1140,9 @@ TRANSACTION(ttLOAN_MANAGE, 82, LoanManage, // All of the LoanManage options will modify the vault, but the // transaction can succeed without options, essentially making it // a noop. - mayModifyVault, ({ + mayModifyVault, + Emittance::emitable, + ({ {sfLoanID, soeREQUIRED}, })) @@ -1074,11 +1153,110 @@ TRANSACTION(ttLOAN_MANAGE, 82, LoanManage, TRANSACTION(ttLOAN_PAY, 84, LoanPay, Delegation::notDelegable, featureLendingProtocol, - mayAuthorizeMPT | mustModifyVault, ({ + mayAuthorizeMPT | mustModifyVault, + Emittance::emitable, + ({ {sfLoanID, soeREQUIRED}, {sfAmount, soeREQUIRED, soeMPTSupported}, })) +/** This transaction type creates the smart contract. */ +#if TRANSACTION_INCLUDE +# include +#endif +TRANSACTION(ttCONTRACT_CREATE, 85, ContractCreate, + Delegation::delegable, + featureSmartContract, + createPseudoAcct, + Emittance::emitable, + ({ + {sfContractCode, soeOPTIONAL}, + {sfContractHash, soeOPTIONAL}, + {sfFunctions, soeOPTIONAL}, + {sfInstanceParameters, soeOPTIONAL}, + {sfInstanceParameterValues, soeOPTIONAL}, + {sfURI, soeOPTIONAL}, +})) + +/** This transaction type modifies the smart contract. */ +#if TRANSACTION_INCLUDE +# include +#endif +TRANSACTION(ttCONTRACT_MODIFY, 86, ContractModify, + Delegation::delegable, + featureSmartContract, + noPriv, + Emittance::emitable, + ({ + {sfContractAccount, soeOPTIONAL}, + {sfOwner, soeOPTIONAL}, + {sfContractCode, soeOPTIONAL}, + {sfContractHash, soeOPTIONAL}, + {sfFunctions, soeOPTIONAL}, + {sfInstanceParameters, soeOPTIONAL}, + {sfInstanceParameterValues, soeOPTIONAL}, + {sfURI, soeOPTIONAL}, +})) + +/** This transaction type deletes the smart contract. */ +#if TRANSACTION_INCLUDE +# include +#endif +TRANSACTION(ttCONTRACT_DELETE, 87, ContractDelete, + Delegation::delegable, + featureSmartContract, + mustDeleteAcct, + Emittance::emitable, + ({ + {sfContractAccount, soeREQUIRED}, +})) + +/** This transaction type claws back funds from the contract. */ +#if TRANSACTION_INCLUDE +# include +#endif +TRANSACTION(ttCONTRACT_CLAWBACK, 88, ContractClawback, + Delegation::delegable, + featureSmartContract, + noPriv, + Emittance::emitable, + ({ + {sfContractAccount, soeOPTIONAL}, + {sfAmount, soeREQUIRED, soeMPTSupported}, +})) + +/** This transaction type deletes user data. */ +#if TRANSACTION_INCLUDE +# include +#endif +TRANSACTION(ttCONTRACT_USER_DELETE, 89, ContractUserDelete, + Delegation::delegable, + featureSmartContract, + noPriv, + Emittance::notEmitable, + ({ + {sfContractAccount, soeREQUIRED}, + {sfFunctionName, soeREQUIRED}, + {sfParameters, soeOPTIONAL}, + {sfComputationAllowance, soeREQUIRED}, +})) + +/** This transaction type calls the smart contract. */ +#if TRANSACTION_INCLUDE +# include +#endif +TRANSACTION(ttCONTRACT_CALL, 90, ContractCall, + Delegation::delegable, + featureSmartContract, + noPriv, + Emittance::notEmitable, + ({ + {sfContractAccount, soeREQUIRED}, + {sfFunctionName, soeREQUIRED}, + {sfParameters, soeOPTIONAL}, + {sfComputationAllowance, soeREQUIRED}, +})) + /** This system-generated transaction type is used to update the status of the various amendments. For details, see: https://xrpl.org/amendments.html @@ -1090,6 +1268,7 @@ TRANSACTION(ttAMENDMENT, 100, EnableAmendment, Delegation::notDelegable, uint256{}, noPriv, + Emittance::notEmitable, ({ {sfLedgerSequence, soeREQUIRED}, {sfAmendment, soeREQUIRED}, @@ -1102,6 +1281,7 @@ TRANSACTION(ttFEE, 101, SetFee, Delegation::notDelegable, uint256{}, noPriv, + Emittance::notEmitable, ({ {sfLedgerSequence, soeOPTIONAL}, // Old version uses raw numbers @@ -1127,6 +1307,7 @@ TRANSACTION(ttUNL_MODIFY, 102, UNLModify, Delegation::notDelegable, uint256{}, noPriv, + Emittance::notEmitable, ({ {sfUNLModifyDisabling, soeREQUIRED}, {sfLedgerSequence, soeREQUIRED}, diff --git a/include/xrpl/protocol/jss.h b/include/xrpl/protocol/jss.h index 632b7df770..09bb126e76 100644 --- a/include/xrpl/protocol/jss.h +++ b/include/xrpl/protocol/jss.h @@ -186,6 +186,7 @@ JSS(common); // out: RPC server_definitions JSS(complete); // out: NetworkOPs, InboundLedger JSS(complete_ledgers); // out: NetworkOPs, PeerImp JSS(consensus); // out: NetworkOPs, LedgerConsensus +JSS(contract_account); // out: ContractInfo JSS(converge_time); // out: NetworkOPs JSS(converge_time_s); // out: NetworkOPs JSS(cookie); // out: NetworkOPs @@ -270,6 +271,8 @@ JSS(flags); // out: AccountOffers, NetworkOPs JSS(forward); // in: AccountTx JSS(freeze); // out: AccountLines JSS(freeze_peer); // out: AccountLines +JSS(function); // in: ContractInfo +JSS(functions); // out: ContractInfo JSS(deep_freeze); // out: AccountLines JSS(deep_freeze_peer); // out: AccountLines JSS(frozen_balances); // out: GatewayBalances @@ -551,6 +554,7 @@ JSS(size); // out: get_aggregate_price JSS(snapshot); // in: Subscribe JSS(source_account); // in: PathRequest, RipplePathFind JSS(source_amount); // in: PathRequest, RipplePathFind +JSS(source_code_uri); // out: ContractInfo JSS(source_currencies); // in: PathRequest, RipplePathFind JSS(source_tag); // out: AccountChannels JSS(stand_alone); // out: NetworkOPs @@ -648,6 +652,7 @@ JSS(url); // in/out: Subscribe, Unsubscribe JSS(url_password); // in: Subscribe JSS(url_username); // in: Subscribe JSS(urlgravatar); // +JSS(user_data); // out: ContractInfo JSS(username); // in: Subscribe JSS(validated); // out: NetworkOPs, RPCHelpers, AccountTx*, Tx JSS(validator_list_expires); // out: NetworkOps, ValidatorList diff --git a/include/xrpl/protocol/st.h b/include/xrpl/protocol/st.h index 61571196f2..c2f0b1bae6 100644 --- a/include/xrpl/protocol/st.h +++ b/include/xrpl/protocol/st.h @@ -7,7 +7,12 @@ #include #include #include +#include +#include +#include #include +#include +#include #include #include #include diff --git a/include/xrpl/protocol_autogen/ledger_entries/AccountRoot.h b/include/xrpl/protocol_autogen/ledger_entries/AccountRoot.h index 15cf14b21a..378cd8014b 100644 --- a/include/xrpl/protocol_autogen/ledger_entries/AccountRoot.h +++ b/include/xrpl/protocol_autogen/ledger_entries/AccountRoot.h @@ -518,6 +518,30 @@ public: { return this->sle_->isFieldPresent(sfLoanBrokerID); } + + /** + * @brief Get sfContractID (soeOPTIONAL) + * @return The field value, or std::nullopt if not present. + */ + [[nodiscard]] + protocol_autogen::Optional + getContractID() const + { + if (hasContractID()) + return this->sle_->at(sfContractID); + return std::nullopt; + } + + /** + * @brief Check if sfContractID is present. + * @return True if the field is present, false otherwise. + */ + [[nodiscard]] + bool + hasContractID() const + { + return this->sle_->isFieldPresent(sfContractID); + } }; /** @@ -819,6 +843,17 @@ public: return *this; } + /** + * @brief Set sfContractID (soeOPTIONAL) + * @return Reference to this builder for method chaining. + */ + AccountRootBuilder& + setContractID(std::decay_t const& value) + { + object_[sfContractID] = value; + return *this; + } + /** * @brief Build and return the completed AccountRoot wrapper. * @param index The ledger entry index. diff --git a/include/xrpl/protocol_autogen/ledger_entries/Contract.h b/include/xrpl/protocol_autogen/ledger_entries/Contract.h new file mode 100644 index 0000000000..1af30ac25a --- /dev/null +++ b/include/xrpl/protocol_autogen/ledger_entries/Contract.h @@ -0,0 +1,334 @@ +// This file is auto-generated. Do not edit. +#pragma once + +#include +#include +#include +#include +#include +#include + +#include +#include + +namespace xrpl::ledger_entries { + +class ContractBuilder; + +/** + * @brief Ledger Entry: Contract + * + * Type: ltCONTRACT (0x0086) + * RPC Name: contract + * + * Immutable wrapper around SLE providing type-safe field access. + * Use ContractBuilder to construct new ledger entries. + */ +class Contract : public LedgerEntryBase +{ +public: + static constexpr LedgerEntryType entryType = ltCONTRACT; + + /** + * @brief Construct a Contract ledger entry wrapper from an existing SLE object. + * @throws std::runtime_error if the ledger entry type doesn't match. + */ + explicit Contract(std::shared_ptr sle) + : LedgerEntryBase(std::move(sle)) + { + // Verify ledger entry type + if (sle_->getType() != entryType) + { + throw std::runtime_error("Invalid ledger entry type for Contract"); + } + } + + // Ledger entry-specific field getters + + /** + * @brief Get sfPreviousTxnID (soeREQUIRED) + * @return The field value. + */ + [[nodiscard]] + SF_UINT256::type::value_type + getPreviousTxnID() const + { + return this->sle_->at(sfPreviousTxnID); + } + + /** + * @brief Get sfPreviousTxnLgrSeq (soeREQUIRED) + * @return The field value. + */ + [[nodiscard]] + SF_UINT32::type::value_type + getPreviousTxnLgrSeq() const + { + return this->sle_->at(sfPreviousTxnLgrSeq); + } + + /** + * @brief Get sfSequence (soeREQUIRED) + * @return The field value. + */ + [[nodiscard]] + SF_UINT32::type::value_type + getSequence() const + { + return this->sle_->at(sfSequence); + } + + /** + * @brief Get sfOwnerNode (soeREQUIRED) + * @return The field value. + */ + [[nodiscard]] + SF_UINT64::type::value_type + getOwnerNode() const + { + return this->sle_->at(sfOwnerNode); + } + + /** + * @brief Get sfOwner (soeREQUIRED) + * @return The field value. + */ + [[nodiscard]] + SF_ACCOUNT::type::value_type + getOwner() const + { + return this->sle_->at(sfOwner); + } + + /** + * @brief Get sfContractAccount (soeREQUIRED) + * @return The field value. + */ + [[nodiscard]] + SF_ACCOUNT::type::value_type + getContractAccount() const + { + return this->sle_->at(sfContractAccount); + } + + /** + * @brief Get sfContractHash (soeREQUIRED) + * @return The field value. + */ + [[nodiscard]] + SF_UINT256::type::value_type + getContractHash() const + { + return this->sle_->at(sfContractHash); + } + + /** + * @brief Get sfInstanceParameterValues (soeOPTIONAL) + * @note This is an untyped field (unknown). + * @return The field value, or std::nullopt if not present. + */ + [[nodiscard]] + std::optional> + getInstanceParameterValues() const + { + if (this->sle_->isFieldPresent(sfInstanceParameterValues)) + return this->sle_->getFieldArray(sfInstanceParameterValues); + return std::nullopt; + } + + /** + * @brief Check if sfInstanceParameterValues is present. + * @return True if the field is present, false otherwise. + */ + [[nodiscard]] + bool + hasInstanceParameterValues() const + { + return this->sle_->isFieldPresent(sfInstanceParameterValues); + } + + /** + * @brief Get sfURI (soeOPTIONAL) + * @return The field value, or std::nullopt if not present. + */ + [[nodiscard]] + protocol_autogen::Optional + getURI() const + { + if (hasURI()) + return this->sle_->at(sfURI); + return std::nullopt; + } + + /** + * @brief Check if sfURI is present. + * @return True if the field is present, false otherwise. + */ + [[nodiscard]] + bool + hasURI() const + { + return this->sle_->isFieldPresent(sfURI); + } +}; + +/** + * @brief Builder for Contract ledger entries. + * + * Provides a fluent interface for constructing ledger entries with method chaining. + * Uses Json::Value internally for flexible ledger entry construction. + * Inherits common field setters from LedgerEntryBuilderBase. + */ +class ContractBuilder : public LedgerEntryBuilderBase +{ +public: + /** + * @brief Construct a new ContractBuilder with required fields. + * @param previousTxnID The sfPreviousTxnID field value. + * @param previousTxnLgrSeq The sfPreviousTxnLgrSeq field value. + * @param sequence The sfSequence field value. + * @param ownerNode The sfOwnerNode field value. + * @param owner The sfOwner field value. + * @param contractAccount The sfContractAccount field value. + * @param contractHash The sfContractHash field value. + */ + ContractBuilder(std::decay_t const& previousTxnID,std::decay_t const& previousTxnLgrSeq,std::decay_t const& sequence,std::decay_t const& ownerNode,std::decay_t const& owner,std::decay_t const& contractAccount,std::decay_t const& contractHash) + : LedgerEntryBuilderBase(ltCONTRACT) + { + setPreviousTxnID(previousTxnID); + setPreviousTxnLgrSeq(previousTxnLgrSeq); + setSequence(sequence); + setOwnerNode(ownerNode); + setOwner(owner); + setContractAccount(contractAccount); + setContractHash(contractHash); + } + + /** + * @brief Construct a ContractBuilder from an existing SLE object. + * @param sle The existing ledger entry to copy from. + * @throws std::runtime_error if the ledger entry type doesn't match. + */ + ContractBuilder(std::shared_ptr sle) + { + if (sle->at(sfLedgerEntryType) != ltCONTRACT) + { + throw std::runtime_error("Invalid ledger entry type for Contract"); + } + object_ = *sle; + } + + /** @brief Ledger entry-specific field setters */ + + /** + * @brief Set sfPreviousTxnID (soeREQUIRED) + * @return Reference to this builder for method chaining. + */ + ContractBuilder& + setPreviousTxnID(std::decay_t const& value) + { + object_[sfPreviousTxnID] = value; + return *this; + } + + /** + * @brief Set sfPreviousTxnLgrSeq (soeREQUIRED) + * @return Reference to this builder for method chaining. + */ + ContractBuilder& + setPreviousTxnLgrSeq(std::decay_t const& value) + { + object_[sfPreviousTxnLgrSeq] = value; + return *this; + } + + /** + * @brief Set sfSequence (soeREQUIRED) + * @return Reference to this builder for method chaining. + */ + ContractBuilder& + setSequence(std::decay_t const& value) + { + object_[sfSequence] = value; + return *this; + } + + /** + * @brief Set sfOwnerNode (soeREQUIRED) + * @return Reference to this builder for method chaining. + */ + ContractBuilder& + setOwnerNode(std::decay_t const& value) + { + object_[sfOwnerNode] = value; + return *this; + } + + /** + * @brief Set sfOwner (soeREQUIRED) + * @return Reference to this builder for method chaining. + */ + ContractBuilder& + setOwner(std::decay_t const& value) + { + object_[sfOwner] = value; + return *this; + } + + /** + * @brief Set sfContractAccount (soeREQUIRED) + * @return Reference to this builder for method chaining. + */ + ContractBuilder& + setContractAccount(std::decay_t const& value) + { + object_[sfContractAccount] = value; + return *this; + } + + /** + * @brief Set sfContractHash (soeREQUIRED) + * @return Reference to this builder for method chaining. + */ + ContractBuilder& + setContractHash(std::decay_t const& value) + { + object_[sfContractHash] = value; + return *this; + } + + /** + * @brief Set sfInstanceParameterValues (soeOPTIONAL) + * @return Reference to this builder for method chaining. + */ + ContractBuilder& + setInstanceParameterValues(STArray const& value) + { + object_.setFieldArray(sfInstanceParameterValues, value); + return *this; + } + + /** + * @brief Set sfURI (soeOPTIONAL) + * @return Reference to this builder for method chaining. + */ + ContractBuilder& + setURI(std::decay_t const& value) + { + object_[sfURI] = value; + return *this; + } + + /** + * @brief Build and return the completed Contract wrapper. + * @param index The ledger entry index. + * @return The constructed ledger entry wrapper. + */ + Contract + build(uint256 const& index) + { + return Contract{std::make_shared(std::move(object_), index)}; + } +}; + +} // namespace xrpl::ledger_entries diff --git a/include/xrpl/protocol_autogen/ledger_entries/ContractData.h b/include/xrpl/protocol_autogen/ledger_entries/ContractData.h new file mode 100644 index 0000000000..8b8b2b1f47 --- /dev/null +++ b/include/xrpl/protocol_autogen/ledger_entries/ContractData.h @@ -0,0 +1,239 @@ +// This file is auto-generated. Do not edit. +#pragma once + +#include +#include +#include +#include +#include +#include + +#include +#include + +namespace xrpl::ledger_entries { + +class ContractDataBuilder; + +/** + * @brief Ledger Entry: ContractData + * + * Type: ltCONTRACT_DATA (0x0087) + * RPC Name: contract_data + * + * Immutable wrapper around SLE providing type-safe field access. + * Use ContractDataBuilder to construct new ledger entries. + */ +class ContractData : public LedgerEntryBase +{ +public: + static constexpr LedgerEntryType entryType = ltCONTRACT_DATA; + + /** + * @brief Construct a ContractData ledger entry wrapper from an existing SLE object. + * @throws std::runtime_error if the ledger entry type doesn't match. + */ + explicit ContractData(std::shared_ptr sle) + : LedgerEntryBase(std::move(sle)) + { + // Verify ledger entry type + if (sle_->getType() != entryType) + { + throw std::runtime_error("Invalid ledger entry type for ContractData"); + } + } + + // Ledger entry-specific field getters + + /** + * @brief Get sfPreviousTxnID (soeREQUIRED) + * @return The field value. + */ + [[nodiscard]] + SF_UINT256::type::value_type + getPreviousTxnID() const + { + return this->sle_->at(sfPreviousTxnID); + } + + /** + * @brief Get sfPreviousTxnLgrSeq (soeREQUIRED) + * @return The field value. + */ + [[nodiscard]] + SF_UINT32::type::value_type + getPreviousTxnLgrSeq() const + { + return this->sle_->at(sfPreviousTxnLgrSeq); + } + + /** + * @brief Get sfOwnerNode (soeREQUIRED) + * @return The field value. + */ + [[nodiscard]] + SF_UINT64::type::value_type + getOwnerNode() const + { + return this->sle_->at(sfOwnerNode); + } + + /** + * @brief Get sfOwner (soeREQUIRED) + * @return The field value. + */ + [[nodiscard]] + SF_ACCOUNT::type::value_type + getOwner() const + { + return this->sle_->at(sfOwner); + } + + /** + * @brief Get sfContractAccount (soeREQUIRED) + * @return The field value. + */ + [[nodiscard]] + SF_ACCOUNT::type::value_type + getContractAccount() const + { + return this->sle_->at(sfContractAccount); + } + + /** + * @brief Get sfContractJson (soeREQUIRED) + * @return The field value. + */ + [[nodiscard]] + SF_JSON::type::value_type + getContractJson() const + { + return this->sle_->at(sfContractJson); + } +}; + +/** + * @brief Builder for ContractData ledger entries. + * + * Provides a fluent interface for constructing ledger entries with method chaining. + * Uses Json::Value internally for flexible ledger entry construction. + * Inherits common field setters from LedgerEntryBuilderBase. + */ +class ContractDataBuilder : public LedgerEntryBuilderBase +{ +public: + /** + * @brief Construct a new ContractDataBuilder with required fields. + * @param previousTxnID The sfPreviousTxnID field value. + * @param previousTxnLgrSeq The sfPreviousTxnLgrSeq field value. + * @param ownerNode The sfOwnerNode field value. + * @param owner The sfOwner field value. + * @param contractAccount The sfContractAccount field value. + * @param contractJson The sfContractJson field value. + */ + ContractDataBuilder(std::decay_t const& previousTxnID,std::decay_t const& previousTxnLgrSeq,std::decay_t const& ownerNode,std::decay_t const& owner,std::decay_t const& contractAccount,std::decay_t const& contractJson) + : LedgerEntryBuilderBase(ltCONTRACT_DATA) + { + setPreviousTxnID(previousTxnID); + setPreviousTxnLgrSeq(previousTxnLgrSeq); + setOwnerNode(ownerNode); + setOwner(owner); + setContractAccount(contractAccount); + setContractJson(contractJson); + } + + /** + * @brief Construct a ContractDataBuilder from an existing SLE object. + * @param sle The existing ledger entry to copy from. + * @throws std::runtime_error if the ledger entry type doesn't match. + */ + ContractDataBuilder(std::shared_ptr sle) + { + if (sle->at(sfLedgerEntryType) != ltCONTRACT_DATA) + { + throw std::runtime_error("Invalid ledger entry type for ContractData"); + } + object_ = *sle; + } + + /** @brief Ledger entry-specific field setters */ + + /** + * @brief Set sfPreviousTxnID (soeREQUIRED) + * @return Reference to this builder for method chaining. + */ + ContractDataBuilder& + setPreviousTxnID(std::decay_t const& value) + { + object_[sfPreviousTxnID] = value; + return *this; + } + + /** + * @brief Set sfPreviousTxnLgrSeq (soeREQUIRED) + * @return Reference to this builder for method chaining. + */ + ContractDataBuilder& + setPreviousTxnLgrSeq(std::decay_t const& value) + { + object_[sfPreviousTxnLgrSeq] = value; + return *this; + } + + /** + * @brief Set sfOwnerNode (soeREQUIRED) + * @return Reference to this builder for method chaining. + */ + ContractDataBuilder& + setOwnerNode(std::decay_t const& value) + { + object_[sfOwnerNode] = value; + return *this; + } + + /** + * @brief Set sfOwner (soeREQUIRED) + * @return Reference to this builder for method chaining. + */ + ContractDataBuilder& + setOwner(std::decay_t const& value) + { + object_[sfOwner] = value; + return *this; + } + + /** + * @brief Set sfContractAccount (soeREQUIRED) + * @return Reference to this builder for method chaining. + */ + ContractDataBuilder& + setContractAccount(std::decay_t const& value) + { + object_[sfContractAccount] = value; + return *this; + } + + /** + * @brief Set sfContractJson (soeREQUIRED) + * @return Reference to this builder for method chaining. + */ + ContractDataBuilder& + setContractJson(std::decay_t const& value) + { + object_[sfContractJson] = value; + return *this; + } + + /** + * @brief Build and return the completed ContractData wrapper. + * @param index The ledger entry index. + * @return The constructed ledger entry wrapper. + */ + ContractData + build(uint256 const& index) + { + return ContractData{std::make_shared(std::move(object_), index)}; + } +}; + +} // namespace xrpl::ledger_entries diff --git a/include/xrpl/protocol_autogen/ledger_entries/ContractSource.h b/include/xrpl/protocol_autogen/ledger_entries/ContractSource.h new file mode 100644 index 0000000000..25e8ce870f --- /dev/null +++ b/include/xrpl/protocol_autogen/ledger_entries/ContractSource.h @@ -0,0 +1,276 @@ +// This file is auto-generated. Do not edit. +#pragma once + +#include +#include +#include +#include +#include +#include + +#include +#include + +namespace xrpl::ledger_entries { + +class ContractSourceBuilder; + +/** + * @brief Ledger Entry: ContractSource + * + * Type: ltCONTRACT_SOURCE (0x0085) + * RPC Name: contract_source + * + * Immutable wrapper around SLE providing type-safe field access. + * Use ContractSourceBuilder to construct new ledger entries. + */ +class ContractSource : public LedgerEntryBase +{ +public: + static constexpr LedgerEntryType entryType = ltCONTRACT_SOURCE; + + /** + * @brief Construct a ContractSource ledger entry wrapper from an existing SLE object. + * @throws std::runtime_error if the ledger entry type doesn't match. + */ + explicit ContractSource(std::shared_ptr sle) + : LedgerEntryBase(std::move(sle)) + { + // Verify ledger entry type + if (sle_->getType() != entryType) + { + throw std::runtime_error("Invalid ledger entry type for ContractSource"); + } + } + + // Ledger entry-specific field getters + + /** + * @brief Get sfPreviousTxnID (soeREQUIRED) + * @return The field value. + */ + [[nodiscard]] + SF_UINT256::type::value_type + getPreviousTxnID() const + { + return this->sle_->at(sfPreviousTxnID); + } + + /** + * @brief Get sfPreviousTxnLgrSeq (soeREQUIRED) + * @return The field value. + */ + [[nodiscard]] + SF_UINT32::type::value_type + getPreviousTxnLgrSeq() const + { + return this->sle_->at(sfPreviousTxnLgrSeq); + } + + /** + * @brief Get sfContractHash (soeREQUIRED) + * @return The field value. + */ + [[nodiscard]] + SF_UINT256::type::value_type + getContractHash() const + { + return this->sle_->at(sfContractHash); + } + + /** + * @brief Get sfContractCode (soeREQUIRED) + * @return The field value. + */ + [[nodiscard]] + SF_VL::type::value_type + getContractCode() const + { + return this->sle_->at(sfContractCode); + } + + /** + * @brief Get sfFunctions (soeREQUIRED) + * @note This is an untyped field (unknown). + * @return The field value. + */ + [[nodiscard]] + STArray const& + getFunctions() const + { + return this->sle_->getFieldArray(sfFunctions); + } + + /** + * @brief Get sfInstanceParameters (soeOPTIONAL) + * @note This is an untyped field (unknown). + * @return The field value, or std::nullopt if not present. + */ + [[nodiscard]] + std::optional> + getInstanceParameters() const + { + if (this->sle_->isFieldPresent(sfInstanceParameters)) + return this->sle_->getFieldArray(sfInstanceParameters); + return std::nullopt; + } + + /** + * @brief Check if sfInstanceParameters is present. + * @return True if the field is present, false otherwise. + */ + [[nodiscard]] + bool + hasInstanceParameters() const + { + return this->sle_->isFieldPresent(sfInstanceParameters); + } + + /** + * @brief Get sfReferenceCount (soeREQUIRED) + * @return The field value. + */ + [[nodiscard]] + SF_UINT64::type::value_type + getReferenceCount() const + { + return this->sle_->at(sfReferenceCount); + } +}; + +/** + * @brief Builder for ContractSource ledger entries. + * + * Provides a fluent interface for constructing ledger entries with method chaining. + * Uses Json::Value internally for flexible ledger entry construction. + * Inherits common field setters from LedgerEntryBuilderBase. + */ +class ContractSourceBuilder : public LedgerEntryBuilderBase +{ +public: + /** + * @brief Construct a new ContractSourceBuilder with required fields. + * @param previousTxnID The sfPreviousTxnID field value. + * @param previousTxnLgrSeq The sfPreviousTxnLgrSeq field value. + * @param contractHash The sfContractHash field value. + * @param contractCode The sfContractCode field value. + * @param functions The sfFunctions field value. + * @param referenceCount The sfReferenceCount field value. + */ + ContractSourceBuilder(std::decay_t const& previousTxnID,std::decay_t const& previousTxnLgrSeq,std::decay_t const& contractHash,std::decay_t const& contractCode,STArray const& functions,std::decay_t const& referenceCount) + : LedgerEntryBuilderBase(ltCONTRACT_SOURCE) + { + setPreviousTxnID(previousTxnID); + setPreviousTxnLgrSeq(previousTxnLgrSeq); + setContractHash(contractHash); + setContractCode(contractCode); + setFunctions(functions); + setReferenceCount(referenceCount); + } + + /** + * @brief Construct a ContractSourceBuilder from an existing SLE object. + * @param sle The existing ledger entry to copy from. + * @throws std::runtime_error if the ledger entry type doesn't match. + */ + ContractSourceBuilder(std::shared_ptr sle) + { + if (sle->at(sfLedgerEntryType) != ltCONTRACT_SOURCE) + { + throw std::runtime_error("Invalid ledger entry type for ContractSource"); + } + object_ = *sle; + } + + /** @brief Ledger entry-specific field setters */ + + /** + * @brief Set sfPreviousTxnID (soeREQUIRED) + * @return Reference to this builder for method chaining. + */ + ContractSourceBuilder& + setPreviousTxnID(std::decay_t const& value) + { + object_[sfPreviousTxnID] = value; + return *this; + } + + /** + * @brief Set sfPreviousTxnLgrSeq (soeREQUIRED) + * @return Reference to this builder for method chaining. + */ + ContractSourceBuilder& + setPreviousTxnLgrSeq(std::decay_t const& value) + { + object_[sfPreviousTxnLgrSeq] = value; + return *this; + } + + /** + * @brief Set sfContractHash (soeREQUIRED) + * @return Reference to this builder for method chaining. + */ + ContractSourceBuilder& + setContractHash(std::decay_t const& value) + { + object_[sfContractHash] = value; + return *this; + } + + /** + * @brief Set sfContractCode (soeREQUIRED) + * @return Reference to this builder for method chaining. + */ + ContractSourceBuilder& + setContractCode(std::decay_t const& value) + { + object_[sfContractCode] = value; + return *this; + } + + /** + * @brief Set sfFunctions (soeREQUIRED) + * @return Reference to this builder for method chaining. + */ + ContractSourceBuilder& + setFunctions(STArray const& value) + { + object_.setFieldArray(sfFunctions, value); + return *this; + } + + /** + * @brief Set sfInstanceParameters (soeOPTIONAL) + * @return Reference to this builder for method chaining. + */ + ContractSourceBuilder& + setInstanceParameters(STArray const& value) + { + object_.setFieldArray(sfInstanceParameters, value); + return *this; + } + + /** + * @brief Set sfReferenceCount (soeREQUIRED) + * @return Reference to this builder for method chaining. + */ + ContractSourceBuilder& + setReferenceCount(std::decay_t const& value) + { + object_[sfReferenceCount] = value; + return *this; + } + + /** + * @brief Build and return the completed ContractSource wrapper. + * @param index The ledger entry index. + * @return The constructed ledger entry wrapper. + */ + ContractSource + build(uint256 const& index) + { + return ContractSource{std::make_shared(std::move(object_), index)}; + } +}; + +} // namespace xrpl::ledger_entries diff --git a/include/xrpl/protocol_autogen/transactions/ContractCall.h b/include/xrpl/protocol_autogen/transactions/ContractCall.h new file mode 100644 index 0000000000..07765c92ba --- /dev/null +++ b/include/xrpl/protocol_autogen/transactions/ContractCall.h @@ -0,0 +1,212 @@ +// This file is auto-generated. Do not edit. +#pragma once + +#include +#include +#include +#include +#include +#include + +#include +#include + +namespace xrpl::transactions { + +class ContractCallBuilder; + +/** + * @brief Transaction: ContractCall + * + * Type: ttCONTRACT_CALL (90) + * Delegable: Delegation::delegable + * Amendment: featureSmartContract + * Privileges: noPriv + * + * Immutable wrapper around STTx providing type-safe field access. + * Use ContractCallBuilder to construct new transactions. + */ +class ContractCall : public TransactionBase +{ +public: + static constexpr xrpl::TxType txType = ttCONTRACT_CALL; + + /** + * @brief Construct a ContractCall transaction wrapper from an existing STTx object. + * @throws std::runtime_error if the transaction type doesn't match. + */ + explicit ContractCall(std::shared_ptr tx) + : TransactionBase(std::move(tx)) + { + // Verify transaction type + if (tx_->getTxnType() != txType) + { + throw std::runtime_error("Invalid transaction type for ContractCall"); + } + } + + // Transaction-specific field getters + + /** + * @brief Get sfContractAccount (soeREQUIRED) + * @return The field value. + */ + [[nodiscard]] + SF_ACCOUNT::type::value_type + getContractAccount() const + { + return this->tx_->at(sfContractAccount); + } + + /** + * @brief Get sfFunctionName (soeREQUIRED) + * @return The field value. + */ + [[nodiscard]] + SF_VL::type::value_type + getFunctionName() const + { + return this->tx_->at(sfFunctionName); + } + /** + * @brief Get sfParameters (soeOPTIONAL) + * @note This is an untyped field. + * @return The field value, or std::nullopt if not present. + */ + [[nodiscard]] + std::optional> + getParameters() const + { + if (this->tx_->isFieldPresent(sfParameters)) + return this->tx_->getFieldArray(sfParameters); + return std::nullopt; + } + + /** + * @brief Check if sfParameters is present. + * @return True if the field is present, false otherwise. + */ + [[nodiscard]] + bool + hasParameters() const + { + return this->tx_->isFieldPresent(sfParameters); + } + + /** + * @brief Get sfComputationAllowance (soeREQUIRED) + * @return The field value. + */ + [[nodiscard]] + SF_UINT32::type::value_type + getComputationAllowance() const + { + return this->tx_->at(sfComputationAllowance); + } +}; + +/** + * @brief Builder for ContractCall transactions. + * + * Provides a fluent interface for constructing transactions with method chaining. + * Uses Json::Value internally for flexible transaction construction. + * Inherits common field setters from TransactionBuilderBase. + */ +class ContractCallBuilder : public TransactionBuilderBase +{ +public: + /** + * @brief Construct a new ContractCallBuilder with required fields. + * @param account The account initiating the transaction. + * @param contractAccount The sfContractAccount field value. + * @param functionName The sfFunctionName field value. + * @param computationAllowance The sfComputationAllowance field value. + * @param sequence Optional sequence number for the transaction. + * @param fee Optional fee for the transaction. + */ + ContractCallBuilder(SF_ACCOUNT::type::value_type account, + std::decay_t const& contractAccount, std::decay_t const& functionName, std::decay_t const& computationAllowance, std::optional sequence = std::nullopt, + std::optional fee = std::nullopt +) + : TransactionBuilderBase(ttCONTRACT_CALL, account, sequence, fee) + { + setContractAccount(contractAccount); + setFunctionName(functionName); + setComputationAllowance(computationAllowance); + } + + /** + * @brief Construct a ContractCallBuilder from an existing STTx object. + * @param tx The existing transaction to copy from. + * @throws std::runtime_error if the transaction type doesn't match. + */ + ContractCallBuilder(std::shared_ptr tx) + { + if (tx->getTxnType() != ttCONTRACT_CALL) + { + throw std::runtime_error("Invalid transaction type for ContractCallBuilder"); + } + object_ = *tx; + } + + /** @brief Transaction-specific field setters */ + + /** + * @brief Set sfContractAccount (soeREQUIRED) + * @return Reference to this builder for method chaining. + */ + ContractCallBuilder& + setContractAccount(std::decay_t const& value) + { + object_[sfContractAccount] = value; + return *this; + } + + /** + * @brief Set sfFunctionName (soeREQUIRED) + * @return Reference to this builder for method chaining. + */ + ContractCallBuilder& + setFunctionName(std::decay_t const& value) + { + object_[sfFunctionName] = value; + return *this; + } + + /** + * @brief Set sfParameters (soeOPTIONAL) + * @return Reference to this builder for method chaining. + */ + ContractCallBuilder& + setParameters(STArray const& value) + { + object_.setFieldArray(sfParameters, value); + return *this; + } + + /** + * @brief Set sfComputationAllowance (soeREQUIRED) + * @return Reference to this builder for method chaining. + */ + ContractCallBuilder& + setComputationAllowance(std::decay_t const& value) + { + object_[sfComputationAllowance] = value; + return *this; + } + + /** + * @brief Build and return the ContractCall wrapper. + * @param publicKey The public key for signing. + * @param secretKey The secret key for signing. + * @return The constructed transaction wrapper. + */ + ContractCall + build(PublicKey const& publicKey, SecretKey const& secretKey) + { + sign(publicKey, secretKey); + return ContractCall{std::make_shared(std::move(object_))}; + } +}; + +} // namespace xrpl::transactions diff --git a/include/xrpl/protocol_autogen/transactions/ContractClawback.h b/include/xrpl/protocol_autogen/transactions/ContractClawback.h new file mode 100644 index 0000000000..1fd7736c8c --- /dev/null +++ b/include/xrpl/protocol_autogen/transactions/ContractClawback.h @@ -0,0 +1,168 @@ +// This file is auto-generated. Do not edit. +#pragma once + +#include +#include +#include +#include +#include +#include + +#include +#include + +namespace xrpl::transactions { + +class ContractClawbackBuilder; + +/** + * @brief Transaction: ContractClawback + * + * Type: ttCONTRACT_CLAWBACK (88) + * Delegable: Delegation::delegable + * Amendment: featureSmartContract + * Privileges: noPriv + * + * Immutable wrapper around STTx providing type-safe field access. + * Use ContractClawbackBuilder to construct new transactions. + */ +class ContractClawback : public TransactionBase +{ +public: + static constexpr xrpl::TxType txType = ttCONTRACT_CLAWBACK; + + /** + * @brief Construct a ContractClawback transaction wrapper from an existing STTx object. + * @throws std::runtime_error if the transaction type doesn't match. + */ + explicit ContractClawback(std::shared_ptr tx) + : TransactionBase(std::move(tx)) + { + // Verify transaction type + if (tx_->getTxnType() != txType) + { + throw std::runtime_error("Invalid transaction type for ContractClawback"); + } + } + + // Transaction-specific field getters + + /** + * @brief Get sfContractAccount (soeOPTIONAL) + * @return The field value, or std::nullopt if not present. + */ + [[nodiscard]] + protocol_autogen::Optional + getContractAccount() const + { + if (hasContractAccount()) + { + return this->tx_->at(sfContractAccount); + } + return std::nullopt; + } + + /** + * @brief Check if sfContractAccount is present. + * @return True if the field is present, false otherwise. + */ + [[nodiscard]] + bool + hasContractAccount() const + { + return this->tx_->isFieldPresent(sfContractAccount); + } + + /** + * @brief Get sfAmount (soeREQUIRED) + * @note This field supports MPT (Multi-Purpose Token) amounts. + * @return The field value. + */ + [[nodiscard]] + SF_AMOUNT::type::value_type + getAmount() const + { + return this->tx_->at(sfAmount); + } +}; + +/** + * @brief Builder for ContractClawback transactions. + * + * Provides a fluent interface for constructing transactions with method chaining. + * Uses Json::Value internally for flexible transaction construction. + * Inherits common field setters from TransactionBuilderBase. + */ +class ContractClawbackBuilder : public TransactionBuilderBase +{ +public: + /** + * @brief Construct a new ContractClawbackBuilder with required fields. + * @param account The account initiating the transaction. + * @param amount The sfAmount field value. + * @param sequence Optional sequence number for the transaction. + * @param fee Optional fee for the transaction. + */ + ContractClawbackBuilder(SF_ACCOUNT::type::value_type account, + std::decay_t const& amount, std::optional sequence = std::nullopt, + std::optional fee = std::nullopt +) + : TransactionBuilderBase(ttCONTRACT_CLAWBACK, account, sequence, fee) + { + setAmount(amount); + } + + /** + * @brief Construct a ContractClawbackBuilder from an existing STTx object. + * @param tx The existing transaction to copy from. + * @throws std::runtime_error if the transaction type doesn't match. + */ + ContractClawbackBuilder(std::shared_ptr tx) + { + if (tx->getTxnType() != ttCONTRACT_CLAWBACK) + { + throw std::runtime_error("Invalid transaction type for ContractClawbackBuilder"); + } + object_ = *tx; + } + + /** @brief Transaction-specific field setters */ + + /** + * @brief Set sfContractAccount (soeOPTIONAL) + * @return Reference to this builder for method chaining. + */ + ContractClawbackBuilder& + setContractAccount(std::decay_t const& value) + { + object_[sfContractAccount] = value; + return *this; + } + + /** + * @brief Set sfAmount (soeREQUIRED) + * @note This field supports MPT (Multi-Purpose Token) amounts. + * @return Reference to this builder for method chaining. + */ + ContractClawbackBuilder& + setAmount(std::decay_t const& value) + { + object_[sfAmount] = value; + return *this; + } + + /** + * @brief Build and return the ContractClawback wrapper. + * @param publicKey The public key for signing. + * @param secretKey The secret key for signing. + * @return The constructed transaction wrapper. + */ + ContractClawback + build(PublicKey const& publicKey, SecretKey const& secretKey) + { + sign(publicKey, secretKey); + return ContractClawback{std::make_shared(std::move(object_))}; + } +}; + +} // namespace xrpl::transactions diff --git a/include/xrpl/protocol_autogen/transactions/ContractCreate.h b/include/xrpl/protocol_autogen/transactions/ContractCreate.h new file mode 100644 index 0000000000..3ad7ecf482 --- /dev/null +++ b/include/xrpl/protocol_autogen/transactions/ContractCreate.h @@ -0,0 +1,321 @@ +// This file is auto-generated. Do not edit. +#pragma once + +#include +#include +#include +#include +#include +#include + +#include +#include + +namespace xrpl::transactions { + +class ContractCreateBuilder; + +/** + * @brief Transaction: ContractCreate + * + * Type: ttCONTRACT_CREATE (85) + * Delegable: Delegation::delegable + * Amendment: featureSmartContract + * Privileges: createPseudoAcct + * + * Immutable wrapper around STTx providing type-safe field access. + * Use ContractCreateBuilder to construct new transactions. + */ +class ContractCreate : public TransactionBase +{ +public: + static constexpr xrpl::TxType txType = ttCONTRACT_CREATE; + + /** + * @brief Construct a ContractCreate transaction wrapper from an existing STTx object. + * @throws std::runtime_error if the transaction type doesn't match. + */ + explicit ContractCreate(std::shared_ptr tx) + : TransactionBase(std::move(tx)) + { + // Verify transaction type + if (tx_->getTxnType() != txType) + { + throw std::runtime_error("Invalid transaction type for ContractCreate"); + } + } + + // Transaction-specific field getters + + /** + * @brief Get sfContractCode (soeOPTIONAL) + * @return The field value, or std::nullopt if not present. + */ + [[nodiscard]] + protocol_autogen::Optional + getContractCode() const + { + if (hasContractCode()) + { + return this->tx_->at(sfContractCode); + } + return std::nullopt; + } + + /** + * @brief Check if sfContractCode is present. + * @return True if the field is present, false otherwise. + */ + [[nodiscard]] + bool + hasContractCode() const + { + return this->tx_->isFieldPresent(sfContractCode); + } + + /** + * @brief Get sfContractHash (soeOPTIONAL) + * @return The field value, or std::nullopt if not present. + */ + [[nodiscard]] + protocol_autogen::Optional + getContractHash() const + { + if (hasContractHash()) + { + return this->tx_->at(sfContractHash); + } + return std::nullopt; + } + + /** + * @brief Check if sfContractHash is present. + * @return True if the field is present, false otherwise. + */ + [[nodiscard]] + bool + hasContractHash() const + { + return this->tx_->isFieldPresent(sfContractHash); + } + /** + * @brief Get sfFunctions (soeOPTIONAL) + * @note This is an untyped field. + * @return The field value, or std::nullopt if not present. + */ + [[nodiscard]] + std::optional> + getFunctions() const + { + if (this->tx_->isFieldPresent(sfFunctions)) + return this->tx_->getFieldArray(sfFunctions); + return std::nullopt; + } + + /** + * @brief Check if sfFunctions is present. + * @return True if the field is present, false otherwise. + */ + [[nodiscard]] + bool + hasFunctions() const + { + return this->tx_->isFieldPresent(sfFunctions); + } + /** + * @brief Get sfInstanceParameters (soeOPTIONAL) + * @note This is an untyped field. + * @return The field value, or std::nullopt if not present. + */ + [[nodiscard]] + std::optional> + getInstanceParameters() const + { + if (this->tx_->isFieldPresent(sfInstanceParameters)) + return this->tx_->getFieldArray(sfInstanceParameters); + return std::nullopt; + } + + /** + * @brief Check if sfInstanceParameters is present. + * @return True if the field is present, false otherwise. + */ + [[nodiscard]] + bool + hasInstanceParameters() const + { + return this->tx_->isFieldPresent(sfInstanceParameters); + } + /** + * @brief Get sfInstanceParameterValues (soeOPTIONAL) + * @note This is an untyped field. + * @return The field value, or std::nullopt if not present. + */ + [[nodiscard]] + std::optional> + getInstanceParameterValues() const + { + if (this->tx_->isFieldPresent(sfInstanceParameterValues)) + return this->tx_->getFieldArray(sfInstanceParameterValues); + return std::nullopt; + } + + /** + * @brief Check if sfInstanceParameterValues is present. + * @return True if the field is present, false otherwise. + */ + [[nodiscard]] + bool + hasInstanceParameterValues() const + { + return this->tx_->isFieldPresent(sfInstanceParameterValues); + } + + /** + * @brief Get sfURI (soeOPTIONAL) + * @return The field value, or std::nullopt if not present. + */ + [[nodiscard]] + protocol_autogen::Optional + getURI() const + { + if (hasURI()) + { + return this->tx_->at(sfURI); + } + return std::nullopt; + } + + /** + * @brief Check if sfURI is present. + * @return True if the field is present, false otherwise. + */ + [[nodiscard]] + bool + hasURI() const + { + return this->tx_->isFieldPresent(sfURI); + } +}; + +/** + * @brief Builder for ContractCreate transactions. + * + * Provides a fluent interface for constructing transactions with method chaining. + * Uses Json::Value internally for flexible transaction construction. + * Inherits common field setters from TransactionBuilderBase. + */ +class ContractCreateBuilder : public TransactionBuilderBase +{ +public: + /** + * @brief Construct a new ContractCreateBuilder with required fields. + * @param account The account initiating the transaction. + * @param sequence Optional sequence number for the transaction. + * @param fee Optional fee for the transaction. + */ + ContractCreateBuilder(SF_ACCOUNT::type::value_type account, + std::optional sequence = std::nullopt, + std::optional fee = std::nullopt +) + : TransactionBuilderBase(ttCONTRACT_CREATE, account, sequence, fee) + { + } + + /** + * @brief Construct a ContractCreateBuilder from an existing STTx object. + * @param tx The existing transaction to copy from. + * @throws std::runtime_error if the transaction type doesn't match. + */ + ContractCreateBuilder(std::shared_ptr tx) + { + if (tx->getTxnType() != ttCONTRACT_CREATE) + { + throw std::runtime_error("Invalid transaction type for ContractCreateBuilder"); + } + object_ = *tx; + } + + /** @brief Transaction-specific field setters */ + + /** + * @brief Set sfContractCode (soeOPTIONAL) + * @return Reference to this builder for method chaining. + */ + ContractCreateBuilder& + setContractCode(std::decay_t const& value) + { + object_[sfContractCode] = value; + return *this; + } + + /** + * @brief Set sfContractHash (soeOPTIONAL) + * @return Reference to this builder for method chaining. + */ + ContractCreateBuilder& + setContractHash(std::decay_t const& value) + { + object_[sfContractHash] = value; + return *this; + } + + /** + * @brief Set sfFunctions (soeOPTIONAL) + * @return Reference to this builder for method chaining. + */ + ContractCreateBuilder& + setFunctions(STArray const& value) + { + object_.setFieldArray(sfFunctions, value); + return *this; + } + + /** + * @brief Set sfInstanceParameters (soeOPTIONAL) + * @return Reference to this builder for method chaining. + */ + ContractCreateBuilder& + setInstanceParameters(STArray const& value) + { + object_.setFieldArray(sfInstanceParameters, value); + return *this; + } + + /** + * @brief Set sfInstanceParameterValues (soeOPTIONAL) + * @return Reference to this builder for method chaining. + */ + ContractCreateBuilder& + setInstanceParameterValues(STArray const& value) + { + object_.setFieldArray(sfInstanceParameterValues, value); + return *this; + } + + /** + * @brief Set sfURI (soeOPTIONAL) + * @return Reference to this builder for method chaining. + */ + ContractCreateBuilder& + setURI(std::decay_t const& value) + { + object_[sfURI] = value; + return *this; + } + + /** + * @brief Build and return the ContractCreate wrapper. + * @param publicKey The public key for signing. + * @param secretKey The secret key for signing. + * @return The constructed transaction wrapper. + */ + ContractCreate + build(PublicKey const& publicKey, SecretKey const& secretKey) + { + sign(publicKey, secretKey); + return ContractCreate{std::make_shared(std::move(object_))}; + } +}; + +} // namespace xrpl::transactions diff --git a/include/xrpl/protocol_autogen/transactions/ContractDelete.h b/include/xrpl/protocol_autogen/transactions/ContractDelete.h new file mode 100644 index 0000000000..6d84efc2d1 --- /dev/null +++ b/include/xrpl/protocol_autogen/transactions/ContractDelete.h @@ -0,0 +1,129 @@ +// This file is auto-generated. Do not edit. +#pragma once + +#include +#include +#include +#include +#include +#include + +#include +#include + +namespace xrpl::transactions { + +class ContractDeleteBuilder; + +/** + * @brief Transaction: ContractDelete + * + * Type: ttCONTRACT_DELETE (87) + * Delegable: Delegation::delegable + * Amendment: featureSmartContract + * Privileges: mustDeleteAcct + * + * Immutable wrapper around STTx providing type-safe field access. + * Use ContractDeleteBuilder to construct new transactions. + */ +class ContractDelete : public TransactionBase +{ +public: + static constexpr xrpl::TxType txType = ttCONTRACT_DELETE; + + /** + * @brief Construct a ContractDelete transaction wrapper from an existing STTx object. + * @throws std::runtime_error if the transaction type doesn't match. + */ + explicit ContractDelete(std::shared_ptr tx) + : TransactionBase(std::move(tx)) + { + // Verify transaction type + if (tx_->getTxnType() != txType) + { + throw std::runtime_error("Invalid transaction type for ContractDelete"); + } + } + + // Transaction-specific field getters + + /** + * @brief Get sfContractAccount (soeREQUIRED) + * @return The field value. + */ + [[nodiscard]] + SF_ACCOUNT::type::value_type + getContractAccount() const + { + return this->tx_->at(sfContractAccount); + } +}; + +/** + * @brief Builder for ContractDelete transactions. + * + * Provides a fluent interface for constructing transactions with method chaining. + * Uses Json::Value internally for flexible transaction construction. + * Inherits common field setters from TransactionBuilderBase. + */ +class ContractDeleteBuilder : public TransactionBuilderBase +{ +public: + /** + * @brief Construct a new ContractDeleteBuilder with required fields. + * @param account The account initiating the transaction. + * @param contractAccount The sfContractAccount field value. + * @param sequence Optional sequence number for the transaction. + * @param fee Optional fee for the transaction. + */ + ContractDeleteBuilder(SF_ACCOUNT::type::value_type account, + std::decay_t const& contractAccount, std::optional sequence = std::nullopt, + std::optional fee = std::nullopt +) + : TransactionBuilderBase(ttCONTRACT_DELETE, account, sequence, fee) + { + setContractAccount(contractAccount); + } + + /** + * @brief Construct a ContractDeleteBuilder from an existing STTx object. + * @param tx The existing transaction to copy from. + * @throws std::runtime_error if the transaction type doesn't match. + */ + ContractDeleteBuilder(std::shared_ptr tx) + { + if (tx->getTxnType() != ttCONTRACT_DELETE) + { + throw std::runtime_error("Invalid transaction type for ContractDeleteBuilder"); + } + object_ = *tx; + } + + /** @brief Transaction-specific field setters */ + + /** + * @brief Set sfContractAccount (soeREQUIRED) + * @return Reference to this builder for method chaining. + */ + ContractDeleteBuilder& + setContractAccount(std::decay_t const& value) + { + object_[sfContractAccount] = value; + return *this; + } + + /** + * @brief Build and return the ContractDelete wrapper. + * @param publicKey The public key for signing. + * @param secretKey The secret key for signing. + * @return The constructed transaction wrapper. + */ + ContractDelete + build(PublicKey const& publicKey, SecretKey const& secretKey) + { + sign(publicKey, secretKey); + return ContractDelete{std::make_shared(std::move(object_))}; + } +}; + +} // namespace xrpl::transactions diff --git a/include/xrpl/protocol_autogen/transactions/ContractModify.h b/include/xrpl/protocol_autogen/transactions/ContractModify.h new file mode 100644 index 0000000000..20f252ac76 --- /dev/null +++ b/include/xrpl/protocol_autogen/transactions/ContractModify.h @@ -0,0 +1,395 @@ +// This file is auto-generated. Do not edit. +#pragma once + +#include +#include +#include +#include +#include +#include + +#include +#include + +namespace xrpl::transactions { + +class ContractModifyBuilder; + +/** + * @brief Transaction: ContractModify + * + * Type: ttCONTRACT_MODIFY (86) + * Delegable: Delegation::delegable + * Amendment: featureSmartContract + * Privileges: noPriv + * + * Immutable wrapper around STTx providing type-safe field access. + * Use ContractModifyBuilder to construct new transactions. + */ +class ContractModify : public TransactionBase +{ +public: + static constexpr xrpl::TxType txType = ttCONTRACT_MODIFY; + + /** + * @brief Construct a ContractModify transaction wrapper from an existing STTx object. + * @throws std::runtime_error if the transaction type doesn't match. + */ + explicit ContractModify(std::shared_ptr tx) + : TransactionBase(std::move(tx)) + { + // Verify transaction type + if (tx_->getTxnType() != txType) + { + throw std::runtime_error("Invalid transaction type for ContractModify"); + } + } + + // Transaction-specific field getters + + /** + * @brief Get sfContractAccount (soeOPTIONAL) + * @return The field value, or std::nullopt if not present. + */ + [[nodiscard]] + protocol_autogen::Optional + getContractAccount() const + { + if (hasContractAccount()) + { + return this->tx_->at(sfContractAccount); + } + return std::nullopt; + } + + /** + * @brief Check if sfContractAccount is present. + * @return True if the field is present, false otherwise. + */ + [[nodiscard]] + bool + hasContractAccount() const + { + return this->tx_->isFieldPresent(sfContractAccount); + } + + /** + * @brief Get sfOwner (soeOPTIONAL) + * @return The field value, or std::nullopt if not present. + */ + [[nodiscard]] + protocol_autogen::Optional + getOwner() const + { + if (hasOwner()) + { + return this->tx_->at(sfOwner); + } + return std::nullopt; + } + + /** + * @brief Check if sfOwner is present. + * @return True if the field is present, false otherwise. + */ + [[nodiscard]] + bool + hasOwner() const + { + return this->tx_->isFieldPresent(sfOwner); + } + + /** + * @brief Get sfContractCode (soeOPTIONAL) + * @return The field value, or std::nullopt if not present. + */ + [[nodiscard]] + protocol_autogen::Optional + getContractCode() const + { + if (hasContractCode()) + { + return this->tx_->at(sfContractCode); + } + return std::nullopt; + } + + /** + * @brief Check if sfContractCode is present. + * @return True if the field is present, false otherwise. + */ + [[nodiscard]] + bool + hasContractCode() const + { + return this->tx_->isFieldPresent(sfContractCode); + } + + /** + * @brief Get sfContractHash (soeOPTIONAL) + * @return The field value, or std::nullopt if not present. + */ + [[nodiscard]] + protocol_autogen::Optional + getContractHash() const + { + if (hasContractHash()) + { + return this->tx_->at(sfContractHash); + } + return std::nullopt; + } + + /** + * @brief Check if sfContractHash is present. + * @return True if the field is present, false otherwise. + */ + [[nodiscard]] + bool + hasContractHash() const + { + return this->tx_->isFieldPresent(sfContractHash); + } + /** + * @brief Get sfFunctions (soeOPTIONAL) + * @note This is an untyped field. + * @return The field value, or std::nullopt if not present. + */ + [[nodiscard]] + std::optional> + getFunctions() const + { + if (this->tx_->isFieldPresent(sfFunctions)) + return this->tx_->getFieldArray(sfFunctions); + return std::nullopt; + } + + /** + * @brief Check if sfFunctions is present. + * @return True if the field is present, false otherwise. + */ + [[nodiscard]] + bool + hasFunctions() const + { + return this->tx_->isFieldPresent(sfFunctions); + } + /** + * @brief Get sfInstanceParameters (soeOPTIONAL) + * @note This is an untyped field. + * @return The field value, or std::nullopt if not present. + */ + [[nodiscard]] + std::optional> + getInstanceParameters() const + { + if (this->tx_->isFieldPresent(sfInstanceParameters)) + return this->tx_->getFieldArray(sfInstanceParameters); + return std::nullopt; + } + + /** + * @brief Check if sfInstanceParameters is present. + * @return True if the field is present, false otherwise. + */ + [[nodiscard]] + bool + hasInstanceParameters() const + { + return this->tx_->isFieldPresent(sfInstanceParameters); + } + /** + * @brief Get sfInstanceParameterValues (soeOPTIONAL) + * @note This is an untyped field. + * @return The field value, or std::nullopt if not present. + */ + [[nodiscard]] + std::optional> + getInstanceParameterValues() const + { + if (this->tx_->isFieldPresent(sfInstanceParameterValues)) + return this->tx_->getFieldArray(sfInstanceParameterValues); + return std::nullopt; + } + + /** + * @brief Check if sfInstanceParameterValues is present. + * @return True if the field is present, false otherwise. + */ + [[nodiscard]] + bool + hasInstanceParameterValues() const + { + return this->tx_->isFieldPresent(sfInstanceParameterValues); + } + + /** + * @brief Get sfURI (soeOPTIONAL) + * @return The field value, or std::nullopt if not present. + */ + [[nodiscard]] + protocol_autogen::Optional + getURI() const + { + if (hasURI()) + { + return this->tx_->at(sfURI); + } + return std::nullopt; + } + + /** + * @brief Check if sfURI is present. + * @return True if the field is present, false otherwise. + */ + [[nodiscard]] + bool + hasURI() const + { + return this->tx_->isFieldPresent(sfURI); + } +}; + +/** + * @brief Builder for ContractModify transactions. + * + * Provides a fluent interface for constructing transactions with method chaining. + * Uses Json::Value internally for flexible transaction construction. + * Inherits common field setters from TransactionBuilderBase. + */ +class ContractModifyBuilder : public TransactionBuilderBase +{ +public: + /** + * @brief Construct a new ContractModifyBuilder with required fields. + * @param account The account initiating the transaction. + * @param sequence Optional sequence number for the transaction. + * @param fee Optional fee for the transaction. + */ + ContractModifyBuilder(SF_ACCOUNT::type::value_type account, + std::optional sequence = std::nullopt, + std::optional fee = std::nullopt +) + : TransactionBuilderBase(ttCONTRACT_MODIFY, account, sequence, fee) + { + } + + /** + * @brief Construct a ContractModifyBuilder from an existing STTx object. + * @param tx The existing transaction to copy from. + * @throws std::runtime_error if the transaction type doesn't match. + */ + ContractModifyBuilder(std::shared_ptr tx) + { + if (tx->getTxnType() != ttCONTRACT_MODIFY) + { + throw std::runtime_error("Invalid transaction type for ContractModifyBuilder"); + } + object_ = *tx; + } + + /** @brief Transaction-specific field setters */ + + /** + * @brief Set sfContractAccount (soeOPTIONAL) + * @return Reference to this builder for method chaining. + */ + ContractModifyBuilder& + setContractAccount(std::decay_t const& value) + { + object_[sfContractAccount] = value; + return *this; + } + + /** + * @brief Set sfOwner (soeOPTIONAL) + * @return Reference to this builder for method chaining. + */ + ContractModifyBuilder& + setOwner(std::decay_t const& value) + { + object_[sfOwner] = value; + return *this; + } + + /** + * @brief Set sfContractCode (soeOPTIONAL) + * @return Reference to this builder for method chaining. + */ + ContractModifyBuilder& + setContractCode(std::decay_t const& value) + { + object_[sfContractCode] = value; + return *this; + } + + /** + * @brief Set sfContractHash (soeOPTIONAL) + * @return Reference to this builder for method chaining. + */ + ContractModifyBuilder& + setContractHash(std::decay_t const& value) + { + object_[sfContractHash] = value; + return *this; + } + + /** + * @brief Set sfFunctions (soeOPTIONAL) + * @return Reference to this builder for method chaining. + */ + ContractModifyBuilder& + setFunctions(STArray const& value) + { + object_.setFieldArray(sfFunctions, value); + return *this; + } + + /** + * @brief Set sfInstanceParameters (soeOPTIONAL) + * @return Reference to this builder for method chaining. + */ + ContractModifyBuilder& + setInstanceParameters(STArray const& value) + { + object_.setFieldArray(sfInstanceParameters, value); + return *this; + } + + /** + * @brief Set sfInstanceParameterValues (soeOPTIONAL) + * @return Reference to this builder for method chaining. + */ + ContractModifyBuilder& + setInstanceParameterValues(STArray const& value) + { + object_.setFieldArray(sfInstanceParameterValues, value); + return *this; + } + + /** + * @brief Set sfURI (soeOPTIONAL) + * @return Reference to this builder for method chaining. + */ + ContractModifyBuilder& + setURI(std::decay_t const& value) + { + object_[sfURI] = value; + return *this; + } + + /** + * @brief Build and return the ContractModify wrapper. + * @param publicKey The public key for signing. + * @param secretKey The secret key for signing. + * @return The constructed transaction wrapper. + */ + ContractModify + build(PublicKey const& publicKey, SecretKey const& secretKey) + { + sign(publicKey, secretKey); + return ContractModify{std::make_shared(std::move(object_))}; + } +}; + +} // namespace xrpl::transactions diff --git a/include/xrpl/protocol_autogen/transactions/ContractUserDelete.h b/include/xrpl/protocol_autogen/transactions/ContractUserDelete.h new file mode 100644 index 0000000000..09f41523d9 --- /dev/null +++ b/include/xrpl/protocol_autogen/transactions/ContractUserDelete.h @@ -0,0 +1,212 @@ +// This file is auto-generated. Do not edit. +#pragma once + +#include +#include +#include +#include +#include +#include + +#include +#include + +namespace xrpl::transactions { + +class ContractUserDeleteBuilder; + +/** + * @brief Transaction: ContractUserDelete + * + * Type: ttCONTRACT_USER_DELETE (89) + * Delegable: Delegation::delegable + * Amendment: featureSmartContract + * Privileges: noPriv + * + * Immutable wrapper around STTx providing type-safe field access. + * Use ContractUserDeleteBuilder to construct new transactions. + */ +class ContractUserDelete : public TransactionBase +{ +public: + static constexpr xrpl::TxType txType = ttCONTRACT_USER_DELETE; + + /** + * @brief Construct a ContractUserDelete transaction wrapper from an existing STTx object. + * @throws std::runtime_error if the transaction type doesn't match. + */ + explicit ContractUserDelete(std::shared_ptr tx) + : TransactionBase(std::move(tx)) + { + // Verify transaction type + if (tx_->getTxnType() != txType) + { + throw std::runtime_error("Invalid transaction type for ContractUserDelete"); + } + } + + // Transaction-specific field getters + + /** + * @brief Get sfContractAccount (soeREQUIRED) + * @return The field value. + */ + [[nodiscard]] + SF_ACCOUNT::type::value_type + getContractAccount() const + { + return this->tx_->at(sfContractAccount); + } + + /** + * @brief Get sfFunctionName (soeREQUIRED) + * @return The field value. + */ + [[nodiscard]] + SF_VL::type::value_type + getFunctionName() const + { + return this->tx_->at(sfFunctionName); + } + /** + * @brief Get sfParameters (soeOPTIONAL) + * @note This is an untyped field. + * @return The field value, or std::nullopt if not present. + */ + [[nodiscard]] + std::optional> + getParameters() const + { + if (this->tx_->isFieldPresent(sfParameters)) + return this->tx_->getFieldArray(sfParameters); + return std::nullopt; + } + + /** + * @brief Check if sfParameters is present. + * @return True if the field is present, false otherwise. + */ + [[nodiscard]] + bool + hasParameters() const + { + return this->tx_->isFieldPresent(sfParameters); + } + + /** + * @brief Get sfComputationAllowance (soeREQUIRED) + * @return The field value. + */ + [[nodiscard]] + SF_UINT32::type::value_type + getComputationAllowance() const + { + return this->tx_->at(sfComputationAllowance); + } +}; + +/** + * @brief Builder for ContractUserDelete transactions. + * + * Provides a fluent interface for constructing transactions with method chaining. + * Uses Json::Value internally for flexible transaction construction. + * Inherits common field setters from TransactionBuilderBase. + */ +class ContractUserDeleteBuilder : public TransactionBuilderBase +{ +public: + /** + * @brief Construct a new ContractUserDeleteBuilder with required fields. + * @param account The account initiating the transaction. + * @param contractAccount The sfContractAccount field value. + * @param functionName The sfFunctionName field value. + * @param computationAllowance The sfComputationAllowance field value. + * @param sequence Optional sequence number for the transaction. + * @param fee Optional fee for the transaction. + */ + ContractUserDeleteBuilder(SF_ACCOUNT::type::value_type account, + std::decay_t const& contractAccount, std::decay_t const& functionName, std::decay_t const& computationAllowance, std::optional sequence = std::nullopt, + std::optional fee = std::nullopt +) + : TransactionBuilderBase(ttCONTRACT_USER_DELETE, account, sequence, fee) + { + setContractAccount(contractAccount); + setFunctionName(functionName); + setComputationAllowance(computationAllowance); + } + + /** + * @brief Construct a ContractUserDeleteBuilder from an existing STTx object. + * @param tx The existing transaction to copy from. + * @throws std::runtime_error if the transaction type doesn't match. + */ + ContractUserDeleteBuilder(std::shared_ptr tx) + { + if (tx->getTxnType() != ttCONTRACT_USER_DELETE) + { + throw std::runtime_error("Invalid transaction type for ContractUserDeleteBuilder"); + } + object_ = *tx; + } + + /** @brief Transaction-specific field setters */ + + /** + * @brief Set sfContractAccount (soeREQUIRED) + * @return Reference to this builder for method chaining. + */ + ContractUserDeleteBuilder& + setContractAccount(std::decay_t const& value) + { + object_[sfContractAccount] = value; + return *this; + } + + /** + * @brief Set sfFunctionName (soeREQUIRED) + * @return Reference to this builder for method chaining. + */ + ContractUserDeleteBuilder& + setFunctionName(std::decay_t const& value) + { + object_[sfFunctionName] = value; + return *this; + } + + /** + * @brief Set sfParameters (soeOPTIONAL) + * @return Reference to this builder for method chaining. + */ + ContractUserDeleteBuilder& + setParameters(STArray const& value) + { + object_.setFieldArray(sfParameters, value); + return *this; + } + + /** + * @brief Set sfComputationAllowance (soeREQUIRED) + * @return Reference to this builder for method chaining. + */ + ContractUserDeleteBuilder& + setComputationAllowance(std::decay_t const& value) + { + object_[sfComputationAllowance] = value; + return *this; + } + + /** + * @brief Build and return the ContractUserDelete wrapper. + * @param publicKey The public key for signing. + * @param secretKey The secret key for signing. + * @return The constructed transaction wrapper. + */ + ContractUserDelete + build(PublicKey const& publicKey, SecretKey const& secretKey) + { + sign(publicKey, secretKey); + return ContractUserDelete{std::make_shared(std::move(object_))}; + } +}; + +} // namespace xrpl::transactions diff --git a/include/xrpl/server/InfoSub.h b/include/xrpl/server/InfoSub.h index d45f4d7740..dc0bf70bcf 100644 --- a/include/xrpl/server/InfoSub.h +++ b/include/xrpl/server/InfoSub.h @@ -148,6 +148,11 @@ public: virtual bool unsubConsensus(std::uint64_t uListener) = 0; + virtual bool + subContractEvent(ref ispListener) = 0; + virtual bool + unsubContractEvent(std::uint64_t uListener) = 0; + // VFALCO TODO Remove // This was added for one particular partner, it // "pushes" subscription data to a particular URL. diff --git a/include/xrpl/server/NetworkOPs.h b/include/xrpl/server/NetworkOPs.h index 75f1e0e1b2..21a51a2862 100644 --- a/include/xrpl/server/NetworkOPs.h +++ b/include/xrpl/server/NetworkOPs.h @@ -247,6 +247,9 @@ public: virtual void pubValidation(std::shared_ptr const& val) = 0; + virtual void + pubContractEvent(std::string const& name, STJson const& event) = 0; + virtual void stateAccounting(Json::Value& obj) = 0; }; diff --git a/include/xrpl/tx/ApplyContext.h b/include/xrpl/tx/ApplyContext.h index 61e738b38f..a5cb97d9ac 100644 --- a/include/xrpl/tx/ApplyContext.h +++ b/include/xrpl/tx/ApplyContext.h @@ -3,10 +3,12 @@ #include #include #include +#include #include #include #include +#include namespace xrpl { @@ -43,6 +45,12 @@ public: XRPAmount const baseFee; beast::Journal const journal; + OpenView& + openView() + { + return base_.view(); + } + ApplyView& view() { @@ -89,10 +97,27 @@ public: wasmReturnCode_ = wasmReturnCode; } + /** Sets the gas used in the metadata */ + void + setEmittedTxns(std::queue> const emittedTxns) + { + emittedTxns_ = emittedTxns; + } + + std::queue> + getEmittedTxns() + { + return emittedTxns_; + } + /** Discard changes and start fresh. */ void discard(); + /** Finalize changes. */ + void + finalize(); + /** Apply the transaction result to the base. */ std::optional apply(TER); @@ -132,14 +157,14 @@ private: TER checkInvariantsHelper(TER const result, XRPAmount const fee, std::index_sequence); - OpenView& base_; + OpenViewSandbox base_; ApplyFlags flags_; std::optional view_; - // The ID of the batch transaction we are executing under, if seated. - std::optional parentBatchId_; std::optional gasUsed_; std::optional wasmReturnCode_; + std::queue> emittedTxns_; + std::optional parentBatchId_; }; } // namespace xrpl diff --git a/include/xrpl/tx/Transactor.h b/include/xrpl/tx/Transactor.h index 287f785cd7..fa54e25a1c 100644 --- a/include/xrpl/tx/Transactor.h +++ b/include/xrpl/tx/Transactor.h @@ -297,6 +297,9 @@ private: std::pair reset(XRPAmount fee); + std::pair + checkInvariants(TER result, XRPAmount fee); + TER consumeSeqProxy(SLE::pointer const& sleAccount); TER diff --git a/include/xrpl/tx/apply.h b/include/xrpl/tx/apply.h index 49b30fea02..96b5d73471 100644 --- a/include/xrpl/tx/apply.h +++ b/include/xrpl/tx/apply.h @@ -9,6 +9,8 @@ namespace xrpl { +class Application; + class HashRouter; class ServiceRegistry; @@ -102,6 +104,24 @@ apply( ApplyFlags flags, beast::Journal journal); +ApplyResult +apply( + ServiceRegistry& registry, + OpenView& view, + uint256 const& parentBatchId, + STTx const& tx, + ApplyFlags flags, + beast::Journal j); + +ApplyResult +apply( + Application& app, + OpenView& view, + uint256 const& parentBatchId, + STTx const& tx, + ApplyFlags flags, + beast::Journal j); + /** Enum class for return value from `applyTransaction` @see applyTransaction diff --git a/include/xrpl/tx/transactors/DeleteUtils.h b/include/xrpl/tx/transactors/DeleteUtils.h new file mode 100644 index 0000000000..6bcca1fb5f --- /dev/null +++ b/include/xrpl/tx/transactors/DeleteUtils.h @@ -0,0 +1,42 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace xrpl { + +// Define a function pointer type that can be used to delete ledger node types. +using DeleterFuncPtr = TER (*)( + ServiceRegistry& registry, + ApplyView& view, + AccountID const& account, + uint256 const& delIndex, + std::shared_ptr const& sleDel, + beast::Journal j); + +DeleterFuncPtr +nonObligationDeleter(LedgerEntryType t); + +TER +deletePreclaim( + PreclaimContext const& ctx, + std::uint32_t seqDelta, + AccountID const account, + AccountID const dest, + bool isPseudoAccount = false); + +TER +deleteDoApply( + ApplyContext& applyCtx, + STAmount const& accountBalance, + AccountID const& account, + AccountID const& dest); + +} // namespace xrpl diff --git a/include/xrpl/tx/transactors/contract/ContractCall.h b/include/xrpl/tx/transactors/contract/ContractCall.h new file mode 100644 index 0000000000..66536494dc --- /dev/null +++ b/include/xrpl/tx/transactors/contract/ContractCall.h @@ -0,0 +1,29 @@ +#pragma once + +#include + +namespace xrpl { + +class ContractCall : public Transactor +{ +public: + static constexpr ConsequencesFactoryType ConsequencesFactory{Normal}; + + explicit ContractCall(ApplyContext& ctx) : Transactor(ctx) + { + } + + static XRPAmount + calculateBaseFee(ReadView const& view, STTx const& tx); + + static NotTEC + preflight(PreflightContext const& ctx); + + static TER + preclaim(PreclaimContext const& ctx); + + TER + doApply() override; +}; + +} // namespace xrpl diff --git a/include/xrpl/tx/transactors/contract/ContractClawback.h b/include/xrpl/tx/transactors/contract/ContractClawback.h new file mode 100644 index 0000000000..b47f603b88 --- /dev/null +++ b/include/xrpl/tx/transactors/contract/ContractClawback.h @@ -0,0 +1,26 @@ +#pragma once + +#include + +namespace xrpl { + +class ContractClawback : public Transactor +{ +public: + static constexpr ConsequencesFactoryType ConsequencesFactory{Normal}; + + explicit ContractClawback(ApplyContext& ctx) : Transactor(ctx) + { + } + + static NotTEC + preflight(PreflightContext const& ctx); + + static TER + preclaim(PreclaimContext const& ctx); + + TER + doApply() override; +}; + +} // namespace xrpl diff --git a/include/xrpl/tx/transactors/contract/ContractCreate.h b/include/xrpl/tx/transactors/contract/ContractCreate.h new file mode 100644 index 0000000000..8f69a8ab57 --- /dev/null +++ b/include/xrpl/tx/transactors/contract/ContractCreate.h @@ -0,0 +1,32 @@ +#pragma once + +#include + +namespace xrpl { + +class ContractCreate : public Transactor +{ +public: + static constexpr ConsequencesFactoryType ConsequencesFactory{Normal}; + + explicit ContractCreate(ApplyContext& ctx) : Transactor(ctx) + { + } + + static XRPAmount + calculateBaseFee(ReadView const& view, STTx const& tx); + + static std::uint32_t + getFlagsMask(PreflightContext const& ctx); + + static NotTEC + preflight(PreflightContext const& ctx); + + static TER + preclaim(PreclaimContext const& ctx); + + TER + doApply() override; +}; + +} // namespace xrpl diff --git a/include/xrpl/tx/transactors/contract/ContractDelete.h b/include/xrpl/tx/transactors/contract/ContractDelete.h new file mode 100644 index 0000000000..ed1d9f81e9 --- /dev/null +++ b/include/xrpl/tx/transactors/contract/ContractDelete.h @@ -0,0 +1,34 @@ +#pragma once + +#include + +namespace xrpl { + +class ContractDelete : public Transactor +{ +public: + static constexpr ConsequencesFactoryType ConsequencesFactory{Normal}; + + explicit ContractDelete(ApplyContext& ctx) : Transactor(ctx) + { + } + + static NotTEC + preflight(PreflightContext const& ctx); + + static TER + preclaim(PreclaimContext const& ctx); + + // Interface used by DeleteAccount + static TER + deleteContract( + ApplyView& view, + std::shared_ptr const& sle, + AccountID const& account, + beast::Journal j); + + TER + doApply() override; +}; + +} // namespace xrpl diff --git a/include/xrpl/tx/transactors/contract/ContractModify.h b/include/xrpl/tx/transactors/contract/ContractModify.h new file mode 100644 index 0000000000..025fb36355 --- /dev/null +++ b/include/xrpl/tx/transactors/contract/ContractModify.h @@ -0,0 +1,29 @@ +#pragma once + +#include + +namespace xrpl { + +class ContractModify : public Transactor +{ +public: + static constexpr ConsequencesFactoryType ConsequencesFactory{Normal}; + + explicit ContractModify(ApplyContext& ctx) : Transactor(ctx) + { + } + + static XRPAmount + calculateBaseFee(ReadView const& view, STTx const& tx); + + static NotTEC + preflight(PreflightContext const& ctx); + + static TER + preclaim(PreclaimContext const& ctx); + + TER + doApply() override; +}; + +} // namespace xrpl diff --git a/include/xrpl/tx/transactors/contract/ContractUserDelete.h b/include/xrpl/tx/transactors/contract/ContractUserDelete.h new file mode 100644 index 0000000000..c525242c57 --- /dev/null +++ b/include/xrpl/tx/transactors/contract/ContractUserDelete.h @@ -0,0 +1,26 @@ +#pragma once + +#include + +namespace xrpl { + +class ContractUserDelete : public Transactor +{ +public: + static constexpr ConsequencesFactoryType ConsequencesFactory{Normal}; + + explicit ContractUserDelete(ApplyContext& ctx) : Transactor(ctx) + { + } + + static NotTEC + preflight(PreflightContext const& ctx); + + static TER + preclaim(PreclaimContext const& ctx); + + TER + doApply() override; +}; + +} // namespace xrpl diff --git a/include/xrpl/tx/transactors/nft/NFTokenMint.h b/include/xrpl/tx/transactors/nft/NFTokenMint.h index d4eeba2bf0..107d327a02 100644 --- a/include/xrpl/tx/transactors/nft/NFTokenMint.h +++ b/include/xrpl/tx/transactors/nft/NFTokenMint.h @@ -1,8 +1,8 @@ #pragma once +#include #include #include -#include namespace xrpl { diff --git a/include/xrpl/tx/wasm/ContractContext.h b/include/xrpl/tx/wasm/ContractContext.h new file mode 100644 index 0000000000..8c21405784 --- /dev/null +++ b/include/xrpl/tx/wasm/ContractContext.h @@ -0,0 +1,72 @@ +#pragma once + +#include +#include +#include +#include + +#include + +namespace xrpl { + +struct ParameterValueVec +{ + STData const value; +}; + +struct FunctionParameterValueVecWithName +{ + Blob const name; + STData const value; +}; + +struct ParameterTypeVec +{ + STDataType const type; +}; + +std::vector +getParameterValueVec(STArray const& functionParameters); + +std::vector +getParameterTypeVec(STArray const& functionParameters); + +enum ExitType : uint8_t { + UNSET = 0, + WASM_ERROR = 1, + ROLLBACK = 2, + ACCEPT = 3, +}; + +struct ContractResult +{ + uint256 const contractHash; // Hash of the contract code + Keylet const contractKeylet; // Keylet for the contract instance + Keylet const contractSourceKeylet; // Keylet for the contract source + Keylet const contractAccountKeylet; // Keylet for the contract account + AccountID const contractAccount; // AccountID of the contract account + std::uint32_t nextSequence; // Next sequence number for the contract account + AccountID const otxnAccount; // AccountID for the originating transaction + uint256 const otxnId; // ID for the originating transaction + std::string exitReason{""}; + int64_t exitCode{-1}; + ContractDataMap dataMap; + ContractEventMap eventMap; + std::queue> emittedTxns{}; + std::size_t changedDataCount{0}; +}; + +struct ContractContext +{ + ApplyContext& applyCtx; + std::vector instanceParameters; + std::vector functionParameters; + std::vector built_txns; + int64_t expected_etxn_count{-1}; // expected emitted transaction count + std::map nonce_used{}; // nonces used in this execution + uint32_t generation = 0; // generation of the contract being executed + uint64_t burden = 0; // computational burden used + ContractResult result; +}; + +} // namespace xrpl diff --git a/include/xrpl/tx/wasm/ContractHostFuncImpl.h b/include/xrpl/tx/wasm/ContractHostFuncImpl.h new file mode 100644 index 0000000000..a243bde647 --- /dev/null +++ b/include/xrpl/tx/wasm/ContractHostFuncImpl.h @@ -0,0 +1,95 @@ +#pragma once + +#include +#include +#include + +namespace xrpl { +class ContractHostFunctionsImpl : public WasmHostFunctionsImpl +{ + ContractContext& contractCtx; + uint256 const contractId = contractCtx.result.contractKeylet.key; + +public: + // Constructor for contract-specific functionality + ContractHostFunctionsImpl(ContractContext& contractContext) + : WasmHostFunctionsImpl(contractContext.applyCtx, contractContext.result.contractKeylet) + , contractCtx(contractContext) + { + } + + // Expected + // getFieldBytesFromSTData(xrpl::STData const& funcParam, std::uint32_t + // stTypeId); + + Expected + instanceParam(std::uint32_t index, std::uint32_t stTypeId) override; + + Expected + functionParam(std::uint32_t index, std::uint32_t stTypeId) override; + + Expected + getDataObjectField(AccountID const& account, std::string_view const& key) override; + + Expected + getDataNestedObjectField( + AccountID const& account, + std::string_view const& key, + std::string_view const& nestedKey) override; + + Expected + getDataArrayElementField(AccountID const& account, size_t index, std::string_view const& key) + override; + + Expected + getDataNestedArrayElementField( + AccountID const& account, + std::string_view const& key, + size_t index, + std::string_view const& nestedKey) override; + + Expected + setDataObjectField( + AccountID const& account, + std::string_view const& key, + STJson::Value const& value) override; + + Expected + setDataNestedObjectField( + AccountID const& account, + std::string_view const& nestedKey, + std::string_view const& key, + STJson::Value const& value) override; + + Expected + setDataArrayElementField( + AccountID const& account, + size_t index, + std::string_view const& key, + STJson::Value const& value) override; + + Expected + setDataNestedArrayElementField( + AccountID const& account, + std::string_view const& key, + size_t index, + std::string_view const& nestedKey, + STJson::Value const& value) override; + + Expected + buildTxn(std::uint16_t const& txType) override; + + Expected + addTxnField(std::uint32_t const& index, SField const& field, Slice const& data) override; + + Expected + emitBuiltTxn(std::uint32_t const& index) override; + + Expected + emitTxn(std::shared_ptr const& stxPtr) override; + + Expected + emitEvent(std::string_view const& eventName, STJson const& eventData) override; +}; + +} // namespace xrpl diff --git a/include/xrpl/tx/wasm/HostFunc.h b/include/xrpl/tx/wasm/HostFunc.h index a3a26cfbaf..4ed9e7b366 100644 --- a/include/xrpl/tx/wasm/HostFunc.h +++ b/include/xrpl/tx/wasm/HostFunc.h @@ -6,6 +6,8 @@ #include #include #include +#include +#include #include #include #include @@ -13,6 +15,7 @@ namespace xrpl { enum class HostFunctionError : int32_t { + SUCCESS = 0, INTERNAL = -1, FIELD_NOT_FOUND = -2, BUFFER_TOO_SMALL = -3, @@ -35,6 +38,8 @@ enum class HostFunctionError : int32_t { FLOAT_COMPUTATION_ERROR = -20, NO_RUNTIME = -21, OUT_OF_GAS = -22, + SUBMIT_TXN_FAILURE = -23, + INVALID_STATE = -24, }; inline int32_t @@ -494,6 +499,119 @@ struct HostFunctions return Unexpected(HostFunctionError::INTERNAL); } + virtual Expected + instanceParam(std::uint32_t index, std::uint32_t stTypeId) + { + return Unexpected(HostFunctionError::INTERNAL); + } + + virtual Expected + functionParam(std::uint32_t index, std::uint32_t stTypeId) + { + return Unexpected(HostFunctionError::INTERNAL); + } + + virtual Expected + getDataObjectField(AccountID const& account, std::string_view const& key) + { + return Unexpected(HostFunctionError::INTERNAL); + } + + virtual Expected + getDataNestedObjectField( + AccountID const& account, + std::string_view const& key, + std::string_view const& nestedKey) + { + return Unexpected(HostFunctionError::INTERNAL); + } + + virtual Expected + getDataArrayElementField(AccountID const& account, size_t index, std::string_view const& key) + { + return Unexpected(HostFunctionError::INTERNAL); + } + + virtual Expected + getDataNestedArrayElementField( + AccountID const& account, + std::string_view const& key, + size_t index, + std::string_view const& nestedKey) + { + return Unexpected(HostFunctionError::INTERNAL); + } + + virtual Expected + setDataObjectField( + AccountID const& account, + std::string_view const& keyName, + STJson::Value const& value) + { + return Unexpected(HostFunctionError::INTERNAL); + } + + virtual Expected + setDataNestedObjectField( + AccountID const& account, + std::string_view const& nestedKey, + std::string_view const& key, + STJson::Value const& value) + { + return Unexpected(HostFunctionError::INTERNAL); + } + + virtual Expected + setDataArrayElementField( + AccountID const& account, + size_t index, + std::string_view const& key, + STJson::Value const& value) + { + return Unexpected(HostFunctionError::INTERNAL); + } + + virtual Expected + setDataNestedArrayElementField( + AccountID const& account, + std::string_view const& key, + size_t index, + std::string_view const& nestedKey, + STJson::Value const& value) + { + return Unexpected(HostFunctionError::INTERNAL); + } + + virtual Expected + buildTxn(std::uint16_t const& txType) + { + return Unexpected(HostFunctionError::INTERNAL); + } + + virtual Expected + addTxnField(std::uint32_t const& index, SField const& field, Slice const& data) + { + return Unexpected(HostFunctionError::INTERNAL); + } + + virtual Expected + emitBuiltTxn(std::uint32_t const& index) + { + return Unexpected(HostFunctionError::INTERNAL); + } + + virtual Expected + emitTxn(std::shared_ptr const& stxPtr) + { + return Unexpected(HostFunctionError::INTERNAL); + } + + virtual Expected + emitEvent(std::string_view const& eventName, STJson const& eventData) + { + return Unexpected(HostFunctionError::INTERNAL); + } + virtual ~HostFunctions() = default; // LCOV_EXCL_STOP }; diff --git a/include/xrpl/tx/wasm/HostFuncWrapper.h b/include/xrpl/tx/wasm/HostFuncWrapper.h index 444c3be97c..ad849ffe7f 100644 --- a/include/xrpl/tx/wasm/HostFuncWrapper.h +++ b/include/xrpl/tx/wasm/HostFuncWrapper.h @@ -300,4 +300,96 @@ using floatLog_proto = int32_t(uint8_t const*, int32_t, uint8_t*, int32_t, int32 wasm_trap_t* floatLog_wrap(void* env, wasm_val_vec_t const* params, wasm_val_vec_t* results); +// Contract-specific host function wrappers + +using instanceParam_proto = int32_t(int32_t, int32_t, uint8_t*, int32_t); +wasm_trap_t* +instanceParam_wrap(void* env, wasm_val_vec_t const* params, wasm_val_vec_t* results); + +using functionParam_proto = int32_t(int32_t, int32_t, uint8_t*, int32_t); +wasm_trap_t* +functionParam_wrap(void* env, wasm_val_vec_t const* params, wasm_val_vec_t* results); + +using getDataObjectField_proto = + int32_t(uint8_t*, int32_t, uint8_t const*, int32_t, uint8_t*, int32_t); +wasm_trap_t* +getDataObjectField_wrap(void* env, wasm_val_vec_t const* params, wasm_val_vec_t* results); + +using getDataNestedObjectField_proto = + int32_t(uint8_t*, int32_t, uint8_t const*, int32_t, uint8_t const*, int32_t, uint8_t*, int32_t); +wasm_trap_t* +getDataNestedObjectField_wrap(void* env, wasm_val_vec_t const* params, wasm_val_vec_t* results); + +using setDataObjectField_proto = + int32_t(uint8_t*, int32_t, uint8_t const*, int32_t, uint8_t*, int32_t); +wasm_trap_t* +setDataObjectField_wrap(void* env, wasm_val_vec_t const* params, wasm_val_vec_t* results); + +using setDataNestedObjectField_proto = + int32_t(uint8_t*, int32_t, uint8_t const*, int32_t, uint8_t const*, int32_t, uint8_t*, int32_t); +wasm_trap_t* +setDataNestedObjectField_wrap(void* env, wasm_val_vec_t const* params, wasm_val_vec_t* results); + +using getDataArrayElementField_proto = + int32_t(uint8_t*, int32_t, int32_t, uint8_t const*, int32_t, uint8_t*, int32_t); +wasm_trap_t* +getDataArrayElementField_wrap(void* env, wasm_val_vec_t const* params, wasm_val_vec_t* results); + +using getDataNestedArrayElementField_proto = int32_t( + uint8_t*, + int32_t, + uint8_t const*, + int32_t, + int32_t, + uint8_t const*, + int32_t, + uint8_t*, + int32_t); +wasm_trap_t* +getDataNestedArrayElementField_wrap( + void* env, + wasm_val_vec_t const* params, + wasm_val_vec_t* results); + +using setDataArrayElementField_proto = + int32_t(uint8_t*, int32_t, int32_t, uint8_t const*, int32_t, uint8_t*, int32_t); +wasm_trap_t* +setDataArrayElementField_wrap(void* env, wasm_val_vec_t const* params, wasm_val_vec_t* results); + +using setDataNestedArrayElementField_proto = int32_t( + uint8_t*, + int32_t, + uint8_t const*, + int32_t, + int32_t, + uint8_t const*, + int32_t, + uint8_t*, + int32_t); +wasm_trap_t* +setDataNestedArrayElementField_wrap( + void* env, + wasm_val_vec_t const* params, + wasm_val_vec_t* results); + +using buildTxn_proto = int32_t(int32_t); +wasm_trap_t* +buildTxn_wrap(void* env, wasm_val_vec_t const* params, wasm_val_vec_t* results); + +using addTxnField_proto = int32_t(int32_t, int32_t, uint8_t const*, int32_t); +wasm_trap_t* +addTxnField_wrap(void* env, wasm_val_vec_t const* params, wasm_val_vec_t* results); + +using emitBuiltTxn_proto = int32_t(int32_t); +wasm_trap_t* +emitBuiltTxn_wrap(void* env, wasm_val_vec_t const* params, wasm_val_vec_t* results); + +using emitTxn_proto = int32_t(uint8_t const*, int32_t); +wasm_trap_t* +emitTxn_wrap(void* env, wasm_val_vec_t const* params, wasm_val_vec_t* results); + +using emitEvent_proto = int32_t(uint8_t const*, int32_t, uint8_t const*, int32_t); +wasm_trap_t* +emitEvent_wrap(void* env, wasm_val_vec_t const* params, wasm_val_vec_t* results); + } // namespace xrpl diff --git a/include/xrpl/tx/wasm/ParamsHelper.h b/include/xrpl/tx/wasm/ParamsHelper.h index 5490411226..ca019c2d7f 100644 --- a/include/xrpl/tx/wasm/ParamsHelper.h +++ b/include/xrpl/tx/wasm/ParamsHelper.h @@ -31,6 +31,7 @@ struct WasmResult int64_t cost; }; typedef WasmResult EscrowResult; +typedef WasmResult WasmRunResult; //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// @@ -181,6 +182,43 @@ wasmParamsHlp(std::vector& v, std::int64_t p, Types&&... args) // wasmParamsHlp(v, std::forward(args)...); // } +template +inline void +wasmParamsHlp(std::vector& v, std::uint8_t const* dt, std::int32_t sz, Types&&... args) +{ + v.push_back({.type = WT_U8V, .of = {.u8v = {.d = dt, .sz = sz}}}); + wasmParamsHlp(v, std::forward(args)...); +} + +template +inline void +wasmParamsHlp(std::vector& v, Bytes const& p, Types&&... args) +{ + wasmParamsHlp(v, p.data(), static_cast(p.size()), std::forward(args)...); +} + +template +inline void +wasmParamsHlp(std::vector& v, std::string_view const& p, Types&&... args) +{ + wasmParamsHlp( + v, + reinterpret_cast(p.data()), + static_cast(p.size()), + std::forward(args)...); +} + +template +inline void +wasmParamsHlp(std::vector& v, std::string const& p, Types&&... args) +{ + wasmParamsHlp( + v, + reinterpret_cast(p.c_str()), + static_cast(p.size()), + std::forward(args)...); +} + inline void wasmParamsHlp(std::vector& v) { diff --git a/src/libxrpl/ledger/View.cpp b/src/libxrpl/ledger/View.cpp index 1702d4243b..77fecd16d5 100644 --- a/src/libxrpl/ledger/View.cpp +++ b/src/libxrpl/ledger/View.cpp @@ -443,6 +443,271 @@ doWithdraw( return accountSend(view, sourceAcct, dstAcct, amount, j, WaiveTransferFee::Yes); } +static TER +canTransferIOU( + ReadView const& view, + AccountID const& sender, + AccountID const& receiver, + STAmount const& amount, + beast::Journal j, + SendIssuerHandling issuerHandling, + SendEscrowHandling escrowHandling, + SendAuthHandling authHandling, + SendFreezeHandling freezeHandling, + SendTransferHandling transferHandling, + SendBalanceHandling balanceHandling) +{ + AccountID issuer = amount.getIssuer(); + // If the issuer is the same as the sender + if (issuerHandling == SendIssuerHandling::ihSENDER_NOT_ALLOWED && issuer == sender) + return tecNO_PERMISSION; + + // If the issuer is the same as the receiver + if (issuerHandling == SendIssuerHandling::ihRECEIVER_NOT_ALLOWED && issuer == receiver) + return tecNO_PERMISSION; + + // If the lsfAllowTrustLineLocking is not enabled + auto const sleIssuer = view.read(keylet::account(issuer)); + if (!sleIssuer) + return tecNO_ISSUER; + + if (issuerHandling != SendIssuerHandling::ihSENDER_NOT_ALLOWED && + issuerHandling != SendIssuerHandling::ihRECEIVER_NOT_ALLOWED && + !sleIssuer->isFlag(lsfDefaultRipple)) + return terNO_RIPPLE; + + if (escrowHandling == SendEscrowHandling::ehCHECK && + !sleIssuer->isFlag(lsfAllowTrustLineLocking)) + return tecNO_PERMISSION; + + // If the sender does not have a trustline to the issuer + auto const sleRippleState = view.read(keylet::line(sender, issuer, amount.getCurrency())); + + if (!sleRippleState) + return tecNO_LINE; + + STAmount const balance = (*sleRippleState)[sfBalance]; + + // If balance is positive, issuer must have higher address than sender + if (balance > beast::zero && issuer < sender) + return tecNO_PERMISSION; // LCOV_EXCL_LINE + + // If balance is negative, issuer must have lower address than sender + if (balance < beast::zero && issuer > sender) + return tecNO_PERMISSION; // LCOV_EXCL_LINE + + // // If the account trustline has no-ripple set for the issuer + // if (auto const ter = requireNoRipple(ctx.view, amount.issue(), account); + // ter != tesSUCCESS) + // return ter; + + // // If the dest trustline has no-ripple set for the issuer + // if (auto const ter = requireNoRipple(ctx.view, amount.issue(), dest); + // ter != tesSUCCESS) + // return ter; + + // If the issuer has requireAuth set, check if the sender is authorized + if (authHandling == SendAuthHandling::ahCHECK_SENDER || + authHandling == SendAuthHandling::ahBOTH) + { + if (auto const ter = requireAuth(view, amount.issue(), sender); ter != tesSUCCESS) + return ter; + } + + // If the issuer has requireAuth set, check if the receiver is authorized + if (authHandling == SendAuthHandling::ahCHECK_RECEIVER || + authHandling == SendAuthHandling::ahBOTH) + { + if (auto const ter = requireAuth(view, amount.issue(), receiver); ter != tesSUCCESS) + return ter; + } + + // If the issuer has frozen the sender + if ((freezeHandling == SendFreezeHandling::fhCHECK_SENDER || + freezeHandling == SendFreezeHandling::fhBOTH) && + isFrozen(view, sender, amount.issue())) + return tecFROZEN; + + // If the issuer has frozen the receiver + if ((freezeHandling == SendFreezeHandling::fhCHECK_RECEIVER || + freezeHandling == SendFreezeHandling::fhBOTH) && + isFrozen(view, receiver, amount.issue())) + return tecFROZEN; + + if (balanceHandling == SendBalanceHandling::bhIGNORE) + return tesSUCCESS; + + STAmount const spendableAmount = accountHolds( + view, + sender, + amount.get(), + fhIGNORE_FREEZE, // already checked freeze above + ahIGNORE_AUTH, // already checked auth above + j); + + // If the balance is less than or equal to 0 + if (spendableAmount <= beast::zero) + return tecINSUFFICIENT_FUNDS; + + // If the spendable amount is less than the amount + if (spendableAmount < amount) + return tecINSUFFICIENT_FUNDS; + + // If the amount is not addable to the balance + if (!canAdd(spendableAmount, amount)) + return tecPRECISION_LOSS; + + return tesSUCCESS; +} + +static TER +canTransferMPT( + ReadView const& view, + AccountID const& sender, + AccountID const& receiver, + STAmount const& amount, + beast::Journal j, + SendIssuerHandling issuerHandling, + SendEscrowHandling escrowHandling, + SendAuthHandling authHandling, + SendFreezeHandling freezeHandling, + SendTransferHandling transferHandling, + SendBalanceHandling balanceHandling) +{ + AccountID issuer = amount.getIssuer(); + // If the issuer is the same as the sender + if (issuerHandling == SendIssuerHandling::ihSENDER_NOT_ALLOWED && issuer == sender) + return tecNO_PERMISSION; + + // If the issuer is the same as the receiver + if (issuerHandling == SendIssuerHandling::ihRECEIVER_NOT_ALLOWED && issuer == receiver) + return tecNO_PERMISSION; + + // If the mpt does not exist + auto const issuanceKey = keylet::mptIssuance(amount.get().getMptID()); + auto const sleIssuance = view.read(issuanceKey); + if (!sleIssuance) + return tecOBJECT_NOT_FOUND; + + // If the lsfMPTCanEscrow is not enabled + if (escrowHandling == SendEscrowHandling::ehCHECK && !sleIssuance->isFlag(lsfMPTCanEscrow)) + return tecNO_PERMISSION; + + // If the issuer is not the same as the issuer of the mpt + if (sleIssuance->getAccountID(sfIssuer) != issuer) + return tecNO_PERMISSION; // LCOV_EXCL_LINE + + // If the sender does not have the mpt + if (!view.exists(keylet::mptoken(issuanceKey.key, sender))) + return tecOBJECT_NOT_FOUND; + + auto const& mptIssue = amount.get(); + + // If the issuer has requireAuth set, check if the sender is authorized + if (authHandling == SendAuthHandling::ahCHECK_SENDER || + authHandling == SendAuthHandling::ahBOTH) + { + if (auto const ter = requireAuth(view, mptIssue, sender, AuthType::WeakAuth); + ter != tesSUCCESS) + return ter; + } + + // If the issuer has requireAuth set, check if the receiver is authorized + if (authHandling == SendAuthHandling::ahCHECK_RECEIVER || + authHandling == SendAuthHandling::ahBOTH) + { + if (auto const ter = requireAuth(view, mptIssue, receiver, AuthType::WeakAuth); + ter != tesSUCCESS) + return ter; + } + + // If the issuer has frozen the sender, return tecLOCKED + if ((freezeHandling == SendFreezeHandling::fhCHECK_SENDER || + freezeHandling == SendFreezeHandling::fhBOTH) && + isFrozen(view, sender, mptIssue)) + return tecLOCKED; + + // If the issuer has frozen the receiver, return tecLOCKED + if ((freezeHandling == SendFreezeHandling::fhCHECK_RECEIVER || + freezeHandling == SendFreezeHandling::fhBOTH) && + isFrozen(view, receiver, mptIssue)) + return tecLOCKED; + + // If the mpt cannot be transferred, return tecNO_AUTH + if (transferHandling == SendTransferHandling::thCHECK) + { + if (auto const ter = canTransfer(view, mptIssue, sender, receiver); ter != tesSUCCESS) + return ter; + } + + if (balanceHandling == SendBalanceHandling::bhIGNORE) + return tesSUCCESS; + + STAmount const spendableAmount = accountHolds( + view, + sender, + amount.get(), + fhIGNORE_FREEZE, // already checked freeze above + ahIGNORE_AUTH, // already checked auth above + j); + + // If the balance is less than or equal to 0, return tecINSUFFICIENT_FUNDS + if (spendableAmount <= beast::zero) + return tecINSUFFICIENT_FUNDS; + + // If the spendable amount is less than the amount, return + // tecINSUFFICIENT_FUNDS + if (spendableAmount < amount) + return tecINSUFFICIENT_FUNDS; + + return tesSUCCESS; +} + +TER +canTransferFT( + ReadView const& view, + AccountID const& sender, + AccountID const& receiver, + STAmount const& amount, + beast::Journal j, + SendIssuerHandling issuerHandling = SendIssuerHandling::ihIGNORE, + SendEscrowHandling escrowHandling = SendEscrowHandling::ehIGNORE, + SendAuthHandling authHandling = SendAuthHandling::ahBOTH, + SendFreezeHandling freezeHandling = SendFreezeHandling::fhBOTH, + SendTransferHandling transferHandling = SendTransferHandling::thIGNORE, + SendBalanceHandling balanceHandling = SendBalanceHandling::bhCHECK) +{ + return std::visit( + [&](TIss const& issue) -> TER { + if constexpr (std::is_same_v) + return canTransferIOU( + view, + sender, + receiver, + amount, + j, + issuerHandling, + escrowHandling, + authHandling, + freezeHandling, + transferHandling, + balanceHandling); + else + return canTransferMPT( + view, + sender, + receiver, + amount, + j, + issuerHandling, + escrowHandling, + authHandling, + freezeHandling, + transferHandling, + balanceHandling); + }, + amount.asset().value()); +} TER cleanupOnAccountDelete( ApplyView& view, diff --git a/src/libxrpl/ledger/helpers/ContractUtils.cpp b/src/libxrpl/ledger/helpers/ContractUtils.cpp new file mode 100644 index 0000000000..6a0b23a434 --- /dev/null +++ b/src/libxrpl/ledger/helpers/ContractUtils.cpp @@ -0,0 +1,643 @@ +//------------------------------------------------------------------------------ +/* + This file is part of rippled: https://github.com/ripple/rippled + Copyright (c) 2025 Ripple Labs Inc. + + 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 + +namespace xrpl { +namespace contract { + +struct BlobHash +{ + std::size_t + operator()(Blob const& b) const noexcept + { + if (b.empty()) + return 0; + return std::hash{}( + std::string_view(reinterpret_cast(b.data()), b.size())); + } +}; + +int64_t +contractCreateFee(uint64_t byteCount) +{ + constexpr uint64_t mul = static_cast(createByteMultiplier); + if (byteCount > std::numeric_limits::max() / mul) + return feeCalculationFailed; // overflow + uint64_t uf = byteCount * mul; + if (uf > static_cast(std::numeric_limits::max())) + return feeCalculationFailed; + return static_cast(uf); +} + +NotTEC +preflightFunctions(STTx const& tx, beast::Journal j) +{ + // Functions must be present if ContractCode is present. + if (!tx.isFieldPresent(sfContractCode)) + return tesSUCCESS; + + if (!tx.isFieldPresent(sfFunctions)) + { + JLOG(j.trace()) << "ContractCreate/Modify: ContractCode present but " + "Functions missing."; + return temARRAY_EMPTY; + } + + auto const& functions = tx.getFieldArray(sfFunctions); + + if (functions.empty()) + { + JLOG(j.trace()) << "ContractCreate/Modify: Functions array empty."; + return temARRAY_EMPTY; + } + + // Functions must not exceed n entries. + if (functions.size() > contract::maxContractFunctions) + { + JLOG(j.trace()) << "ContractCreate/Modify: Functions array too large."; + return temARRAY_TOO_LARGE; + } + + std::unordered_set uniqueFunctions; + uniqueFunctions.reserve(functions.size()); + for (auto const& function : functions) + { + // Functions must be unique by name. + auto const& functionName = function.getFieldVL(sfFunctionName); + if (!uniqueFunctions.insert(functionName).second) + { + JLOG(j.trace()) << "Duplicate function name: " << strHex(functionName); + return temREDUNDANT; + } + + auto const& parameters = function.getFieldArray(sfParameters); + + // Function Parameters must not exceed n entries each. + if (parameters.size() > contract::maxContractParams) + { + JLOG(j.trace()) << "ContractCreate/Modify: Function Parameters " + "array is too large."; + return temARRAY_TOO_LARGE; + } + + std::unordered_set uniqueParameters; + uniqueParameters.reserve(parameters.size()); + + for (auto const& param : parameters) + { + // Function Parameter must have a flag. + if (!param.isFieldPresent(sfParameterFlag)) + { + JLOG(j.trace()) << "ContractCreate/Modify: Function Parameter " + "is missing flag."; + return temMALFORMED; + } + + // Function Parameter must have a type. + if (!param.isFieldPresent(sfParameterType)) + { + JLOG(j.trace()) << "ContractCreate/Modify: Function Parameter " + "is missing type."; + return temMALFORMED; + } + + // Function Parameter flags must be valid. + auto const flags = param.getFieldU32(sfParameterFlag); + if (flags & tfContractParameterMask) + { + JLOG(j.trace()) << "ContractCreate/Modify: Invalid parameter " + "flag in Function."; + return temINVALID_FLAG; + } + } + } + return tesSUCCESS; +} + +NotTEC +preflightInstanceParameters(STTx const& tx, beast::Journal j) +{ + if (!tx.isFieldPresent(sfInstanceParameters)) + return tesSUCCESS; + + auto const& instanceParameters = tx.getFieldArray(sfInstanceParameters); + + // InstanceParameters must not be empty. + if (instanceParameters.empty()) + { + JLOG(j.trace()) << "ContractCreate/Modify: InstanceParameters empty array."; + return temARRAY_EMPTY; + } + + // InstanceParameters must not exceed n entries. + if (instanceParameters.size() > contract::maxContractParams) + { + JLOG(j.trace()) << "ContractCreate/Modify: InstanceParameters array " + "is too large."; + return temARRAY_TOO_LARGE; + } + + std::unordered_set uniqueParameters; + uniqueParameters.reserve(instanceParameters.size()); + for (auto const& param : instanceParameters) + { + // Instance Parameter must have a flag. + if (!param.isFieldPresent(sfParameterFlag)) + { + JLOG(j.trace()) << "ContractCreate/Modify: Instance Parameter is missing flag."; + return temMALFORMED; + } + + // Instance Parameter must have a type. + if (!param.isFieldPresent(sfParameterType)) + { + JLOG(j.trace()) << "ContractCreate/Modify: Instance Parameter is missing type."; + return temMALFORMED; + } + + // Instance Parameter flags must be valid. + auto const flags = param.getFieldU32(sfParameterFlag); + if (flags & tfContractParameterMask) + { + JLOG(j.trace()) << "ContractCreate/Modify: Invalid parameter " + "flag in Instance Parameter."; + return temINVALID_FLAG; + } + } + return tesSUCCESS; +} + +bool +validateParameterMapping(STArray const& params, STArray const& values, beast::Journal j) +{ + if (params.size() != values.size()) + { + JLOG(j.trace()) << "ContractCreate/Modify: InstanceParameterValues size " + "does not match InstanceParameters size."; + return false; + } + return true; +} + +NotTEC +preflightInstanceParameterValues(STTx const& tx, beast::Journal j) +{ + if (!tx.isFieldPresent(sfInstanceParameterValues)) + return tesSUCCESS; + + auto const& instanceParameterValues = tx.getFieldArray(sfInstanceParameterValues); + + // InstanceParameters must not be empty. + if (instanceParameterValues.empty()) + { + JLOG(j.trace()) << "ContractCreate/Modify: InstanceParameterValues is missing."; + return temARRAY_EMPTY; + } + + // InstanceParameterValues must not exceed n entries. + if (instanceParameterValues.size() > contract::maxContractParams) + { + JLOG(j.trace()) << "ContractCreate/Modify: InstanceParameterValues " + "array is too large."; + return temARRAY_TOO_LARGE; + } + + std::unordered_set uniqueParameters; + uniqueParameters.reserve(instanceParameterValues.size()); + for (auto const& param : instanceParameterValues) + { + // Instance Parameter must have a flag. + if (!param.isFieldPresent(sfParameterFlag)) + { + JLOG(j.trace()) << "ContractCreate/Modify: Instance Parameter is missing flag."; + return temMALFORMED; + } + + // Instance Parameter must have a value. + if (!param.isFieldPresent(sfParameterValue)) + { + JLOG(j.trace()) << "ContractCreate/Modify: Instance Parameter is " + "missing value."; + return temMALFORMED; + } + + // Instance Parameter flags must be valid. + auto const flags = param.getFieldU32(sfParameterFlag); + if (flags & tfContractParameterMask) + { + JLOG(j.trace()) << "ContractCreate/Modify: Invalid parameter " + "flag in Instance Parameter."; + return temINVALID_FLAG; + } + } + + // Only validate the mapping if InstanceParameters are present + bool valid = true; + if (tx.isFieldPresent(sfInstanceParameters)) + valid = validateParameterMapping( + tx.getFieldArray(sfInstanceParameters), tx.getFieldArray(sfInstanceParameterValues), j); + if (!valid) + { + JLOG(j.trace()) << "ContractCreate/Modify: InstanceParameterValues do not match " + "InstanceParameters."; + return temMALFORMED; + } + + // Validate flags in InstanceParameterValues + if (auto const res = preflightFlagParameters(instanceParameterValues, j); !isTesSuccess(res)) + { + JLOG(j.trace()) << "ContractCreate/Modify: InstanceParameterValues flag " + "validation failed: " + << transToken(res); + return res; + } + + return tesSUCCESS; +} + +bool +isValidParameterFlag(std::uint32_t flags) +{ + return (flags & tfContractParameterMask) == 0; +} + +NotTEC +preflightFlagParameters(STArray const& parameters, beast::Journal j) +{ + for (auto const& param : parameters) + { + if (!param.isFieldPresent(sfParameterFlag) || + !isValidParameterFlag(param.getFieldU32(sfParameterFlag))) + continue; // Skip invalid flags + + switch (param.getFieldU32(sfParameterFlag)) + { + case tfSendAmount: { + if (!param.isFieldPresent(sfParameterValue)) + return temMALFORMED; + auto const& value = param.getFieldData(sfParameterValue); + STAmount amount = value.getFieldAmount(); + // Preflight Transfer Amount + if (isXRP(amount)) + { + if (amount <= beast::zero) + return temBAD_AMOUNT; + } + else if (amount.holds()) + { + if (amount.native() || amount <= beast::zero) + return temBAD_AMOUNT; + + if (badCurrency() == amount.getCurrency()) + return temBAD_CURRENCY; + } + else if (amount.holds()) + { + if (amount.native() || amount.mpt() > MPTAmount{maxMPTokenAmount} || + amount <= beast::zero) + return temBAD_AMOUNT; + } + break; + } + case tfSendNFToken: { + break; + } + case tfAuthorizeToken: { + return temDISABLED; + break; + } + } + } + return tesSUCCESS; +} + +TER +preclaimFlagParameters( + ReadView const& view, + AccountID const& sourceAccount, + AccountID const& contractAccount, + STArray const& parameters, + beast::Journal j) +{ + for (auto const& param : parameters) + { + if (!param.isFieldPresent(sfParameterFlag) || + !isValidParameterFlag(param.getFieldU32(sfParameterFlag))) + continue; // Skip invalid flags + + switch (param.getFieldU32(sfParameterFlag)) + { + case tfSendAmount: { + if (!param.isFieldPresent(sfParameterValue)) + return tecINTERNAL; + + auto const& value = param.getFieldData(sfParameterValue); + STAmount amount = value.getFieldAmount(); + // Preclaim Transfer Amount + if (isXRP(amount)) + { + auto const accountSle = view.read(keylet::account(sourceAccount)); + if (!accountSle) + return tecINTERNAL; + + auto const& mSourceBalance = accountSle->getFieldAmount(sfBalance); + if (mSourceBalance < amount.xrp()) + return tecUNFUNDED; + } + else + { + if (auto ter = canTransferFT( + view, + sourceAccount, + contractAccount, + amount, + j, + SendIssuerHandling::ihIGNORE, + SendEscrowHandling::ehIGNORE, + SendAuthHandling::ahBOTH, + SendFreezeHandling::fhBOTH, + SendTransferHandling::thIGNORE, + SendBalanceHandling::bhCHECK)) + { + JLOG(j.trace()) << "preclaimFlagParameters: Cannot " + "transfer amount: " + << amount; + return ter; + } + } + break; + } + case tfSendNFToken: { + if (!param.isFieldPresent(sfParameterValue)) + return tecINTERNAL; + auto const& value = param.getFieldData(sfParameterValue); + auto const& nftokenID = value.getFieldH256(); + // Preclaim Transfer NFT Token + if (!nft::findToken(view, sourceAccount, nftokenID)) + { + JLOG(j.trace()) + << "preclaimFlagParameters: Cannot transfer NFT token: " << nftokenID; + return tecNO_ENTRY; + } + break; + } + case tfAuthorizeToken: { + break; + } + } + } + return tesSUCCESS; +} + +TER +doApplyFlagParameters( + ApplyView& view, + STTx const& tx, + AccountID const& sourceAccount, + AccountID const& contractAccount, + STArray const& parameters, + XRPAmount const& priorBalance, + beast::Journal j) +{ + for (auto const& param : parameters) + { + if (!param.isFieldPresent(sfParameterFlag) || + !isValidParameterFlag(param.getFieldU32(sfParameterFlag))) + continue; // Skip invalid flags + + switch (param.getFieldU32(sfParameterFlag)) + { + case tfSendAmount: { + if (!param.isFieldPresent(sfParameterValue)) + return tecINTERNAL; + + auto const& value = param.getFieldData(sfParameterValue); + STAmount amount = value.getFieldAmount(); + if (auto ter = accountSend( + view, sourceAccount, contractAccount, amount, j, WaiveTransferFee::No); + !isTesSuccess(ter)) + { + JLOG(j.trace()) << "doApplyFlagParameters: Failed to send amount: " << amount; + return ter; + } + break; + } + case tfSendNFToken: { + if (!param.isFieldPresent(sfParameterValue)) + return tecINTERNAL; + auto const& value = param.getFieldData(sfParameterValue); + auto const& nftokenID = value.getFieldH256(); + if (auto ter = + nft::transferNFToken(view, sourceAccount, contractAccount, nftokenID); + !isTesSuccess(ter)) + { + JLOG(j.trace()) + << "doApplyFlagParameters: Failed to send NFT token: " << nftokenID; + return ter; + } + break; + } + case tfAuthorizeToken: { + return tecINTERNAL; + // if (!param.isFieldPresent(sfParameterValue)) + // return tecINTERNAL; + // // Handle tfAuthorizeToken if needed + // auto const& value = param.getFieldData(sfParameterValue); + // STAmount limit = value.getFieldAmount(); + // Asset asset = Asset{limit.issue()}; + // if (auto ter = canAddHolding(view, asset); + // !isTesSuccess(ter)) + // { + // JLOG(j.trace()) << "doApplyFlagParameters: Cannot add " + // "holding for asset: " + // << to_string(asset); + // return ter; + // } + // // Set the issuer to the contract account for the holding + // limit.setIssuer(contractAccount); + // if (auto ter = addEmptyHolding( + // view, contractAccount, priorBalance, asset, limit, + // j); + // !isTesSuccess(ter)) + // { + // JLOG(j.trace()) << "doApplyFlagParameters: Failed to add + // " + // "holding for asset: " + // << to_string(asset); + // return ter; + // } + // break; + } + } + } + return tesSUCCESS; +} + +uint32_t +contractDataReserve(uint32_t size) +{ + // Divide by dataByteMultiplier and round up to the nearest whole number + return (size + dataByteMultiplier - 1U) / dataByteMultiplier; +} + +TER +setContractData( + ServiceRegistry& registry, + ApplyView& view, + AccountID const& account, + AccountID const& contractAccount, + STJson const& data) +{ + auto const j = registry.getJournal("View"); + auto const sleAccount = view.peek(keylet::account(account)); + if (!sleAccount) + return tefINTERNAL; + + // if the blob is too large don't set it + if (data.size() > maxContractDataSize) + return temARRAY_TOO_LARGE; + + auto dataKeylet = keylet::contractData(account, contractAccount); + auto dataSle = view.peek(dataKeylet); + + // DELETE + if (data.size() == 0) + { + if (!dataSle) + return tesSUCCESS; + + uint32_t oldDataReserve = contractDataReserve(dataSle->getFieldJson(sfContractJson).size()); + + std::uint64_t const page = (*dataSle)[sfOwnerNode]; + // Remove the page from the account directory + if (!view.dirRemove(keylet::ownerDir(account), page, dataKeylet.key, false)) + return tefBAD_LEDGER; + + // remove the actual contract data sle + view.erase(dataSle); + + // reduce the owner count + adjustOwnerCount(view, sleAccount, -oldDataReserve, j); + return tesSUCCESS; + } + + std::uint32_t ownerCount{(*sleAccount)[sfOwnerCount]}; + bool createNew = !dataSle; + if (createNew) + { + // CREATE + uint32_t dataReserve = contractDataReserve(data.size()); + uint32_t newReserve = ownerCount + dataReserve; + XRPAmount const newReserveAmount{view.fees().accountReserve(newReserve)}; + if (STAmount((*sleAccount)[sfBalance]).xrp() < newReserveAmount) + return tecINSUFFICIENT_RESERVE; + + adjustOwnerCount(view, sleAccount, dataReserve, j); + // create an entry + dataSle = std::make_shared(dataKeylet); + dataSle->setFieldJson(sfContractJson, data); + dataSle->setAccountID(sfOwner, account); + dataSle->setAccountID(sfContractAccount, contractAccount); + + auto const page = + view.dirInsert(keylet::ownerDir(account), dataKeylet.key, describeOwnerDir(account)); + if (!page) + return tecDIR_FULL; + + dataSle->setFieldU64(sfOwnerNode, *page); + + // add new data to ledger + view.insert(dataSle); + } + else + { + // UPDATE + uint32_t oldDataReserve = contractDataReserve(dataSle->getFieldJson(sfContractJson).size()); + uint32_t newDataReserve = contractDataReserve(data.size()); + if (newDataReserve != oldDataReserve) + { + // if the reserve changes, we need to adjust the owner count + uint32_t newReserve = ownerCount - oldDataReserve + newDataReserve; + XRPAmount const newReserveAmount{view.fees().accountReserve(newReserve)}; + if (STAmount((*sleAccount)[sfBalance]).xrp() < newReserveAmount) + return tecINSUFFICIENT_RESERVE; + + adjustOwnerCount(view, sleAccount, newReserve, j); + } + + // update the data + dataSle->setFieldJson(sfContractJson, data); + view.update(dataSle); + } + return tesSUCCESS; +} + +TER +finalizeContractData( + ServiceRegistry& registry, + ApplyView& view, + AccountID const& contractAccount, + ContractDataMap const& dataMap, + ContractEventMap const& eventMap, + uint256 const& txnID) +{ + auto const& j = registry.getJournal("View"); + uint16_t changeCount = 0; + + for (auto const& [name, data] : eventMap) + registry.getOPs().pubContractEvent(name, data); + + for (auto const& accEntry : dataMap) + { + auto const& acc = accEntry.first; + auto const& cacheEntry = accEntry.second; + bool is_modified = cacheEntry.first; + auto const& jsonData = cacheEntry.second; + if (is_modified) + { + changeCount++; + if (changeCount > maxDataModifications) + { + // overflow + JLOG(j.trace()) << "ContractError[TX:" << txnID + << "]: SetContractData failed: Too many data changes"; + return tecWASM_REJECTED; + } + + TER result = setContractData(registry, view, acc, contractAccount, jsonData); + if (!isTesSuccess(result)) + { + JLOG(j.warn()) << "ContractError[TX:" << txnID + << "]: SetContractData failed: " << result << " Account: " << acc; + return result; + } + } + } + return tesSUCCESS; +} + +} // namespace contract +} // namespace xrpl diff --git a/src/libxrpl/tx/transactors/nft/NFTokenUtils.cpp b/src/libxrpl/ledger/helpers/NFTokenUtils.cpp similarity index 94% rename from src/libxrpl/tx/transactors/nft/NFTokenUtils.cpp rename to src/libxrpl/ledger/helpers/NFTokenUtils.cpp index 3368887d94..874c483c3d 100644 --- a/src/libxrpl/tx/transactors/nft/NFTokenUtils.cpp +++ b/src/libxrpl/ledger/helpers/NFTokenUtils.cpp @@ -1,15 +1,16 @@ +#include #include #include #include #include #include +#include #include #include #include #include #include #include -#include #include #include @@ -1106,5 +1107,56 @@ checkTrustlineDeepFrozen( return tesSUCCESS; } +TER +transferNFToken( + ApplyView& view, + AccountID const& buyer, + AccountID const& seller, + uint256 const& nftokenID) +{ + auto tokenAndPage = nft::findTokenAndPage(view, seller, nftokenID); + + if (!tokenAndPage) + return tecINTERNAL; // LCOV_EXCL_LINE + + if (auto const ret = nft::removeToken(view, seller, nftokenID, std::move(tokenAndPage->page)); + !isTesSuccess(ret)) + return ret; + + auto const sleBuyer = view.read(keylet::account(buyer)); + if (!sleBuyer) + return tecINTERNAL; // LCOV_EXCL_LINE + + std::uint32_t const buyerOwnerCountBefore = sleBuyer->getFieldU32(sfOwnerCount); + + auto const insertRet = nft::insertToken(view, buyer, std::move(tokenAndPage->token)); + + // if fixNFTokenReserve is enabled, check if the buyer has sufficient + // reserve to own a new object, if their OwnerCount changed. + // + // There was an issue where the buyer accepts a sell offer, the ledger + // didn't check if the buyer has enough reserve, meaning that buyer can get + // NFTs free of reserve. + if (view.rules().enabled(fixNFTokenReserve)) + { + // To check if there is sufficient reserve, we cannot use mPriorBalance + // because NFT is sold for a price. So we must use the balance after + // the deduction of the potential offer price. A small caveat here is + // that the balance has already deducted the transaction fee, meaning + // that the reserve requirement is a few drops higher. + auto const buyerBalance = sleBuyer->getFieldAmount(sfBalance); + + auto const buyerOwnerCountAfter = sleBuyer->getFieldU32(sfOwnerCount); + if (buyerOwnerCountAfter > buyerOwnerCountBefore) + { + if (auto const reserve = view.fees().accountReserve(buyerOwnerCountAfter); + buyerBalance < reserve) + return tecINSUFFICIENT_RESERVE; + } + } + + return insertRet; +} + } // namespace nft } // namespace xrpl diff --git a/src/libxrpl/protocol/Emitable.cpp b/src/libxrpl/protocol/Emitable.cpp new file mode 100644 index 0000000000..96f00cd47b --- /dev/null +++ b/src/libxrpl/protocol/Emitable.cpp @@ -0,0 +1,167 @@ +#include +#include +#include +#include +#include + +namespace xrpl { + +Emitable::Emitable() +{ + emitableTx_ = { +#pragma push_macro("TRANSACTION") +#undef TRANSACTION + +#define TRANSACTION(tag, value, name, delegatable, amendment, permissions, emitable, fields) \ + {value, emitable}, +#include + +#undef TRANSACTION +#pragma pop_macro("TRANSACTION") + }; + + granularEmitableMap_ = { +#pragma push_macro("EMITABLE") +#undef EMITABLE + +#define EMITABLE(type, txType, value) {#type, type}, + +#include + +#undef EMITABLE +#pragma pop_macro("EMITABLE") + }; + + granularNameMap_ = { +#pragma push_macro("EMITABLE") +#undef EMITABLE + +#define EMITABLE(type, txType, value) {type, #type}, + +#include + +#undef EMITABLE +#pragma pop_macro("EMITABLE") + }; + + granularTxTypeMap_ = { +#pragma push_macro("EMITABLE") +#undef EMITABLE + +#define EMITABLE(type, txType, value) {type, txType}, + +#include + +#undef EMITABLE +#pragma pop_macro("EMITABLE") + }; + + for ([[maybe_unused]] auto const& emitable : granularEmitableMap_) + XRPL_ASSERT( + emitable.second > UINT16_MAX, + "xrpl::Emitable::granularEmitableMap_ : granular emitable " + "value must not exceed the maximum uint16_t value."); +} + +Emitable const& +Emitable::getInstance() +{ + static Emitable const instance; + return instance; +} + +std::optional +Emitable::getEmitableName(std::uint32_t const value) const +{ + auto const emitableValue = static_cast(value); + if (auto const granular = getGranularName(emitableValue)) + return *granular; + + // not a granular emitable, check if it maps to a transaction type + auto const txType = emitableToTxType(value); + if (auto const* item = TxFormats::getInstance().findByType(txType); item != nullptr) + return item->getName(); + + return std::nullopt; +} + +std::optional +Emitable::getGranularValue(std::string const& name) const +{ + auto const it = granularEmitableMap_.find(name); + if (it != granularEmitableMap_.end()) + return static_cast(it->second); + + return std::nullopt; +} + +std::optional +Emitable::getGranularName(GranularEmitableType const& value) const +{ + auto const it = granularNameMap_.find(value); + if (it != granularNameMap_.end()) + return it->second; + + return std::nullopt; +} + +std::optional +Emitable::getGranularTxType(GranularEmitableType const& gpType) const +{ + auto const it = granularTxTypeMap_.find(gpType); + if (it != granularTxTypeMap_.end()) + return it->second; + + return std::nullopt; +} + +bool +Emitable::isEmitable(std::uint32_t const& emitableValue) const +{ + auto const granularEmitable = getGranularName(static_cast(emitableValue)); + if (granularEmitable) + // granular emitables are always allowed to be delegated + return true; + + auto const txType = emitableToTxType(emitableValue); + auto const it = emitableTx_.find(txType); + + // if (rules.enabled(fixDelegateV1_1)) + // { + // if (it == delegatableTx_.end()) + // return false; + + // auto const txFeaturesIt = txFeatureMap_.find(txType); + // XRPL_ASSERT( + // txFeaturesIt != txFeatureMap_.end(), + // "xrpl::Emitables::isDelegatable : tx exists in txFeatureMap_"); + + // // fixDelegateV1_1: Delegation is only allowed if the required + // amendment + // // for the transaction is enabled. For transactions that do not + // require + // // an amendment, delegation is always allowed. + // if (txFeaturesIt->second != uint256{} && + // !rules.enabled(txFeaturesIt->second)) + // return false; + // } + + if (it != emitableTx_.end() && it->second == Emittance::notEmitable) + return false; + + return true; +} + +uint32_t +Emitable::txToEmitableType(TxType const& type) const +{ + return static_cast(type) + 1; +} + +TxType +Emitable::emitableToTxType(uint32_t const& value) const +{ + return static_cast(value - 1); +} + +} // namespace xrpl diff --git a/src/libxrpl/protocol/Indexes.cpp b/src/libxrpl/protocol/Indexes.cpp index 61a64b2a54..620d351482 100644 --- a/src/libxrpl/protocol/Indexes.cpp +++ b/src/libxrpl/protocol/Indexes.cpp @@ -79,10 +79,12 @@ enum class LedgerNameSpace : std::uint16_t { VAULT = 'V', LOAN_BROKER = 'l', // lower-case L LOAN = 'L', + CONTRACT_SOURCE = 'Z', + CONTRACT = 'z', + CONTRACT_DATA = 'b', // No longer used or supported. Left here to reserve the space // to avoid accidental reuse. - CONTRACT [[deprecated]] = 'c', GENERATOR [[deprecated]] = 'g', NICKNAME [[deprecated]] = 'n', }; @@ -530,6 +532,24 @@ permissionedDomain(uint256 const& domainID) noexcept return {ltPERMISSIONED_DOMAIN, domainID}; } +Keylet +contractSource(uint256 const& contractHash) noexcept +{ + return {ltCONTRACT_SOURCE, indexHash(LedgerNameSpace::CONTRACT_SOURCE, contractHash)}; +} + +Keylet +contract(uint256 const& contractHash, AccountID const& owner, std::uint32_t seq) noexcept +{ + return {ltCONTRACT, indexHash(LedgerNameSpace::CONTRACT, contractHash, owner, seq)}; +} + +Keylet +contractData(AccountID const& owner, AccountID const& contractAccount) noexcept +{ + return {ltCONTRACT_DATA, indexHash(LedgerNameSpace::CONTRACT_DATA, owner, contractAccount)}; +} + } // namespace keylet } // namespace xrpl diff --git a/src/libxrpl/protocol/InnerObjectFormats.cpp b/src/libxrpl/protocol/InnerObjectFormats.cpp index 8429c51aea..c7c658d247 100644 --- a/src/libxrpl/protocol/InnerObjectFormats.cpp +++ b/src/libxrpl/protocol/InnerObjectFormats.cpp @@ -159,6 +159,35 @@ InnerObjectFormats::InnerObjectFormats() {sfTxnSignature, soeOPTIONAL}, {sfSigners, soeOPTIONAL}, }); + + add(sfFunction.jsonName.c_str(), + sfFunction.getCode(), + { + {sfFunctionName, soeREQUIRED}, + {sfParameters, soeOPTIONAL}, + }); + + add(sfInstanceParameter.jsonName, + sfInstanceParameter.getCode(), + { + {sfParameterFlag, soeREQUIRED}, + {sfParameterType, soeREQUIRED}, + }); + + add(sfInstanceParameterValue.jsonName, + sfInstanceParameterValue.getCode(), + { + {sfParameterFlag, soeREQUIRED}, + {sfParameterValue, soeREQUIRED}, + }); + + add(sfParameter.jsonName, + sfParameter.getCode(), + { + {sfParameterFlag, soeOPTIONAL}, + {sfParameterType, soeOPTIONAL}, + {sfParameterValue, soeOPTIONAL}, + }); } InnerObjectFormats const& diff --git a/src/libxrpl/protocol/STData.cpp b/src/libxrpl/protocol/STData.cpp new file mode 100644 index 0000000000..24e165e3ca --- /dev/null +++ b/src/libxrpl/protocol/STData.cpp @@ -0,0 +1,968 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +namespace xrpl { + +template +constexpr std::enable_if_t::value && std::is_signed::value, U> +to_unsigned(S value) +{ + if (value < 0 || std::numeric_limits::max() < value) + Throw("Value out of range"); + return static_cast(value); +} + +template +constexpr std::enable_if_t::value && std::is_unsigned::value, U1> +to_unsigned(U2 value) +{ + if (std::numeric_limits::max() < value) + Throw("Value out of range"); + return static_cast(value); +} + +// TODO +STData::STData(SField const& n) : STBase(n), inner_type_(STI_NOTPRESENT), data_(STBase{}) +{ +} + +STData::STData(SField const& n, unsigned char v) + : STBase(n) + , inner_type_(STI_UINT8) + , data_(detail::STVar(detail::defaultObject, sfCloseResolution)) +{ + setFieldUsingSetValue(v); +} + +STData::STData(SField const& n, std::uint16_t v) + : STBase(n) + , inner_type_(STI_UINT16) + , data_(detail::STVar(detail::defaultObject, sfSignerWeight)) +{ + setFieldUsingSetValue(v); +} + +STData::STData(SField const& n, std::uint32_t v) + : STBase(n), inner_type_(STI_UINT32), data_(detail::STVar(detail::defaultObject, sfNetworkID)) +{ + setFieldUsingSetValue(v); +} + +STData::STData(SField const& n, std::uint64_t v) + : STBase(n), inner_type_(STI_UINT64), data_(detail::STVar(detail::defaultObject, sfIndexNext)) +{ + setFieldUsingSetValue(v); +} + +STData::STData(SField const& n, uint128 const& v) + : STBase(n), inner_type_(STI_UINT128), data_(detail::STVar(detail::defaultObject, sfEmailHash)) +{ + setFieldUsingSetValue(v); +} + +STData::STData(SField const& n, uint160 const& v) + : STBase(n) + , inner_type_(STI_UINT160) + , data_(detail::STVar(detail::defaultObject, sfTakerPaysCurrency)) +{ + setFieldUsingSetValue(v); +} + +STData::STData(SField const& n, uint192 const& v) + : STBase(n) + , inner_type_(STI_UINT192) + , data_(detail::STVar(detail::defaultObject, sfMPTokenIssuanceID)) +{ + setFieldUsingSetValue(v); +} + +STData::STData(SField const& n, uint256 const& v) + : STBase(n), inner_type_(STI_UINT256), data_(detail::STVar(detail::defaultObject, sfLedgerHash)) +{ + setFieldUsingSetValue(v); +} + +STData::STData(SField const& n, Blob const& v) + : STBase(n), inner_type_(STI_VL), data_(detail::STVar(detail::defaultObject, sfURI)) +{ + setFieldUsingSetValue(Buffer(v.data(), v.size())); +} + +STData::STData(SField const& n, Slice const& v) + : STBase(n), inner_type_(STI_VL), data_(detail::STVar(detail::defaultObject, sfURI)) +{ + setFieldUsingSetValue(Buffer(v.data(), v.size())); +} + +STData::STData(SField const& n, STAmount const& v) + : STBase(n), inner_type_(STI_AMOUNT), data_(detail::STVar(detail::defaultObject, sfAmount)) +{ + setFieldUsingAssignment(v); +} + +STData::STData(SField const& n, AccountID const& v) + : STBase(n), inner_type_(STI_ACCOUNT), data_(detail::STVar(detail::defaultObject, sfAccount)) +{ + setFieldUsingSetValue(v); +} + +STData::STData(SField const& n, STIssue const& v) + : STBase(n), inner_type_(STI_ISSUE), data_(detail::STVar(detail::defaultObject, sfAsset)) +{ + setFieldUsingAssignment(v); +} + +STData::STData(SField const& n, STCurrency const& v) + : STBase(n), inner_type_(STI_CURRENCY), data_(detail::STVar(detail::defaultObject, sfBaseAsset)) +{ + setFieldUsingAssignment(v); +} + +STData::STData(SField const& n, STNumber const& v) + : STBase(n), inner_type_(STI_NUMBER), data_(detail::STVar(detail::defaultObject, sfNumber)) +{ + setFieldUsingAssignment(v); +} + +STData::STData(SerialIter& sit, SField const& name) : STBase(name), data_(STBase{}) +{ + std::uint16_t stype = SerializedTypeID(sit.get16()); + inner_type_ = stype; + SerializedTypeID s = static_cast(stype); + switch (s) + { + case STI_UINT8: { + data_ = detail::STVar(sit, sfCloseResolution); + break; + } + case STI_UINT16: { + data_ = detail::STVar(sit, sfSignerWeight); + break; + } + case STI_UINT32: { + data_ = detail::STVar(sit, sfNetworkID); + break; + } + case STI_UINT64: { + data_ = detail::STVar(sit, sfIndexNext); + break; + } + case STI_UINT128: { + data_ = detail::STVar(sit, sfEmailHash); + break; + } + case STI_UINT160: { + data_ = detail::STVar(sit, sfTakerPaysCurrency); + break; + } + case STI_UINT192: { + data_ = detail::STVar(sit, sfMPTokenIssuanceID); + break; + } + case STI_UINT256: { + data_ = detail::STVar(sit, sfLedgerHash); + break; + } + case STI_VL: { + data_ = detail::STVar(sit, sfURI); + break; + } + case STI_AMOUNT: { + data_ = detail::STVar(sit, sfAmount); + break; + } + case STI_ACCOUNT: { + data_ = detail::STVar(sit, sfAccount); + break; + } + case STI_ISSUE: { + data_ = detail::STVar(sit, sfAsset); + break; + } + case STI_CURRENCY: { + data_ = detail::STVar(sit, sfBaseAsset); + break; + } + case STI_NUMBER: { + data_ = detail::STVar(sit, sfNumber); + break; + } + default: + Throw("STData: unknown type"); + } +} + +STBase* +STData::copy(std::size_t n, void* buf) const +{ + return emplace(n, buf, *this); +} + +STBase* +STData::move(std::size_t n, void* buf) +{ + return emplace(n, buf, std::move(*this)); +} + +std::size_t +STData::size() const +{ + switch (static_cast(inner_type_)) + { + case STI_UINT8: { + return sizeof(uint8_t); + } + case STI_UINT16: { + return sizeof(uint16_t); + } + case STI_UINT32: { + return sizeof(uint32_t); + } + case STI_UINT64: { + return sizeof(uint64_t); + } + case STI_UINT128: { + return uint128::size(); + } + case STI_UINT160: { + return uint160::size(); + } + case STI_UINT192: { + return uint192::size(); + } + case STI_UINT256: { + return uint256::size(); + } + case STI_VL: { + STBlob const& st_blob = data_.get().downcast(); + return st_blob.size(); + } + case STI_AMOUNT: { + // TODO: STAmount::size() + STAmount const& st_amt = data_.get().downcast(); + return st_amt.native() ? 8 : 48; + } + case STI_ACCOUNT: { + return uint160::size(); + } + case STI_ISSUE: { + // const STIssue& st_issue = data_.get().downcast(); + return 40; // 20 bytes for currency + 20 bytes for account + } + case STI_CURRENCY: { + // const STCurrency& st_currency = + // data_.get().downcast(); + return 20; // 20 bytes for currency + } + case STI_NUMBER: { + return sizeof(double); + } + default: + Throw("STData: unknown type"); + } +} + +SerializedTypeID +STData::getSType() const +{ + return STI_DATA; +} + +void +STData::add(Serializer& s) const +{ + s.add16(inner_type_); + + switch (static_cast(inner_type_)) + { + case STI_UINT8: { + STUInt8 const& st_uint8 = data_.get().downcast(); + st_uint8.add(s); + break; + } + case STI_UINT16: { + STUInt16 const& st_uint16 = data_.get().downcast(); + st_uint16.add(s); + break; + } + case STI_UINT32: { + STUInt32 const& st_uint32 = data_.get().downcast(); + st_uint32.add(s); + break; + } + case STI_UINT64: { + STUInt64 const& st_uint64 = data_.get().downcast(); + st_uint64.add(s); + break; + } + case STI_UINT128: { + STUInt128 const& st_uint128 = data_.get().downcast(); + st_uint128.add(s); + break; + } + case STI_UINT160: { + STUInt160 const& st_uint160 = data_.get().downcast(); + st_uint160.add(s); + break; + } + case STI_UINT192: { + STUInt192 const& st_uint192 = data_.get().downcast(); + st_uint192.add(s); + break; + } + case STI_UINT256: { + STUInt256 const& st_uint256 = data_.get().downcast(); + st_uint256.add(s); + break; + } + case STI_VL: { + STBlob const& st_blob = data_.get().downcast(); + st_blob.add(s); + break; + } + case STI_AMOUNT: { + STAmount const& st_amt = data_.get().downcast(); + st_amt.add(s); + break; + } + case STI_ACCOUNT: { + STAccount const& st_acc = data_.get().downcast(); + st_acc.add(s); + break; + } + case STI_ISSUE: { + STIssue const& st_issue = data_.get().downcast(); + st_issue.add(s); + break; + } + case STI_CURRENCY: { + STCurrency const& st_currency = data_.get().downcast(); + st_currency.add(s); + break; + } + case STI_NUMBER: { + STNumber const& st_number = data_.get().downcast(); + st_number.add(s); + break; + } + default: + Throw("STData: unknown type"); + } +} + +bool +STData::isEquivalent(STBase const& t) const +{ + auto const* const tPtr = dynamic_cast(&t); + return tPtr && (default_ == tPtr->default_) && (inner_type_ == tPtr->inner_type_) && + (data_ == tPtr->data_); +} + +bool +STData::isDefault() const +{ + return default_; +} + +std::string +STData::getInnerTypeString() const +{ + std::string inner_type_str = "Unknown"; + switch (static_cast(inner_type_)) + { + case STI_UINT8: + inner_type_str = "UINT8"; + break; + case STI_UINT16: + inner_type_str = "UINT16"; + break; + case STI_UINT32: + inner_type_str = "UINT32"; + break; + case STI_UINT64: + inner_type_str = "UINT64"; + break; + case STI_UINT128: + inner_type_str = "UINT128"; + break; + case STI_UINT160: + inner_type_str = "UINT160"; + break; + case STI_UINT192: + inner_type_str = "UINT192"; + break; + case STI_UINT256: + inner_type_str = "UINT256"; + break; + case STI_VL: + inner_type_str = "VL"; + break; + case STI_AMOUNT: + inner_type_str = "AMOUNT"; + break; + case STI_ACCOUNT: + inner_type_str = "ACCOUNT"; + break; + case STI_ISSUE: + inner_type_str = "ISSUE"; + break; + case STI_CURRENCY: + inner_type_str = "CURRENCY"; + break; + case STI_NUMBER: + inner_type_str = "NUMBER"; + break; + // Add other known types as needed + default: + inner_type_str = std::to_string(inner_type_); + } + + return inner_type_str; +} + +std::string +STData::getText() const +{ + std::string inner_type_str = getInnerTypeString(); + return "STData{InnerType: " + inner_type_str + ", Data: " + data_.get().getText() + "}"; +} + +Json::Value +STData::getJson(JsonOptions options) const +{ + Json::Value ret(Json::objectValue); + ret[jss::type] = getInnerTypeString(); + ret[jss::value] = data_.get().getJson(options); + return ret; +} + +STBase* +STData::makeFieldPresent() +{ + STBase* f = &data_.get(); // getPIndex(index); + + if (f->getSType() != STI_NOTPRESENT) + return f; + + data_ = detail::STVar(detail::nonPresentObject, f->getFName()); + return &data_.get(); +} + +void +STData::setFieldU8(unsigned char v) +{ + inner_type_ = STI_UINT8; + data_ = detail::STVar(detail::defaultObject, sfCloseResolution); + setFieldUsingSetValue(v); +} + +void +STData::setFieldU16(std::uint16_t v) +{ + inner_type_ = STI_UINT16; + data_ = detail::STVar(detail::defaultObject, sfSignerWeight); + setFieldUsingSetValue(v); +} + +void +STData::setFieldU32(std::uint32_t v) +{ + inner_type_ = STI_UINT32; + data_ = detail::STVar(detail::defaultObject, sfNetworkID); + setFieldUsingSetValue(v); +} + +void +STData::setFieldU64(std::uint64_t v) +{ + inner_type_ = STI_UINT64; + data_ = detail::STVar(detail::defaultObject, sfIndexNext); + setFieldUsingSetValue(v); +} + +void +STData::setFieldH128(uint128 const& v) +{ + inner_type_ = STI_UINT128; + data_ = detail::STVar(detail::defaultObject, sfEmailHash); + setFieldUsingSetValue(v); +} + +void +STData::setFieldH160(uint160 const& v) +{ + inner_type_ = STI_UINT160; + data_ = detail::STVar(detail::defaultObject, sfTakerPaysCurrency); + setFieldUsingSetValue(v); +} + +void +STData::setFieldH192(uint192 const& v) +{ + inner_type_ = STI_UINT192; + data_ = detail::STVar(detail::defaultObject, sfMPTokenIssuanceID); + setFieldUsingSetValue(v); +} + +void +STData::setFieldH256(uint256 const& v) +{ + inner_type_ = STI_UINT256; + data_ = detail::STVar(detail::defaultObject, sfLedgerHash); + setFieldUsingSetValue(v); +} + +void +STData::setFieldVL(Blob const& v) +{ + inner_type_ = STI_VL; + data_ = detail::STVar(detail::defaultObject, sfData); + setFieldUsingSetValue(Buffer(v.data(), v.size())); +} + +void +STData::setFieldVL(Slice const& s) +{ + inner_type_ = STI_VL; + data_ = detail::STVar(detail::defaultObject, sfData); + setFieldUsingSetValue(Buffer(s.data(), s.size())); +} + +void +STData::setAccountID(AccountID const& v) +{ + inner_type_ = STI_ACCOUNT; + data_ = detail::STVar(detail::defaultObject, sfAccount); + setFieldUsingSetValue(v); +} + +void +STData::setFieldAmount(STAmount const& v) +{ + inner_type_ = STI_AMOUNT; + data_ = detail::STVar(detail::defaultObject, sfAmount); + setFieldUsingAssignment(v); +} + +void +STData::setIssue(STIssue const& v) +{ + inner_type_ = STI_ISSUE; + data_ = detail::STVar(detail::defaultObject, sfAsset); + setFieldUsingAssignment(v); +} + +void +STData::setCurrency(STCurrency const& v) +{ + inner_type_ = STI_CURRENCY; + data_ = detail::STVar(detail::defaultObject, sfBaseAsset); + setFieldUsingAssignment(v); +} + +void +STData::setFieldNumber(STNumber const& v) +{ + inner_type_ = STI_NUMBER; + data_ = detail::STVar(detail::defaultObject, sfNumber); + setFieldUsingAssignment(v); +} + +unsigned char +STData::getFieldU8() const +{ + return getFieldByValue(); +} + +std::uint16_t +STData::getFieldU16() const +{ + return getFieldByValue(); +} + +std::uint32_t +STData::getFieldU32() const +{ + return getFieldByValue(); +} + +std::uint64_t +STData::getFieldU64() const +{ + return getFieldByValue(); +} + +uint128 +STData::getFieldH128() const +{ + return getFieldByValue(); +} + +uint160 +STData::getFieldH160() const +{ + return getFieldByValue(); +} + +uint192 +STData::getFieldH192() const +{ + return getFieldByValue(); +} + +uint256 +STData::getFieldH256() const +{ + return getFieldByValue(); +} + +Blob +STData::getFieldVL() const +{ + STBlob empty; + STBlob const& b = getFieldByConstRef(empty); + return Blob(b.data(), b.data() + b.size()); +} + +AccountID +STData::getAccountID() const +{ + return getFieldByValue(); +} + +STAmount const& +STData::getFieldAmount() const +{ + static STAmount const empty{}; + return getFieldByConstRef(empty); +} + +STIssue +STData::getFieldIssue() const +{ + static STIssue const empty{}; + return getFieldByConstRef(empty); +} + +STCurrency +STData::getFieldCurrency() const +{ + static STCurrency const empty{}; + return getFieldByConstRef(empty); +} + +STNumber +STData::getFieldNumber() const +{ + static STNumber const empty{}; + return getFieldByConstRef(empty); +} + +STData +dataFromJson(SField const& field, Json::Value const& v) +{ + Json::Value type; + Json::Value value; + + if (!v.isObject()) + Throw("STData: expected object"); + + type = v[jss::type]; + value = v[jss::value]; + + if (type.isNull()) + Throw("STData: type is null"); + if (value.isNull()) + Throw("STData: value is null"); + + auto typeStr = type.asString(); + + if (typeStr == "UINT8") + { + STData data(field, static_cast(value.asUInt())); + return data; + } + else if (typeStr == "UINT16") + { + STData data(field, static_cast(value.asUInt())); + return data; + } + else if (typeStr == "UINT32") + { + try + { + if (value.isString()) + { + STData data(field, beast::lexicalCastThrow(value.asString())); + return data; + } + else if (value.isInt()) + { + STData data(field, to_unsigned(value.asInt())); + return data; + } + else if (value.isUInt()) + { + STData data(field, safe_cast(value.asUInt())); + return data; + } + else + { + Throw("bad type for UINT32"); + } + } + catch (std::exception const&) + { + Throw("invalid data for UINT32"); + } + } + else if (typeStr == "UINT64") + { + try + { + if (value.isString()) + { + auto const str = value.asString(); + + std::uint64_t val; + + bool const useBase10 = field.shouldMeta(SField::sMD_BaseTen); + + // if the field is amount, serialize as base 10 + auto [p, ec] = + std::from_chars(str.data(), str.data() + str.size(), val, useBase10 ? 10 : 16); + + if (ec != std::errc() || (p != str.data() + str.size())) + Throw("STData: invalid UINT64 data"); + + STData data(field, val); + return data; + } + else if (value.isInt()) + { + STData data(field, to_unsigned(value.asInt())); + return data; + } + else if (value.isUInt()) + { + STData data(field, safe_cast(value.asUInt())); + return data; + } + else + { + Throw("STData: bad type for UINT64"); + } + } + catch (std::exception const&) + { + Throw("STData: invalid data for UINT64"); + } + } + else if (typeStr == "UINT128") + { + if (!value.isString()) + { + Throw("STData: expected string for UINT128"); + } + + uint128 num; + + if (auto const s = value.asString(); !num.parseHex(s)) + { + if (!s.empty()) + { + Throw("STData: invalid UINT128 data"); + } + + num.zero(); + } + + STData data(field, num); + return data; + } + else if (typeStr == "UINT192") + { + if (!value.isString()) + { + Throw("STData: expected string for UINT192"); + } + + uint192 num; + + if (auto const s = value.asString(); !num.parseHex(s)) + { + if (!s.empty()) + { + Throw("STData: invalid UINT192 data"); + } + + num.zero(); + } + + STData data(field, num); + return data; + } + else if (typeStr == "UINT160") + { + if (!value.isString()) + { + Throw("STData: expected string for UINT160"); + } + + uint160 num; + + if (auto const s = value.asString(); !num.parseHex(s)) + { + if (!s.empty()) + { + Throw("STData: invalid UINT160 data"); + } + + num.zero(); + } + + STData data(field, num); + return data; + } + else if (typeStr == "UINT256") + { + if (!value.isString()) + { + Throw("STData: expected string for UINT256"); + } + + uint256 num; + + if (auto const s = value.asString(); !num.parseHex(s)) + { + if (!s.empty()) + { + Throw("STData: invalid UINT256 data"); + } + + num.zero(); + } + STData data(field, num); + return data; + } + else if (typeStr == "VL") + { + if (!value.isString()) + { + Throw("STData: expected string for VL"); + } + + try + { + if (auto vBlob = strUnHex(value.asString())) + { + STData data(field, *vBlob); + return data; + } + else + { + Throw("invalid data"); + } + } + catch (std::exception const&) + { + Throw("STData: invalid data"); + } + } + else if (typeStr == "AMOUNT") + { + try + { + STData data(field, amountFromJson(field, value)); + return data; + } + catch (std::exception const&) + { + Throw("STData: invalid data for AMOUNT"); + } + } + else if (typeStr == "ACCOUNT") + { + if (!value.isString()) + { + Throw("STData: expected string for ACCOUNT"); + } + + std::string const strValue = value.asString(); + + try + { + if (AccountID account; account.parseHex(strValue)) + { + STData data(field, account); + return data; + } + + if (auto result = parseBase58(strValue)) + { + STData data(field, *result); + return data; + } + + Throw("STData: invalid data for ACCOUNT"); + } + catch (std::exception const&) + { + Throw("STData: invalid data for ACCOUNT"); + } + } + else if (typeStr == "ISSUE") + { + try + { + STData data(field, issueFromJson(field, value)); + return data; + } + catch (std::exception const&) + { + Throw("STData: invalid data for ISSUE"); + } + } + else if (typeStr == "CURRENCY") + { + try + { + STData data(field, currencyFromJson(field, value)); + return data; + } + catch (std::exception const&) + { + Throw("STData: invalid data for CURRENCY"); + } + } + else if (typeStr == "NUMBER") + { + if (!value.isString()) + { + Throw("STData: expected string for NUMBER"); + } + + STNumber number = numberFromJson(field, value); + STData data(field, number); + return data; + } + + // Handle unknown or unsupported type + Throw("STData: unsupported type string: " + typeStr); +} + +} // namespace xrpl diff --git a/src/libxrpl/protocol/STDataType.cpp b/src/libxrpl/protocol/STDataType.cpp new file mode 100644 index 0000000000..19f0afb91e --- /dev/null +++ b/src/libxrpl/protocol/STDataType.cpp @@ -0,0 +1,253 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +namespace xrpl { + +// TODO +STDataType::STDataType(SField const& n) : STBase(n), inner_type_(STI_NOTPRESENT) +{ +} + +STDataType::STDataType(SField const& n, SerializedTypeID v) + : STBase(n), inner_type_(v), default_(false) +{ +} + +STDataType::STDataType(SerialIter& sit, SField const& name) + : STBase(name), inner_type_(STI_DATA), default_(false) +{ + std::uint16_t stype = SerializedTypeID(sit.get16()); + inner_type_ = stype; +} + +STBase* +STDataType::copy(std::size_t n, void* buf) const +{ + return emplace(n, buf, *this); +} + +STBase* +STDataType::move(std::size_t n, void* buf) +{ + return emplace(n, buf, std::move(*this)); +} + +SerializedTypeID +STDataType::getSType() const +{ + return STI_DATATYPE; +} + +void +STDataType::setInnerSType(SerializedTypeID v) +{ + inner_type_ = v; +} + +void +STDataType::add(Serializer& s) const +{ + s.add16(inner_type_); +} + +bool +STDataType::isEquivalent(STBase const& t) const +{ + auto const* const tPtr = dynamic_cast(&t); + return tPtr && (default_ == tPtr->default_) && (inner_type_ == tPtr->inner_type_); +} + +bool +STDataType::isDefault() const +{ + return default_; +} + +std::string +STDataType::getInnerTypeString() const +{ + std::string inner_type_str = "Unknown"; + // Optionally, convert inner_type_ to its string representation if mappings + // exist + switch (static_cast(inner_type_)) + { + case STI_UINT8: + inner_type_str = "UINT8"; + break; + case STI_UINT16: + inner_type_str = "UINT16"; + break; + case STI_UINT32: + inner_type_str = "UINT32"; + break; + case STI_UINT64: + inner_type_str = "UINT64"; + break; + case STI_UINT128: + inner_type_str = "UINT128"; + break; + case STI_UINT160: + inner_type_str = "UINT160"; + break; + case STI_UINT192: + inner_type_str = "UINT192"; + break; + case STI_UINT256: + inner_type_str = "UINT256"; + break; + case STI_VL: + inner_type_str = "VL"; + break; + case STI_ACCOUNT: + inner_type_str = "ACCOUNT"; + break; + case STI_AMOUNT: + inner_type_str = "AMOUNT"; + break; + case STI_ISSUE: + inner_type_str = "ISSUE"; + break; + case STI_CURRENCY: + inner_type_str = "CURRENCY"; + break; + case STI_NUMBER: + inner_type_str = "NUMBER"; + break; + // Add other known types as needed + default: + inner_type_str = std::to_string(inner_type_); + } + + return inner_type_str; +} + +std::string +STDataType::getText() const +{ + std::string inner_type_str = getInnerTypeString(); + return "STDataType{InnerType: " + inner_type_str + "}"; +} + +Json::Value +STDataType::getJson(JsonOptions) const +{ + Json::Value ret(Json::objectValue); + ret[jss::type] = getInnerTypeString(); + return ret; +} + +STDataType +dataTypeFromJson(SField const& field, Json::Value const& v) +{ + SerializedTypeID typeId = STI_NOTPRESENT; + Json::Value type; + Json::Value value; + + if (!v.isObject()) + { + Throw("STData: expected object"); + } + + type = v[jss::type]; + auto typeStr = type.asString(); + + if (typeStr == "UINT8") + { + typeId = STI_UINT8; + STDataType data(field, typeId); + return data; + } + else if (typeStr == "UINT16") + { + typeId = STI_UINT16; + STDataType data(field, typeId); + return data; + } + else if (typeStr == "UINT32") + { + typeId = STI_UINT32; + STDataType data(field, typeId); + return data; + } + else if (typeStr == "UINT64") + { + typeId = STI_UINT64; + STDataType data(field, typeId); + return data; + } + else if (typeStr == "UINT128") + { + typeId = STI_UINT128; + STDataType data(field, typeId); + return data; + } + else if (typeStr == "UINT160") + { + typeId = STI_UINT160; + STDataType data(field, typeId); + return data; + } + else if (typeStr == "UINT192") + { + typeId = STI_UINT192; + STDataType data(field, typeId); + return data; + } + else if (typeStr == "UINT256") + { + typeId = STI_UINT256; + STDataType data(field, typeId); + return data; + } + else if (typeStr == "VL") + { + typeId = STI_VL; + STDataType data(field, typeId); + return data; + } + else if (typeStr == "ACCOUNT") + { + typeId = STI_ACCOUNT; + STDataType data(field, typeId); + return data; + } + else if (typeStr == "AMOUNT") + { + typeId = STI_AMOUNT; + STDataType data(field, typeId); + return data; + } + else if (typeStr == "ISSUE") + { + typeId = STI_ISSUE; + STDataType data(field, typeId); + return data; + } + else if (typeStr == "CURRENCY") + { + typeId = STI_CURRENCY; + STDataType data(field, typeId); + return data; + } + else if (typeStr == "NUMBER") + { + typeId = STI_NUMBER; + STDataType data(field, typeId); + return data; + } + + // Handle unknown or unsupported type + Throw("STData: unsupported type string: " + typeStr); +} + +} // namespace xrpl diff --git a/src/libxrpl/protocol/STJson.cpp b/src/libxrpl/protocol/STJson.cpp new file mode 100644 index 0000000000..37f214ea96 --- /dev/null +++ b/src/libxrpl/protocol/STJson.cpp @@ -0,0 +1,758 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +namespace xrpl { + +STJson::STJson(SField const& name) : STBase{name}, data_{Map{}} +{ +} + +STJson::STJson(SerialIter& sit, SField const& name) : STBase{name} +{ + if (sit.empty()) + { + data_ = Map{}; + return; + } + + int length = sit.getVLDataLength(); + if (length < 0) + Throw("Invalid STJson length"); + + if (length == 0) + { + data_ = Map{}; + return; + } + + // Read type byte + auto typeByte = sit.get8(); + JsonType type = static_cast(typeByte); + length--; // Account for type byte + + int initialBytesLeft = sit.getBytesLeft(); + + if (type == JsonType::Array) + { + Array array; + while (sit.getBytesLeft() > 0 && (initialBytesLeft - sit.getBytesLeft()) < length) + { + auto valueVL = sit.getVL(); + if (!valueVL.empty()) + { + SerialIter valueSit(valueVL.data(), valueVL.size()); + auto value = makeValueFromVLWithType(valueSit); + array.push_back(std::move(value)); + } + else + { + array.push_back(nullptr); + } + } + data_ = std::move(array); + } + else // JsonType::Object + { + Map map; + while (sit.getBytesLeft() > 0 && (initialBytesLeft - sit.getBytesLeft()) < length) + { + auto [key, value] = parsePair(sit); + map.emplace(std::move(key), std::move(value)); + } + data_ = std::move(map); + } + + int consumedBytes = initialBytesLeft - sit.getBytesLeft(); + if (consumedBytes != length) + Throw("STJson length mismatch"); +} + +STJson::STJson(Map&& map) : data_(std::move(map)) +{ +} + +STJson::STJson(Array&& array) : data_(std::move(array)) +{ +} + +SerializedTypeID +STJson::getSType() const +{ + return STI_JSON; +} + +bool +STJson::isArray() const +{ + return std::holds_alternative(data_); +} + +bool +STJson::isObject() const +{ + return std::holds_alternative(data_); +} + +STJson::JsonType +STJson::getType() const +{ + return isArray() ? JsonType::Array : JsonType::Object; +} + +int +STJson::getDepth() const +{ + if (isArray()) + { + auto const& array = std::get(data_); + for (auto const& value : array) + { + if (value) + { + auto nested = std::dynamic_pointer_cast(value); + if (nested) + return 1 + nested->getDepth(); + } + } + return 0; + } + else // isObject() + { + auto const& map = std::get(data_); + for (auto const& [key, value] : map) + { + if (value) + { + auto nested = std::dynamic_pointer_cast(value); + if (nested) + return 1 + nested->getDepth(); + } + } + return 0; + } +} + +void +STJson::validateDepth(Value const& value, int currentDepth) const +{ + if (!value) + return; + + auto nested = std::dynamic_pointer_cast(value); + if (!nested) + return; + + int valueDepth = nested->getDepth(); + if (currentDepth + valueDepth > 1) + Throw("STJson nesting depth exceeds maximum of 1"); +} + +void +STJson::setObjectField(Key const& key, Value const& value) +{ + if (!isObject()) + Throw("STJson::setObjectField called on non-object"); + validateDepth(value, 0); + std::get(data_)[key] = value; +} + +std::shared_ptr +STJson::fromBlob(void const* data, std::size_t size) +{ + SerialIter sit(static_cast(data), size); + return fromSerialIter(sit); +} + +std::shared_ptr +STJson::fromSerialIter(SerialIter& sit) +{ + if (sit.empty()) + return nullptr; + + int length = sit.getVLDataLength(); + if (length < 0) + Throw("Invalid STJson length"); + + if (length == 0) + return std::make_shared(Map{}); + + // Read type byte + auto typeByte = sit.get8(); + JsonType type = static_cast(typeByte); + length--; // Account for type byte + + int initialBytesLeft = sit.getBytesLeft(); + + if (type == JsonType::Array) + { + Array array; + while (sit.getBytesLeft() > 0 && (initialBytesLeft - sit.getBytesLeft()) < length) + { + auto valueVL = sit.getVL(); + if (!valueVL.empty()) + { + SerialIter valueSit(valueVL.data(), valueVL.size()); + auto value = makeValueFromVLWithType(valueSit); + array.push_back(std::move(value)); + } + else + { + array.push_back(nullptr); + } + } + + int consumedBytes = initialBytesLeft - sit.getBytesLeft(); + if (consumedBytes != length) + Throw("STJson length mismatch"); + + return std::make_shared(std::move(array)); + } + else // JsonType::Object + { + Map map; + while (sit.getBytesLeft() > 0 && (initialBytesLeft - sit.getBytesLeft()) < length) + { + auto [key, value] = parsePair(sit); + map.emplace(std::move(key), std::move(value)); + } + + int consumedBytes = initialBytesLeft - sit.getBytesLeft(); + if (consumedBytes != length) + Throw("STJson length mismatch"); + + return std::make_shared(std::move(map)); + } +} + +std::pair +STJson::parsePair(SerialIter& sit) +{ + auto keyBlob = sit.getVL(); + std::string key(reinterpret_cast(keyBlob.data()), keyBlob.size()); + auto valueVL = sit.getVL(); + if (valueVL.empty()) + return {std::move(key), nullptr}; + + SerialIter valueSit(valueVL.data(), valueVL.size()); + auto value = makeValueFromVLWithType(valueSit); + + return {std::move(key), std::move(value)}; +} + +STJson::Array +STJson::parseArray(SerialIter& sit, int length) +{ + Array array; + int initialBytesLeft = sit.getBytesLeft(); + + while (sit.getBytesLeft() > 0 && (initialBytesLeft - sit.getBytesLeft()) < length) + { + auto valueVL = sit.getVL(); + if (!valueVL.empty()) + { + SerialIter valueSit(valueVL.data(), valueVL.size()); + auto value = makeValueFromVLWithType(valueSit); + array.push_back(std::move(value)); + } + else + { + array.push_back(nullptr); + } + } + + return array; +} + +STJson::Value +STJson::makeValueFromVLWithType(SerialIter& sit) +{ + if (sit.getBytesLeft() == 0) + return nullptr; + + // Read SType marker (1 byte) + auto typeCode = sit.get8(); + SerializedTypeID stype = static_cast(typeCode); + + // Dispatch to correct SType + switch (stype) + { + case STI_UINT8: + return std::make_shared(sfCloseResolution, sit.get8()); + case STI_UINT16: + return std::make_shared(sfSignerWeight, sit.get16()); + case STI_UINT32: + return std::make_shared(sfNetworkID, sit.get32()); + case STI_UINT64: + return std::make_shared(sfIndexNext, sit.get64()); + case STI_UINT128: + return std::make_shared(sfEmailHash, sit.get128()); + case STI_UINT160: + return std::make_shared(sfTakerPaysCurrency, sit.get160()); + case STI_UINT192: + return std::make_shared(sfMPTokenIssuanceID, sit.get192()); + case STI_UINT256: + return std::make_shared(sfLedgerHash, sit.get256()); + case STI_VL: { + auto blob = sit.getVL(); + return std::make_shared(sfData, blob.data(), blob.size()); + } + case STI_ACCOUNT: + return std::make_shared(sit, sfAccount); + case STI_AMOUNT: + return std::make_shared(sit, sfAmount); + // case STI_NUMBER: + // return std::make_shared(sit, sfNumber); + case STI_ISSUE: + return std::make_shared(sit, sfAsset); + case STI_CURRENCY: + return std::make_shared(sit, sfBaseAsset); + case STI_JSON: + return std::make_shared(sit, sfContractJson); + case STI_OBJECT: + case STI_ARRAY: + case STI_PATHSET: + case STI_VECTOR256: + default: + // Unknown type, treat as blob + { + auto blob = sit.getSlice(sit.getBytesLeft()); + return std::make_shared(sfData, blob.data(), blob.size()); + } + } +} + +std::optional +STJson::getObjectField(Key const& key) const +{ + if (!isObject()) + return std::nullopt; + + auto const& map = std::get(data_); + auto it = map.find(key); + if (it == map.end() || !it->second) + return std::nullopt; + return it->second; +} + +void +STJson::setNestedObjectField(Key const& key, Key const& nestedKey, Value const& value) +{ + if (!isObject()) + Throw("STJson::setNestedObjectField called on non-object"); + + validateDepth(value, 1); // We're at depth 1 (nested) + + auto& map = std::get(data_); + auto it = map.find(key); + std::shared_ptr nested; + if (it == map.end() || !it->second) + { + // Create new nested STJson + nested = std::make_shared(); + map[key] = nested; + } + else + { + nested = std::dynamic_pointer_cast(it->second); + if (!nested) + { + // Overwrite with new STJson if not an STJson + nested = std::make_shared(); + map[key] = nested; + } + } + nested->setObjectField(nestedKey, value); +} + +std::optional +STJson::getNestedObjectField(Key const& key, Key const& nestedKey) const +{ + if (!isObject()) + return std::nullopt; + + auto const& map = std::get(data_); + auto it = map.find(key); + if (it == map.end() || !it->second) + return std::nullopt; + auto nested = std::dynamic_pointer_cast(it->second); + if (!nested) + return std::nullopt; + return nested->getObjectField(nestedKey); +} + +STJson::Map const& +STJson::getMap() const +{ + if (!isObject()) + Throw("STJson::getMap called on non-object"); + return std::get(data_); +} + +STJson::Array const& +STJson::getArray() const +{ + if (!isArray()) + Throw("STJson::getArray called on non-array"); + return std::get(data_); +} + +void +STJson::pushArrayElement(Value const& value) +{ + if (!isArray()) + Throw("STJson::pushArrayElement called on non-array"); + validateDepth(value, 0); + std::get(data_).push_back(value); +} + +std::optional +STJson::getArrayElement(size_t index) const +{ + if (!isArray()) + return std::nullopt; + + auto const& array = std::get(data_); + if (index >= array.size()) + return std::nullopt; + + return array[index]; +} + +void +STJson::setArrayElement(size_t index, Value const& value) +{ + if (!isArray()) + Throw("STJson::setArrayElement called on non-array"); + validateDepth(value, 0); + + auto& array = std::get(data_); + // Auto-resize with nulls if needed + if (index >= array.size()) + array.resize(index + 1, nullptr); + + array[index] = value; +} + +void +STJson::setArrayElementField(size_t index, Key const& key, Value const& value) +{ + if (!isArray()) + Throw("STJson::setArrayElementField called on non-array"); + + validateDepth(value, 1); // We're at depth 1 (inside array element) + + auto& array = std::get(data_); + // Auto-resize with nulls if needed + if (index >= array.size()) + array.resize(index + 1, nullptr); + + // Get or create STJson object at index + std::shared_ptr element; + if (!array[index]) + { + element = std::make_shared(Map{}); + array[index] = element; + } + else + { + element = std::dynamic_pointer_cast(array[index]); + if (!element) + { + // Replace with new STJson if not an STJson + element = std::make_shared(Map{}); + array[index] = element; + } + } + + element->setObjectField(key, value); +} + +std::optional +STJson::getArrayElementField(size_t index, Key const& key) const +{ + if (!isArray()) + return std::nullopt; + + auto const& array = std::get(data_); + if (index >= array.size()) + return std::nullopt; + + auto element = std::dynamic_pointer_cast(array[index]); + if (!element) + return std::nullopt; + + return element->getObjectField(key); +} + +size_t +STJson::arraySize() const +{ + if (!isArray()) + return 0; + return std::get(data_).size(); +} + +void +STJson::setNestedArrayElement(Key const& key, size_t index, Value const& value) +{ + if (!isObject()) + Throw("STJson::setNestedArrayElement called on non-object"); + + validateDepth(value, 1); // We're at depth 1 (nested array) + + auto& map = std::get(data_); + auto it = map.find(key); + std::shared_ptr arrayJson; + + if (it == map.end() || !it->second) + { + // Create new nested STJson array + arrayJson = std::make_shared(Array{}); + map[key] = arrayJson; + } + else + { + arrayJson = std::dynamic_pointer_cast(it->second); + if (!arrayJson) + { + // Replace with new STJson array if not an STJson + arrayJson = std::make_shared(Array{}); + map[key] = arrayJson; + } + else if (!arrayJson->isArray()) + { + // Replace with array if not an array + arrayJson = std::make_shared(Array{}); + map[key] = arrayJson; + } + } + + arrayJson->setArrayElement(index, value); +} + +void +STJson::setNestedArrayElementField( + Key const& key, + size_t index, + Key const& nestedKey, + Value const& value) +{ + if (!isObject()) + Throw("STJson::setNestedArrayElementField called on non-object"); + + validateDepth(value, 1); // We're at depth 1 (nested array element field - + // still counts as depth 1) + + auto& map = std::get(data_); + auto it = map.find(key); + std::shared_ptr arrayJson; + + if (it == map.end() || !it->second) + { + // Create new nested STJson array + arrayJson = std::make_shared(Array{}); + map[key] = arrayJson; + } + else + { + arrayJson = std::dynamic_pointer_cast(it->second); + if (!arrayJson) + { + // Replace with new STJson array if not an STJson + arrayJson = std::make_shared(Array{}); + map[key] = arrayJson; + } + else if (!arrayJson->isArray()) + { + // Replace with array if not an array + arrayJson = std::make_shared(Array{}); + map[key] = arrayJson; + } + } + + arrayJson->setArrayElementField(index, nestedKey, value); +} + +std::optional +STJson::getNestedArrayElement(Key const& key, size_t index) const +{ + if (!isObject()) + return std::nullopt; + + auto const& map = std::get(data_); + auto it = map.find(key); + if (it == map.end() || !it->second) + return std::nullopt; + + auto arrayJson = std::dynamic_pointer_cast(it->second); + if (!arrayJson || !arrayJson->isArray()) + return std::nullopt; + + return arrayJson->getArrayElement(index); +} + +std::optional +STJson::getNestedArrayElementField(Key const& key, size_t index, Key const& nestedKey) const +{ + if (!isObject()) + return std::nullopt; + + auto const& map = std::get(data_); + auto it = map.find(key); + if (it == map.end() || !it->second) + return std::nullopt; + + auto arrayJson = std::dynamic_pointer_cast(it->second); + if (!arrayJson || !arrayJson->isArray()) + return std::nullopt; + + return arrayJson->getArrayElementField(index, nestedKey); +} + +void +STJson::addVLKey(Serializer& s, std::string const& str) +{ + s.addVL(str.data(), str.size()); +} + +void +STJson::addVLValue(Serializer& s, std::shared_ptr const& value) +{ + if (!value) + { + s.addVL(nullptr, 0); + return; + } + Serializer tmp; + tmp.add8(static_cast(value->getSType())); + value->add(tmp); + s.addVL(tmp.peekData().data(), tmp.peekData().size()); +} + +void +STJson::add(Serializer& s) const +{ + Serializer inner; + + // Add type byte + inner.add8(static_cast(getType())); + + if (isArray()) + { + auto const& array = std::get(data_); + for (auto const& value : array) + { + addVLValue(inner, value); + } + } + else // isObject() + { + auto const& map = std::get(data_); + for (auto const& [key, value] : map) + { + addVLKey(inner, key); + addVLValue(inner, value); + } + } + + s.addVL(inner.peekData().data(), inner.peekData().size()); +} + +Json::Value +STJson::getJson(JsonOptions options) const +{ + if (isArray()) + { + Json::Value arr(Json::arrayValue); + auto const& array = std::get(data_); + for (auto const& value : array) + { + if (value) + arr.append(value->getJson(options)); + else + arr.append(Json::nullValue); + } + return arr; + } + else // isObject() + { + Json::Value obj(Json::objectValue); + auto const& map = std::get(data_); + for (auto const& [key, value] : map) + { + if (value) + obj[key] = value->getJson(options); + else + obj[key] = Json::nullValue; + } + return obj; + } +} + +bool +STJson::isEquivalent(STBase const& t) const +{ + auto const* const tPtr = dynamic_cast(&t); + return tPtr && (data_ == tPtr->data_); +} + +bool +STJson::isDefault() const +{ + return default_; +} + +Blob +STJson::toBlob() const +{ + Serializer s; + add(s); + return s.peekData(); +} + +std::size_t +STJson::size() const +{ + Serializer s; + add(s); + return s.size(); +} + +void +STJson::setValue(STJson const& v) +{ + data_ = v.data_; +} + +STBase* +STJson::copy(std::size_t n, void* buf) const +{ + return emplace(n, buf, *this); +} + +STBase* +STJson::move(std::size_t n, void* buf) +{ + return emplace(n, buf, std::move(*this)); +} + +} // namespace xrpl diff --git a/src/libxrpl/protocol/STObject.cpp b/src/libxrpl/protocol/STObject.cpp index f758760337..239d8e7080 100644 --- a/src/libxrpl/protocol/STObject.cpp +++ b/src/libxrpl/protocol/STObject.cpp @@ -19,8 +19,11 @@ #include #include #include +#include +#include #include #include +#include #include #include #include @@ -637,6 +640,20 @@ STObject::getAccountID(SField const& field) const return getFieldByValue(field); } +STData +STObject::getFieldData(SField const& field) const +{ + static STData const empty{field}; + return getFieldByConstRef(field, empty); +} + +STDataType +STObject::getFieldDataType(SField const& field) const +{ + static STDataType const empty{field}; + return getFieldByConstRef(field, empty); +} + Blob STObject::getFieldVL(SField const& field) const { @@ -697,6 +714,13 @@ STObject::getFieldNumber(SField const& field) const return getFieldByConstRef(field, empty); } +STJson const& +STObject::getFieldJson(SField const& field) const +{ + static STJson const empty{field}; + return getFieldByConstRef(field, empty); +} + void STObject::set(std::unique_ptr v) { @@ -719,6 +743,69 @@ STObject::set(STBase&& v) } } +void +STObject::addFieldFromSlice(SField const& sfield, Slice const& data) +{ + SerialIter sit(data.data(), data.size()); + std::unique_ptr element; + + switch (sfield.fieldType) + { + case STI_AMOUNT: + element = std::make_unique(sit, sfield); + break; + case STI_ACCOUNT: + element = std::make_unique(sit, sfield); + break; + case STI_UINT8: + element = std::make_unique(sit, sfield); + break; + case STI_UINT16: + element = std::make_unique(sit, sfield); + break; + case STI_UINT32: + element = std::make_unique(sit, sfield); + break; + case STI_UINT64: + element = std::make_unique(sit, sfield); + break; + case STI_UINT128: + element = std::make_unique(sit, sfield); + break; + case STI_UINT160: + element = std::make_unique(sit, sfield); + break; + case STI_UINT256: + element = std::make_unique(sit, sfield); + break; + case STI_VECTOR256: + element = std::make_unique(sit, sfield); + break; + case STI_VL: + element = std::make_unique(sit, sfield); + break; + case STI_CURRENCY: + element = std::make_unique(sit, sfield); + break; + case STI_ISSUE: + element = std::make_unique(sit, sfield); + break; + case STI_PATHSET: + element = std::make_unique(sit, sfield); + break; + case STI_ARRAY: + element = std::make_unique(sit, sfield); + break; + case STI_OBJECT: + element = std::make_unique(sit, sfield, 0); + break; + default: + throw std::runtime_error("Unsupported SField type"); + } + + this->set(std::move(element)); +} + void STObject::setFieldU8(SField const& field, unsigned char v) { @@ -827,6 +914,12 @@ STObject::setFieldObject(SField const& field, STObject const& v) setFieldUsingAssignment(field, v); } +void +STObject::setFieldJson(SField const& field, STJson const& v) +{ + setFieldUsingAssignment(field, v); +} + Json::Value STObject::getJson(JsonOptions options) const { diff --git a/src/libxrpl/protocol/STParsedJSON.cpp b/src/libxrpl/protocol/STParsedJSON.cpp index c928f49375..3d18907939 100644 --- a/src/libxrpl/protocol/STParsedJSON.cpp +++ b/src/libxrpl/protocol/STParsedJSON.cpp @@ -16,6 +16,8 @@ #include #include #include +#include +#include #include #include #include @@ -903,6 +905,52 @@ parseLeaf( } break; + case STI_DATA: { + try + { + ret = detail::make_stvar(dataFromJson(field, value)); + } + catch (std::exception const&) + { + std::cout << "STI_DATA failed for field: " << fieldName + << " in object: " << json_name << "\n"; + error = invalid_data(json_name, fieldName); + return ret; + } + + break; + } + case STI_DATATYPE: { + try + { + ret = detail::make_stvar(dataTypeFromJson(field, value)); + } + catch (std::exception const&) + { + std::cout << "STI_DATATYPE failed for field: " << fieldName + << " in object: " << json_name << "\n"; + error = invalid_data(json_name, fieldName); + return ret; + } + break; + } + + case STI_JSON: + Throw("STI_JSON is not supported"); + // try + // { + // ret = detail::make_stvar( + // dataTypeFromJson(field, value)); + // } + // catch (std::exception const&) + // { + // std::cout << "STI_DATATYPE failed for field: " << fieldName + // << " in object: " << json_name << "\n"; + // error = invalid_data(json_name, fieldName); + // return ret; + // } + break; + default: error = bad_type(json_name, fieldName); return ret; diff --git a/src/libxrpl/protocol/STVar.cpp b/src/libxrpl/protocol/STVar.cpp index 8b76d8a322..4987685adb 100644 --- a/src/libxrpl/protocol/STVar.cpp +++ b/src/libxrpl/protocol/STVar.cpp @@ -8,8 +8,11 @@ #include #include #include +#include +#include #include #include +#include #include #include #include @@ -222,6 +225,15 @@ STVar::constructST(SerializedTypeID id, int depth, Args&&... args) case STI_CURRENCY: construct(std::forward(args)...); return; + case STI_DATA: + construct(std::forward(args)...); + return; + case STI_DATATYPE: + construct(std::forward(args)...); + return; + case STI_JSON: + construct(std::forward(args)...); + return; default: Throw("Unknown object type"); } diff --git a/src/libxrpl/protocol/TER.cpp b/src/libxrpl/protocol/TER.cpp index 09c8acd4e3..abbd5b6b6c 100644 --- a/src/libxrpl/protocol/TER.cpp +++ b/src/libxrpl/protocol/TER.cpp @@ -107,6 +107,7 @@ transResults() MAKE_ERROR(tecPSEUDO_ACCOUNT, "This operation is not allowed against a pseudo-account."), MAKE_ERROR(tecPRECISION_LOSS, "The amounts used by the transaction cannot interact."), MAKE_ERROR(tecWASM_REJECTED, "The custom WASM code that was run rejected your transaction."), + MAKE_ERROR(tecINVALID_PARAMETERS, "Contract parameters do not match the expected ABI."), MAKE_ERROR(tefALREADY, "The exact transaction was already in this ledger."), MAKE_ERROR(tefBAD_ADD_AUTH, "Not authorized to add account."), diff --git a/src/libxrpl/protocol/TxFormats.cpp b/src/libxrpl/protocol/TxFormats.cpp index 4492ae271b..4fcfae9762 100644 --- a/src/libxrpl/protocol/TxFormats.cpp +++ b/src/libxrpl/protocol/TxFormats.cpp @@ -40,7 +40,7 @@ TxFormats::TxFormats() #undef TRANSACTION #define UNWRAP(...) __VA_ARGS__ -#define TRANSACTION(tag, value, name, delegable, amendment, privileges, fields) \ +#define TRANSACTION(tag, value, name, delegatable, amendment, privileges, emitable, fields) \ add(jss::name, tag, UNWRAP fields, getCommonFields()); #include diff --git a/src/libxrpl/server/InfoSub.cpp b/src/libxrpl/server/InfoSub.cpp index ea70dc3a83..4709ea9928 100644 --- a/src/libxrpl/server/InfoSub.cpp +++ b/src/libxrpl/server/InfoSub.cpp @@ -32,6 +32,7 @@ InfoSub::~InfoSub() m_source.unsubValidations(mSeq); m_source.unsubPeerStatus(mSeq); m_source.unsubConsensus(mSeq); + m_source.unsubContractEvent(mSeq); // Use the internal unsubscribe so that it won't call // back to us and modify its own parameter diff --git a/src/libxrpl/tx/ApplyContext.cpp b/src/libxrpl/tx/ApplyContext.cpp index cd379dd960..8fcdd73486 100644 --- a/src/libxrpl/tx/ApplyContext.cpp +++ b/src/libxrpl/tx/ApplyContext.cpp @@ -28,13 +28,21 @@ ApplyContext::ApplyContext( XRPL_ASSERT( parentBatchId.has_value() == ((flags_ & tapBATCH) == tapBATCH), "Parent Batch ID should be set if batch apply flag is set"); - view_.emplace(&base_, flags_); + view_.emplace(&base_.view(), flags_); } void ApplyContext::discard() { - view_.emplace(&base_, flags_); + base_.discard(); + view_.emplace(&base_.view(), flags_); +} + +void +ApplyContext::finalize() +{ + base_.commit(); + view_.emplace(&base_.view(), flags_); } std::optional @@ -45,7 +53,8 @@ ApplyContext::apply(TER ter) view_->setWasmReturnCode(*wasmReturnCode_); } view_->setGasUsed(gasUsed_); - return view_->apply(base_, tx, ter, parentBatchId_, (flags_ & tapDRY_RUN) != 0u, journal); + return view_->apply( + base_.view(), tx, ter, parentBatchId_, (flags_ & tapDRY_RUN) != 0u, journal); } std::size_t @@ -62,7 +71,7 @@ ApplyContext::visit( std::shared_ptr const&, std::shared_ptr const&)> const& func) { - view_->visit(base_, func); + view_->visit(base_.view(), func); } TER diff --git a/src/libxrpl/tx/Transactor.cpp b/src/libxrpl/tx/Transactor.cpp index cb65fc5ce5..bb11bc825f 100644 --- a/src/libxrpl/tx/Transactor.cpp +++ b/src/libxrpl/tx/Transactor.cpp @@ -4,6 +4,7 @@ #include #include #include +#include #include #include #include @@ -17,7 +18,6 @@ #include #include #include -#include namespace xrpl { @@ -582,6 +582,32 @@ Transactor::ticketDelete( return tesSUCCESS; } +std::pair +Transactor::checkInvariants(TER result, XRPAmount fee) +{ + // Check invariants: if `tecINVARIANT_FAILED` is not returned, we can + // proceed to apply the tx + result = ctx_.checkInvariants(result, fee); + + if (result == tecINVARIANT_FAILED) + { + // if invariants checking failed again, reset the context and + // attempt to only claim a fee. + auto const resetResult = reset(fee); + if (!isTesSuccess(resetResult.first)) + result = resetResult.first; + + fee = resetResult.second; + + // Check invariants again to ensure the fee claiming doesn't + // violate invariants. + if (isTesSuccess(result) || isTecClaim(result)) + result = ctx_.checkInvariants(result, fee); + } + + return {result, fee}; +} + // check stuff before you bother to lock the ledger void Transactor::preCompute() @@ -647,7 +673,8 @@ Transactor::checkSign( } auto const pkSigner = sigObject.getFieldVL(sfSigningPubKey); - // Ignore signature check on batch inner transactions + // Ignore signature check on batch inner transactions (e.g., emitted + // transactions from contracts) if (parentBatchId && view.rules().enabled(featureBatch)) { // Defensive Check: These values are also checked in Batch::preflight @@ -1046,8 +1073,10 @@ Transactor::reset(XRPAmount fee) auto const balance = payerSle->getFieldAmount(sfBalance).xrp(); // balance should have already been checked in checkFee / preFlight. + // For batch/inner transactions (fee == 0), the balance can + // legitimately be zero (e.g. contract pseudo-accounts). XRPL_ASSERT( - balance != beast::zero && (!view().open() || balance >= fee), + (fee == beast::zero || balance != beast::zero) && (!view().open() || balance >= fee), "xrpl::Transactor::reset : valid balance"); // We retry/reject the transaction if the account balance is zero or @@ -1262,26 +1291,9 @@ Transactor::operator()() if (applied) { - // Check invariants: if `tecINVARIANT_FAILED` is not returned, we can - // proceed to apply the tx - result = ctx_.checkInvariants(result, fee); - - if (result == tecINVARIANT_FAILED) - { - // if invariants checking failed again, reset the context and - // attempt to only claim a fee. - auto const resetResult = reset(fee); - if (!isTesSuccess(resetResult.first)) - result = resetResult.first; - - fee = resetResult.second; - - // Check invariants again to ensure the fee claiming doesn't - // violate invariants. - if (isTesSuccess(result) || isTecClaim(result)) - result = ctx_.checkInvariants(result, fee); - } - + auto const invariantsResult = checkInvariants(result, fee); + result = invariantsResult.first; + fee = invariantsResult.second; // We ran through the invariant checker, which can, in some cases, // return a tef error code. Don't apply the transaction in that case. if (!isTecClaim(result) && !isTesSuccess(result)) @@ -1316,6 +1328,70 @@ Transactor::operator()() applied = false; } + if (metadata && ctx_.getEmittedTxns().size() > 0) + { + OpenView emittedTxnsView(batch_view, ctx_.openView()); + auto const parentBatchId = ctx_.tx.getTransactionID(); + + auto applyOneTransaction = [this, &parentBatchId, &emittedTxnsView](STTx const& tx) { + OpenView perTxBatchView(batch_view, emittedTxnsView); + + auto const ret = xrpl::apply( + ctx_.registry, perTxBatchView, parentBatchId, tx, tapBATCH, ctx_.journal); + XRPL_ASSERT( + ret.applied == (isTesSuccess(ret.ter) || isTecClaim(ret.ter)), + "Inner transaction should not be applied"); + + JLOG(ctx_.journal.debug()) + << "BatchTrace[" << parentBatchId << "]: " << tx.getTransactionID() << " " + << (ret.applied ? "applied" : "failure") << ": " << transToken(ret.ter); + + // If the transaction should be applied push its changes to the + // whole-batch view. + if (ret.applied && (isTesSuccess(ret.ter) || isTecClaim(ret.ter))) + perTxBatchView.apply(emittedTxnsView); + + return ret; + }; + + bool emitResult = true; + auto emittedTxns = ctx_.getEmittedTxns(); + while (!emittedTxns.empty()) + { + auto txn = emittedTxns.front(); + emittedTxns.pop(); + auto const result = applyOneTransaction(*txn); + XRPL_ASSERT( + result.applied == (isTesSuccess(result.ter) || isTecClaim(result.ter)), + "Outer Batch failure, inner transaction should not be applied"); + + if (!isTesSuccess(result.ter)) + emitResult = false; + } + + if (emitResult) + emittedTxnsView.apply(ctx_.openView()); + else + { + // reset context + result = tecWASM_REJECTED; + auto const resetResult = reset(fee); + if (!isTesSuccess(resetResult.first)) + result = resetResult.first; + fee = resetResult.second; + + // InvariantCheck + auto const invariantsResult = checkInvariants(result, fee); + result = invariantsResult.first; + fee = invariantsResult.second; + + // apply + metadata = ctx_.apply(result); + } + } + + ctx_.finalize(); + JLOG(j_.trace()) << (applied ? "applied " : "not applied ") << transToken(result); return {result, applied, metadata}; diff --git a/src/libxrpl/tx/invariants/InvariantCheck.cpp b/src/libxrpl/tx/invariants/InvariantCheck.cpp index bf0f65f000..cf2a737c2b 100644 --- a/src/libxrpl/tx/invariants/InvariantCheck.cpp +++ b/src/libxrpl/tx/invariants/InvariantCheck.cpp @@ -859,7 +859,8 @@ ValidPseudoAccounts::visitEntry( errors_.emplace_back(error.str()); } } - if (before && before->at(sfSequence) != after->at(sfSequence)) + if (before && before->at(sfSequence) != after->at(sfSequence) && + !after->isFieldPresent(sfContractID)) { errors_.emplace_back("pseudo-account sequence changed"); } diff --git a/src/libxrpl/tx/invariants/NFTInvariant.cpp b/src/libxrpl/tx/invariants/NFTInvariant.cpp index cf00dc9290..0e09dbb1b8 100644 --- a/src/libxrpl/tx/invariants/NFTInvariant.cpp +++ b/src/libxrpl/tx/invariants/NFTInvariant.cpp @@ -2,11 +2,12 @@ // #include #include +#include #include +#include #include #include #include -#include namespace xrpl { diff --git a/src/libxrpl/tx/transactors/DeleteUtils.cpp b/src/libxrpl/tx/transactors/DeleteUtils.cpp new file mode 100644 index 0000000000..78dacd1344 --- /dev/null +++ b/src/libxrpl/tx/transactors/DeleteUtils.cpp @@ -0,0 +1,382 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +namespace xrpl { + +// Local function definitions that provides signature compatibility. +TER +offerDelete( + ServiceRegistry& registry, + ApplyView& view, + AccountID const& account, + uint256 const& delIndex, + std::shared_ptr const& sleDel, + beast::Journal j) +{ + return offerDelete(view, sleDel, j); +} + +TER +removeSignersFromLedger( + ServiceRegistry& registry, + ApplyView& view, + AccountID const& account, + uint256 const& delIndex, + std::shared_ptr const& sleDel, + beast::Journal j) +{ + return SignerListSet::removeFromLedger(registry, view, account, j); +} + +TER +removeTicketFromLedger( + ServiceRegistry&, + ApplyView& view, + AccountID const& account, + uint256 const& delIndex, + std::shared_ptr const&, + beast::Journal j) +{ + return Transactor::ticketDelete(view, account, delIndex, j); +} + +TER +removeDepositPreauthFromLedger( + ServiceRegistry&, + ApplyView& view, + AccountID const&, + uint256 const& delIndex, + std::shared_ptr const&, + beast::Journal j) +{ + return DepositPreauth::removeFromLedger(view, delIndex, j); +} + +TER +removeNFTokenOfferFromLedger( + ServiceRegistry& registry, + ApplyView& view, + AccountID const& account, + uint256 const& delIndex, + std::shared_ptr const& sleDel, + beast::Journal) +{ + if (!nft::deleteTokenOffer(view, sleDel)) + return tefBAD_LEDGER; + + return tesSUCCESS; +} + +TER +removeDIDFromLedger( + ServiceRegistry& registry, + ApplyView& view, + AccountID const& account, + uint256 const& delIndex, + std::shared_ptr const& sleDel, + beast::Journal j) +{ + return DIDDelete::deleteSLE(view, sleDel, account, j); +} + +TER +removeOracleFromLedger( + ServiceRegistry&, + ApplyView& view, + AccountID const& account, + uint256 const&, + std::shared_ptr const& sleDel, + beast::Journal j) +{ + return OracleDelete::deleteOracle(view, sleDel, account, j); +} + +TER +removeCredentialFromLedger( + ServiceRegistry&, + ApplyView& view, + AccountID const&, + uint256 const&, + std::shared_ptr const& sleDel, + beast::Journal j) +{ + return credentials::deleteSLE(view, sleDel, j); +} + +TER +removeDelegateFromLedger( + ServiceRegistry& registry, + ApplyView& view, + AccountID const& account, + uint256 const& delIndex, + std::shared_ptr const& sleDel, + beast::Journal j) +{ + return DelegateSet::deleteDelegate(view, sleDel, account, j); +} + +TER +removeContractFromLedger( + ServiceRegistry& registry, + ApplyView& view, + AccountID const& account, + uint256 const& delIndex, + std::shared_ptr const& sleDel, + beast::Journal j) +{ + return ContractDelete::deleteContract(view, sleDel, account, j); +} + +// Return nullptr if the LedgerEntryType represents an obligation that can't +// be deleted. Otherwise return the pointer to the function that can delete +// the non-obligation +DeleterFuncPtr +nonObligationDeleter(LedgerEntryType t) +{ + switch (t) + { + case ltOFFER: + return offerDelete; + case ltSIGNER_LIST: + return removeSignersFromLedger; + case ltTICKET: + return removeTicketFromLedger; + case ltDEPOSIT_PREAUTH: + return removeDepositPreauthFromLedger; + case ltNFTOKEN_OFFER: + return removeNFTokenOfferFromLedger; + case ltDID: + return removeDIDFromLedger; + case ltORACLE: + return removeOracleFromLedger; + case ltCREDENTIAL: + return removeCredentialFromLedger; + case ltDELEGATE: + return removeDelegateFromLedger; + case ltCONTRACT: + return removeContractFromLedger; + default: + return nullptr; + } +} + +TER +deletePreclaim( + PreclaimContext const& ctx, + std::uint32_t seqDelta, + AccountID const account, + AccountID const dest, + bool isPseudoAccount) +{ + auto destSle = ctx.view.read(keylet::account(dest)); + + if (!destSle) + return tecNO_DST; + + if ((*destSle)[sfFlags] & lsfRequireDestTag && !ctx.tx[~sfDestinationTag]) + return tecDST_TAG_NEEDED; + + // If credentials are provided - check them anyway + if (auto const err = credentials::valid(ctx.tx, ctx.view, account, ctx.j); !isTesSuccess(err)) + return err; + + // if credentials then postpone auth check to doApply, to check for expired + // credentials + if (!ctx.tx.isFieldPresent(sfCredentialIDs)) + { + // Check whether the destination account requires deposit authorization. + if (destSle->getFlags() & lsfDepositAuth) + { + if (!ctx.view.exists(keylet::depositPreauth(dest, account)) && !isPseudoAccount) + return tecNO_PERMISSION; + } + } + + auto srcSle = ctx.view.read(keylet::account(account)); + XRPL_ASSERT(srcSle, "xrpl::DeleteAccount::preclaim : non-null account"); + if (!srcSle) + return terNO_ACCOUNT; + + { + // If an issuer has any issued NFTs resident in the ledger then it + // cannot be deleted. + if ((*srcSle)[~sfMintedNFTokens] != (*srcSle)[~sfBurnedNFTokens]) + return tecHAS_OBLIGATIONS; + + // If the account owns any NFTs it cannot be deleted. + Keylet const first = keylet::nftpage_min(account); + Keylet const last = keylet::nftpage_max(account); + + auto const cp = ctx.view.read( + Keylet(ltNFTOKEN_PAGE, ctx.view.succ(first.key, last.key.next()).value_or(last.key))); + if (cp) + return tecHAS_OBLIGATIONS; + } + + // We don't allow an account to be deleted if its sequence number + // is within 256 of the current ledger. This prevents replay of old + // transactions if this account is resurrected after it is deleted. + // + // We look at the account's Sequence rather than the transaction's + // Sequence in preparation for Tickets. + if ((*srcSle)[sfSequence] + seqDelta > ctx.view.seq()) + return tecTOO_SOON; + + // When fixNFTokenRemint is enabled, we don't allow an account to be + // deleted if is within 256 of the + // current ledger. This is to prevent having duplicate NFTokenIDs after + // account re-creation. + // + // Without this restriction, duplicate NFTokenIDs can be reproduced when + // authorized minting is involved. Because when the minter mints a NFToken, + // the issuer's sequence does not change. So when the issuer re-creates + // their account and mints a NFToken, it is possible that the + // NFTokenSequence of this NFToken is the same as the one that the + // authorized minter minted in a previous ledger. + if ((*srcSle)[~sfFirstNFTokenSequence].value_or(0) + (*srcSle)[~sfMintedNFTokens].value_or(0) + + seqDelta > + ctx.view.seq()) + return tecTOO_SOON; + + // Verify that the account does not own any objects that would prevent + // the account from being deleted. + Keylet const ownerDirKeylet{keylet::ownerDir(account)}; + if (dirIsEmpty(ctx.view, ownerDirKeylet)) + return tesSUCCESS; + + std::shared_ptr sleDirNode{}; + unsigned int uDirEntry{0}; + uint256 dirEntry{beast::zero}; + + // Account has no directory at all. This _should_ have been caught + // by the dirIsEmpty() check earlier, but it's okay to catch it here. + if (!cdirFirst(ctx.view, ownerDirKeylet.key, sleDirNode, uDirEntry, dirEntry)) + return tesSUCCESS; + + std::int32_t deletableDirEntryCount{0}; + do + { + // Make sure any directory node types that we find are the kind + // we can delete. + auto sleItem = ctx.view.read(keylet::child(dirEntry)); + if (!sleItem) + { + // Directory node has an invalid index. Bail out. + JLOG(ctx.j.fatal()) << "DeleteAccount: directory node in ledger " << ctx.view.seq() + << " has index to object that is missing: " << to_string(dirEntry); + return tefBAD_LEDGER; + } + + LedgerEntryType const nodeType{safe_cast((*sleItem)[sfLedgerEntryType])}; + + if (!nonObligationDeleter(nodeType)) + return tecHAS_OBLIGATIONS; + + // We found a deletable directory entry. Count it. If we find too + // many deletable directory entries then bail out. + if (++deletableDirEntryCount > maxDeletableDirEntries) + return tefTOO_BIG; + + } while (cdirNext(ctx.view, ownerDirKeylet.key, sleDirNode, uDirEntry, dirEntry)); + + return tesSUCCESS; +} + +TER +deleteDoApply( + ApplyContext& applyCtx, + STAmount const& accountBalance, + AccountID const& account, + AccountID const& dest) +{ + auto& view = applyCtx.view(); + STTx const tx = applyCtx.tx; + beast::Journal j = applyCtx.journal; + + auto srcSle = view.peek(keylet::account(account)); + XRPL_ASSERT(srcSle, "xrpl::deleteDoApply : non-null source account"); + + if (!srcSle) + return tefBAD_LEDGER; + + auto destSle = view.peek(keylet::account(dest)); + XRPL_ASSERT(destSle, "xrpl::deleteDoApply : non-null destination account"); + + if (!destSle) + return tefBAD_LEDGER; + + if (tx.isFieldPresent(sfCredentialIDs)) + { + if (auto err = verifyDepositPreauth(tx, view, account, dest, destSle, j); + !isTesSuccess(err)) + return err; + } + + Keylet const ownerDirKeylet{keylet::ownerDir(account)}; + auto const ter = cleanupOnAccountDelete( + view, + ownerDirKeylet, + [&](LedgerEntryType nodeType, + uint256 const& dirEntry, + std::shared_ptr& sleItem) -> std::pair { + if (auto deleter = nonObligationDeleter(nodeType)) + { + TER const result{deleter(applyCtx.registry, view, account, dirEntry, sleItem, j)}; + + return {result, SkipEntry::No}; + } + + UNREACHABLE( + "xrpl::deleteDoApply : undeletable item not found " + "in preclaim"); + JLOG(j.error()) << "DeleteAccount undeletable item not " + "found in preclaim."; + return {tecHAS_OBLIGATIONS, SkipEntry::No}; + }, + j); + if (ter != tesSUCCESS) + return ter; + + // Transfer any XRP remaining after the fee is paid to the destination: + (*destSle)[sfBalance] = (*destSle)[sfBalance] + accountBalance; + (*srcSle)[sfBalance] = (*srcSle)[sfBalance] - accountBalance; + applyCtx.deliver(accountBalance); + + // DA: Pseudo accounts can have 0 balance, so we skip this assert. + // FIX FIX FIX: DA FIX + // XRPL_ASSERT( + // (*srcSle)[sfBalance] == XRPAmount(0), + // "xrpl::deleteDoApply : source balance is zero"); + + // If there's still an owner directory associated with the source account + // delete it. + if (view.exists(ownerDirKeylet) && !view.emptyDirDelete(ownerDirKeylet)) + { + JLOG(j.error()) << "DeleteAccount cannot delete root dir node of " << toBase58(account); + return tecHAS_OBLIGATIONS; + } + + // Re-arm the password change fee if we can and need to. + if (accountBalance > XRPAmount(0) && (*destSle).isFlag(lsfPasswordSpent)) + (*destSle).clearFlag(lsfPasswordSpent); + + view.update(destSle); + view.erase(srcSle); + + return tesSUCCESS; +} + +} // namespace xrpl diff --git a/src/libxrpl/tx/transactors/account/AccountDelete.cpp b/src/libxrpl/tx/transactors/account/AccountDelete.cpp index 8c2f23b754..080c6d0934 100644 --- a/src/libxrpl/tx/transactors/account/AccountDelete.cpp +++ b/src/libxrpl/tx/transactors/account/AccountDelete.cpp @@ -3,6 +3,7 @@ #include #include #include +#include #include #include #include @@ -11,9 +12,9 @@ #include #include #include +#include #include #include -#include #include #include @@ -169,6 +170,18 @@ removeDelegateFromLedger( return DelegateSet::deleteDelegate(view, sleDel, account, j); } +TER +removeContractFromLedger( + ServiceRegistry&, + ApplyView& view, + AccountID const& account, + uint256 const& /*delIndex*/, + std::shared_ptr const& sleDel, + beast::Journal j) +{ + return ContractDelete::deleteContract(view, sleDel, account, j); +} + // Return nullptr if the LedgerEntryType represents an obligation that can't // be deleted. Otherwise return the pointer to the function that can delete // the non-obligation @@ -195,6 +208,8 @@ nonObligationDeleter(LedgerEntryType t) return removeCredentialFromLedger; case ltDELEGATE: return removeDelegateFromLedger; + case ltCONTRACT: + return removeContractFromLedger; default: return nullptr; } diff --git a/src/libxrpl/tx/transactors/contract/ContractCall.cpp b/src/libxrpl/tx/transactors/contract/ContractCall.cpp new file mode 100644 index 0000000000..fd1e415d14 --- /dev/null +++ b/src/libxrpl/tx/transactors/contract/ContractCall.cpp @@ -0,0 +1,329 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace xrpl { + +XRPAmount +ContractCall::calculateBaseFee(ReadView const& view, STTx const& tx) +{ + XRPAmount extraFee{0}; + if (auto const allowance = tx[~sfComputationAllowance]; allowance) + { + extraFee += (*allowance) * view.fees().gasPrice / MICRO_DROPS_PER_DROP; + } + return Transactor::calculateBaseFee(view, tx) + extraFee; +} + +NotTEC +ContractCall::preflight(PreflightContext const& ctx) +{ + auto const flags = ctx.tx.getFlags(); + if (flags & tfUniversalMask) + { + JLOG(ctx.j.trace()) << "ContractCreate: tfUniversalMask is not allowed."; + return temINVALID_FLAG; + } + + return tesSUCCESS; +} + +TER +ContractCall::preclaim(PreclaimContext const& ctx) +{ + AccountID const account = ctx.tx[sfAccount]; + auto const accountSle = ctx.view.read(keylet::account(account)); + if (!accountSle) + { + JLOG(ctx.j.trace()) << "ContractCall: Account does not exist."; + return tecNO_TARGET; + } + + // The ContractAccount doesn't exist or isn't a smart contract + // pseudo-account. + AccountID const contractAccount = ctx.tx[sfContractAccount]; + auto const caSle = ctx.view.read(keylet::account(contractAccount)); + if (!caSle) + { + JLOG(ctx.j.trace()) << "ContractCall: Contract Account does not exist."; + return tecNO_TARGET; + } + + // The function doesn't exist on the provided contract. + uint256 const contractID = caSle->getFieldH256(sfContractID); + auto const contractSle = ctx.view.read(keylet::contract(contractID)); + if (!contractSle) + { + JLOG(ctx.j.trace()) << "ContractCall: Contract does not exist."; + return tecNO_TARGET; + } + + if (!contractSle->at(sfContractHash)) + { + JLOG(ctx.j.trace()) << "ContractCall: Contract does not have a hash."; + return tecNO_TARGET; + } + + auto const contractSourceSle = + ctx.view.read(keylet::contractSource(contractSle->at(sfContractHash))); + if (!contractSourceSle) + { + JLOG(ctx.j.trace()) << "ContractCall: ContractSource does not exist."; + return tecNO_TARGET; + } + + if (!contractSourceSle->isFieldPresent(sfFunctions)) + { + JLOG(ctx.j.trace()) << "ContractCall: Contract does not have any functions defined."; + return temMALFORMED; + } + + auto const& functions = contractSourceSle->getFieldArray(sfFunctions); + auto const functionName = ctx.tx.getFieldVL(sfFunctionName); + std::string functionNameHexStr(functionName.begin(), functionName.end()); + auto it = std::find_if( + functions.begin(), functions.end(), [&functionNameHexStr](STObject const& func) { + auto const funcName = func.getFieldVL(sfFunctionName); + std::string functionNameDefHexStr(funcName.begin(), funcName.end()); + return functionNameDefHexStr == functionNameHexStr; + }); + + if (it == functions.end()) + { + JLOG(ctx.j.trace()) << "ContractCall: FunctionName: " << functionNameHexStr + << " does not exist in contract abi."; + return temMALFORMED; + } + + if (ctx.tx.isFieldPresent(sfParameters)) + { + STArray const& params = ctx.tx.getFieldArray(sfParameters); + if (auto ter = + contract::preclaimFlagParameters(ctx.view, account, contractAccount, params, ctx.j); + !isTesSuccess(ter)) + { + JLOG(ctx.j.trace()) << "ContractCreate: Failed to preclaim flag parameters."; + return ter; + } + } + + // The parameters don't match the function's ABI. + return tesSUCCESS; +} + +TER +ContractCall::doApply() +{ + AccountID const contractAccount = ctx_.tx[sfContractAccount]; + + auto const caSle = ctx_.view().read(keylet::account(contractAccount)); + if (!caSle) + { + JLOG(j_.trace()) << "ContractCall: ContractAccount does not exist."; + return tefINTERNAL; + } + + auto const accountSle = ctx_.view().read(keylet::account(account_)); + if (!accountSle) + { + JLOG(j_.trace()) << "ContractCall: Account does not exist."; + return tefINTERNAL; + } + + uint256 const contractID = caSle->getFieldH256(sfContractID); + Keylet const k = keylet::contract(contractID); + auto const contractSle = ctx_.view().read(k); + if (!contractSle) + { + JLOG(j_.trace()) << "ContractCall: Contract does not exist."; + return tefINTERNAL; + } + + uint256 const contractHash = contractSle->at(sfContractHash); + auto const contractSourceSle = + ctx_.view().read(keylet::contractSource(contractSle->at(sfContractHash))); + if (!contractSourceSle) + { + JLOG(j_.trace()) << "ContractCall: ContractSource does not exist."; + return tefINTERNAL; + } + + // // Handle the flags for the contract call. + if (ctx_.tx.isFieldPresent(sfParameters)) + { + STArray const& params = ctx_.tx.getFieldArray(sfParameters); + if (auto ter = contract::doApplyFlagParameters( + ctx_.view(), + ctx_.tx, + account_, + contractAccount, + params, + preFeeBalance_, + ctx_.journal); + !isTesSuccess(ter)) + { + JLOG(ctx_.journal.trace()) << "ContractCall: Failed to handle flag parameters."; + return ter; + } + } + + // WASM execution + auto const wasmStr = contractSourceSle->getFieldVL(sfContractCode); + std::vector wasm(wasmStr.begin(), wasmStr.end()); + auto const functionName = ctx_.tx.getFieldVL(sfFunctionName); + std::string funcName(functionName.begin(), functionName.end()); + + auto const contractFunctions = contractSle->isFieldPresent(sfFunctions) + ? contractSle->getFieldArray(sfFunctions) + : contractSourceSle->getFieldArray(sfFunctions); + std::optional function; + for (auto const& contractFunction : contractFunctions) + { + if (contractFunction.getFieldVL(sfFunctionName) == functionName) + function = contractFunction; + } + if (!function) + { + JLOG(j_.trace()) << "ContractCall: FunctionName does not exist in contract."; + return tefINTERNAL; + } + + // ContractCall Parameters + std::vector functionParameters; + if (ctx_.tx.isFieldPresent(sfParameters)) + { + STArray const& funcParams = ctx_.tx.getFieldArray(sfParameters); + functionParameters = getParameterValueVec(funcParams); + } + + // ContractSource/Contract Default Parameters + std::vector instanceParameters; + if (contractSle->isFieldPresent(sfInstanceParameterValues)) + { + STArray const& instParams = contractSle->getFieldArray(sfInstanceParameterValues); + instanceParameters = getParameterValueVec(instParams); + } + + // The parameters don't match the function's ABI. + std::vector typeVec; + if (function->isFieldPresent(sfParameters)) + { + STArray const& funcParamsDef = function->getFieldArray(sfParameters); + typeVec = xrpl::getParameterTypeVec(funcParamsDef); + if (functionParameters.size() != typeVec.size()) + return tecINVALID_PARAMETERS; + } + + for (std::size_t i = 0; i < functionParameters.size(); i++) + { + if (functionParameters[i].value.getInnerSType() != typeVec[i].type.getInnerSType()) + return tecINVALID_PARAMETERS; + } + + xrpl::ContractDataMap dataMap; + xrpl::ContractEventMap eventMap; + ContractContext contractCtx = { + .applyCtx = ctx_, + .instanceParameters = instanceParameters, + .functionParameters = functionParameters, + .built_txns = {}, + .expected_etxn_count = 1, + .generation = 0, + .burden = 0, + .result = + { + .contractHash = contractHash, + .contractKeylet = k, + .contractSourceKeylet = k, + .contractAccountKeylet = k, + .contractAccount = contractAccount, + .nextSequence = caSle->getFieldU32(sfSequence), + .otxnAccount = account_, + .otxnId = ctx_.tx.getTransactionID(), + .exitReason = "", + .exitCode = -1, + .dataMap = dataMap, + .eventMap = eventMap, + .changedDataCount = 0, + }, + }; + + ContractHostFunctionsImpl ledgerDataProvider(contractCtx); + + if (!ctx_.tx.isFieldPresent(sfComputationAllowance)) + { + JLOG(j_.trace()) << "ContractCall: Computation allowance is not set."; + return tefINTERNAL; + } + + std::uint32_t allowance = ctx_.tx[sfComputationAllowance]; + auto re = runEscrowWasm(wasm, ledgerDataProvider, allowance, funcName, {}); + + // Wasm Result + if (re.has_value()) + { + // TODO: better error handling for this conversion + // if (allowance > re.value().cost) + // { + // allowance -= static_cast(re.value().cost); + // // auto const returnAllowance = [&]() { + // // ctx_.view().update( + // // keylet::account(contractAccount), + // // [allowance](SLE& sle) { + // // sle.setFieldU32( + // // sfBalance, + // // sle.getFieldU32(sfBalance) + allowance); + // // }); + // // }; + // // returnAllowance(); + // } + + ctx_.setGasUsed(static_cast(re.value().cost)); + auto ret = re.value().result; + if (ret < 0) + { + JLOG(j_.trace()) << "WASM Execution Failed: " << contractCtx.result.exitReason; + ctx_.setWasmReturnCode(ret); + // ctx_.setWasmReturnStr(contractCtx.result.exitReason); + return tecWASM_REJECTED; + } + + if (auto res = contract::finalizeContractData( + ctx_.registry, + ctx_.view(), + contractAccount, + contractCtx.result.dataMap, + contractCtx.result.eventMap, + ctx_.tx.getTransactionID()); + !isTesSuccess(res)) + { + JLOG(j_.trace()) << "Contract data finalization failed: " << transHuman(res); + return res; + } + + ctx_.setWasmReturnCode(ret); + // ctx_.setWasmReturnStr(contractCtx.result.exitReason); + ctx_.setEmittedTxns(contractCtx.result.emittedTxns); + return tesSUCCESS; + } + else + { + JLOG(j_.trace()) << "WASM Failure: " + transHuman(re.error()); + auto const errorCode = TERtoInt(re.error()); + ctx_.setWasmReturnCode(errorCode); + // ctx_.setWasmReturnStr(contractCtx.result.exitReason); + return re.error(); + } + return tesSUCCESS; +} + +} // namespace xrpl diff --git a/src/libxrpl/tx/transactors/contract/ContractClawback.cpp b/src/libxrpl/tx/transactors/contract/ContractClawback.cpp new file mode 100644 index 0000000000..1712d07c8c --- /dev/null +++ b/src/libxrpl/tx/transactors/contract/ContractClawback.cpp @@ -0,0 +1,32 @@ +#include +#include +#include + +namespace xrpl { + +NotTEC +ContractClawback::preflight(PreflightContext const& ctx) +{ + auto const flags = ctx.tx.getFlags(); + if (flags & tfUniversalMask) + { + JLOG(ctx.j.trace()) << "ContractClawback: tfUniversalMask is not allowed."; + return temINVALID_FLAG; + } + + return tesSUCCESS; +} + +TER +ContractClawback::preclaim(PreclaimContext const& ctx) +{ + return tesSUCCESS; +} + +TER +ContractClawback::doApply() +{ + return tesSUCCESS; +} + +} // namespace xrpl diff --git a/src/libxrpl/tx/transactors/contract/ContractCreate.cpp b/src/libxrpl/tx/transactors/contract/ContractCreate.cpp new file mode 100644 index 0000000000..2b91baf403 --- /dev/null +++ b/src/libxrpl/tx/transactors/contract/ContractCreate.cpp @@ -0,0 +1,272 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace xrpl { + +XRPAmount +ContractCreate::calculateBaseFee(ReadView const& view, STTx const& tx) +{ + XRPAmount const maxAmount{std::numeric_limits::max()}; + XRPAmount createFee{0}; + + if (tx.isFieldPresent(sfCreateCode)) + createFee = XRPAmount{contract::contractCreateFee(tx.getFieldVL(sfCreateCode).size())}; + + if (createFee > maxAmount - view.fees().increment) + { + JLOG(debugLog().trace()) << "ContractCreate: Create fee overflow detected."; + return XRPAmount{INITIAL_XRP}; + } + + createFee += view.fees().increment; + + auto baseFee = Transactor::calculateBaseFee(view, tx); + if (baseFee > maxAmount - createFee) + { + JLOG(debugLog().trace()) << "ContractCreate: Total fee overflow detected."; + return XRPAmount{INITIAL_XRP}; + } + + return createFee + baseFee; +} + +std::uint32_t +ContractCreate::getFlagsMask(PreflightContext const& ctx) +{ + return tfContractMask; +} + +NotTEC +ContractCreate::preflight(PreflightContext const& ctx) +{ + auto const flags = ctx.tx.getFlags(); + + if ((flags & (tfCodeImmutable | tfABIImmutable | tfImmutable)) > tfImmutable) + { + JLOG(ctx.j.trace()) << "ContractCreate: Cannot set more than one immutability flag."; + return temINVALID_FLAG; + } + + if (!ctx.tx.isFieldPresent(sfContractCode) && !ctx.tx.isFieldPresent(sfContractHash)) + { + JLOG(ctx.j.trace()) << "ContractCreate: Neither ContractCode nor ContractHash present"; + return temMALFORMED; + } + + if (ctx.tx.isFieldPresent(sfContractCode) && ctx.tx.isFieldPresent(sfContractHash)) + { + JLOG(ctx.j.trace()) << "ContractCreate: Both ContractCode and ContractHash present"; + return temMALFORMED; + } + + if (auto const res = contract::preflightFunctions(ctx.tx, ctx.j); !isTesSuccess(res)) + { + JLOG(ctx.j.trace()) << "ContractCreate: Functions validation failed: " << transToken(res); + return res; + } + + if (auto const res = contract::preflightInstanceParameters(ctx.tx, ctx.j); !isTesSuccess(res)) + { + JLOG(ctx.j.trace()) << "ContractCreate: InstanceParameters validation failed: " + << transToken(res); + return res; + } + + if (auto const res = contract::preflightInstanceParameterValues(ctx.tx, ctx.j); + !isTesSuccess(res)) + { + JLOG(ctx.j.trace()) << "ContractCreate: InstanceParameterValues validation failed: " + << transToken(res); + return res; + } + + return tesSUCCESS; +} + +TER +ContractCreate::preclaim(PreclaimContext const& ctx) +{ + // ContractHash is provided but there is no existing corresponding + // ContractSource ledger object + bool isInstall = ctx.tx.isFieldPresent(sfContractHash); + auto contractHash = ctx.tx.at(~sfContractHash); + if (isInstall && !ctx.view.exists(keylet::contractSource(*contractHash))) + { + JLOG(ctx.j.trace()) << "ContractCreate: ContractHash provided but no " + "corresponding ContractSource exists"; + return temMALFORMED; + } + + // The ContractCode provided is invalid. + if (ctx.tx.isFieldPresent(sfContractCode)) + { + xrpl::Blob wasmBytes = ctx.tx.getFieldVL(sfContractCode); + if (wasmBytes.empty()) + { + JLOG(ctx.j.trace()) << "ContractCreate: ContractCode provided is empty."; + return temMALFORMED; + } + + contractHash = xrpl::sha512Half_s(xrpl::Slice(wasmBytes.data(), wasmBytes.size())); + if (ctx.view.exists(keylet::contractSource(*contractHash))) + isInstall = true; + + // Iterate through the functions and validate them? + // HostFunctions mock; + // auto const re = preflightEscrowWasm(wasmBytes, "finish", {}, &mock, + // ctx.j); if (!isTesSuccess(re)) + // { + // JLOG(ctx.j.debug()) << "EscrowCreate.FinishFunction bad WASM"; + // return re; + // } + } + + // The ABI provided in Functions doesn't match the code. + + // InstanceParameters don't match what's in the existing ContractSource + // ledger object. + if (isInstall && ctx.tx.isFieldPresent(sfInstanceParameterValues)) + { + auto const sle = ctx.view.read(keylet::contractSource(*contractHash)); + if (!sle) + return tefINTERNAL; // LCOV_EXCL_LINE + + // Already validated in preflight, but we can check here too. + auto const& instanceParams = sle->getFieldArray(sfInstanceParameters); + auto const& instanceParamValues = ctx.tx.getFieldArray(sfInstanceParameterValues); + if (auto const isValid = + contract::validateParameterMapping(instanceParams, instanceParamValues, ctx.j); + !isValid) + { + JLOG(ctx.j.trace()) << "ContractCreate: InstanceParameters do not match what's in " + "the existing ContractSource ledger object."; + return temMALFORMED; + } + } + + return tesSUCCESS; +} + +TER +ContractCreate::doApply() +{ + auto const accountSle = ctx_.view().peek(keylet::account(account_)); + if (!accountSle) + { + JLOG(j_.trace()) << "ContractCreate: Account not found."; + return tefINTERNAL; // LCOV_EXCL_LINE + } + + std::shared_ptr sourceSle; + bool isInstall = ctx_.tx.isFieldPresent(sfContractHash); + auto contractHash = ctx_.tx[~sfContractHash]; + xrpl::Blob wasmBytes; + if (ctx_.tx.isFieldPresent(sfContractCode)) + { + wasmBytes = ctx_.tx.getFieldVL(sfContractCode); + contractHash = xrpl::sha512Half_s(xrpl::Slice(wasmBytes.data(), wasmBytes.size())); + if (ctx_.view().exists(keylet::contractSource(*contractHash))) + isInstall = true; + } + + if (isInstall) + { + sourceSle = ctx_.view().peek(keylet::contractSource(*contractHash)); + if (!sourceSle) + return tefINTERNAL; // LCOV_EXCL_LINE + + sourceSle->at(sfReferenceCount) = sourceSle->getFieldU64(sfReferenceCount) + 1; + ctx_.view().update(sourceSle); + } + else + { + sourceSle = std::make_shared(keylet::contractSource(*contractHash)); + sourceSle->at(sfContractHash) = *contractHash; + sourceSle->at(sfContractCode) = makeSlice(wasmBytes); + sourceSle->setFieldArray(sfFunctions, ctx_.tx.getFieldArray(sfFunctions)); + if (ctx_.tx.isFieldPresent(sfInstanceParameters)) + sourceSle->setFieldArray( + sfInstanceParameters, ctx_.tx.getFieldArray(sfInstanceParameters)); + sourceSle->at(sfReferenceCount) = 1; + + ctx_.view().insert(sourceSle); + } + + std::uint32_t const seq = ctx_.tx.getSeqValue(); + auto const contractKeylet = keylet::contract(*contractHash, account_, seq); + auto contractSle = std::make_shared(contractKeylet); + + auto maybePseudo = createPseudoAccount(view(), contractSle->key(), sfContractID); + if (!maybePseudo) + return maybePseudo.error(); // LCOV_EXCL_LINE + + auto& pseudoSle = *maybePseudo; + auto pseudoAccount = pseudoSle->at(sfAccount); + + contractSle->at(sfContractAccount) = pseudoAccount; + contractSle->at(sfOwner) = account_; + contractSle->at(sfFlags) = ctx_.tx.getFlags(); + contractSle->at(sfSequence) = seq; + contractSle->at(sfContractHash) = *contractHash; + if (ctx_.tx.isFieldPresent(sfInstanceParameterValues)) + contractSle->setFieldArray( + sfInstanceParameterValues, ctx_.tx.getFieldArray(sfInstanceParameterValues)); + + if (ctx_.tx.isFieldPresent(sfURI)) + contractSle->setFieldVL(sfURI, ctx_.tx.getFieldVL(sfURI)); + + ctx_.view().insert(contractSle); + + // Handle the instance parameters for the contract creation. + if (ctx_.tx.isFieldPresent(sfInstanceParameterValues)) + { + STArray const& params = ctx_.tx.getFieldArray(sfInstanceParameterValues); + + // Note: We have to do preclaim and apply here because we will only have + // the pseudo account after the contract is created. + if (auto ter = + contract::preclaimFlagParameters(ctx_.view(), account_, pseudoAccount, params, j_); + !isTesSuccess(ter)) + { + JLOG(j_.trace()) << "ContractCreate: Failed to preclaim flag parameters."; + return ter; + } + + if (auto ter = contract::doApplyFlagParameters( + ctx_.view(), ctx_.tx, account_, pseudoAccount, params, preFeeBalance_, j_); + !isTesSuccess(ter)) + { + JLOG(j_.trace()) << "ContractCreate: Failed to apply flag parameters."; + return ter; + } + } + + // Add Contract to ContractAccount Dir + // TODO: use dirLink + { + auto const page = view().dirInsert( + keylet::ownerDir(pseudoAccount), contractKeylet, describeOwnerDir(pseudoAccount)); + + if (!page) + return tecDIR_FULL; + + contractSle->setFieldU64(sfOwnerNode, *page); + adjustOwnerCount(ctx_.view(), pseudoSle, 1, j_); + } + + return tesSUCCESS; +} + +} // namespace xrpl diff --git a/src/libxrpl/tx/transactors/contract/ContractDelete.cpp b/src/libxrpl/tx/transactors/contract/ContractDelete.cpp new file mode 100644 index 0000000000..6b91cd7ca5 --- /dev/null +++ b/src/libxrpl/tx/transactors/contract/ContractDelete.cpp @@ -0,0 +1,149 @@ +#include +#include +#include +#include +#include + +namespace xrpl { + +NotTEC +ContractDelete::preflight(PreflightContext const& ctx) +{ + auto const flags = ctx.tx.getFlags(); + if (flags & tfUniversalMask) + { + JLOG(ctx.j.trace()) << "ContractDelete: only tfUniversalMask is allowed."; + return temINVALID_FLAG; + } + + return tesSUCCESS; +} + +TER +ContractDelete::preclaim(PreclaimContext const& ctx) +{ + AccountID const account = ctx.tx.getAccountID(sfAccount); + AccountID const contractAccount = + ctx.tx.isFieldPresent(sfContractAccount) ? ctx.tx.getAccountID(sfContractAccount) : account; + + auto const caSle = ctx.view.read(keylet::account(contractAccount)); + if (!caSle) + { + JLOG(ctx.j.trace()) << "ContractDelete: Account does not exist."; + return terNO_ACCOUNT; + } + + if (!caSle->isFieldPresent(sfContractID)) + { + JLOG(ctx.j.trace()) << "ContractDelete: Account is not a smart " + "contract pseudo-account."; + return tecNO_PERMISSION; + } + + uint256 const contractID = caSle->getFieldH256(sfContractID); + auto const contractSle = ctx.view.read(keylet::contract(contractID)); + if (!contractSle) + { + JLOG(ctx.j.trace()) << "ContractDelete: Contract does not exist."; + return tecNO_TARGET; + } + + if (contractSle->getAccountID(sfOwner) != account) + { + JLOG(ctx.j.trace()) << "ContractDelete: Cannot delete a contract that " + "does not belong to the account."; + return tecNO_PERMISSION; + } + + std::uint32_t flags = contractSle->getFlags(); + + // Check if the contract is undeletable. + if (flags & tfUndeletable) + { + JLOG(ctx.j.trace()) << "ContractDelete: Contract is undeletable."; + return tecNO_PERMISSION; + } + + AccountID const owner = contractSle->getAccountID(sfOwner); + if (auto const res = deletePreclaim(ctx, 0, account, owner, true); !isTesSuccess(res)) + return res; + return tesSUCCESS; +} + +TER +ContractDelete::deleteContract( + ApplyView& view, + std::shared_ptr const& sle, + AccountID const& account, + beast::Journal j) +{ + if (!sle) + return tecINTERNAL; // LCOV_EXCL_LINE + + if (!view.dirRemove(keylet::ownerDir(account), (*sle)[sfOwnerNode], sle->key(), false)) + { + // LCOV_EXCL_START + JLOG(j.trace()) << "Unable to delete Delegate from owner."; + return tefBAD_LEDGER; + // LCOV_EXCL_STOP + } + + auto const sleOwner = view.peek(keylet::account(account)); + if (!sleOwner) + return tecINTERNAL; // LCOV_EXCL_LINE + + adjustOwnerCount(view, sleOwner, -1, j); + view.erase(sle); + return tesSUCCESS; +} + +TER +ContractDelete::doApply() +{ + AccountID const account = ctx_.tx.getAccountID(sfAccount); + AccountID const contractAccount = ctx_.tx.isFieldPresent(sfContractAccount) + ? ctx_.tx.getAccountID(sfContractAccount) + : account; + + auto const caSle = ctx_.view().read(keylet::account(contractAccount)); + if (!caSle) + { + JLOG(j_.trace()) << "ContractModify: Account does not exist."; + return tefBAD_LEDGER; + } + + uint256 const contractID = caSle->getFieldH256(sfContractID); + auto const contractSle = ctx_.view().read(keylet::contract(contractID)); + if (!contractSle) + { + JLOG(j_.trace()) << "ContractDelete: Contract does not exist."; + return tecNO_TARGET; + } + + // Lower the reference count of the ContractSource or remove the source from + // the ledger. + uint256 const contractHash = contractSle->getFieldH256(sfContractHash); + auto oldSourceSle = ctx_.view().peek(keylet::contractSource(contractHash)); + if (oldSourceSle->getFieldU64(sfReferenceCount) == 1) + { + // NOTE: We dont adjust the owner count because ContractSource is an + // unowned object. + ctx_.view().erase(oldSourceSle); + } + else + { + oldSourceSle->setFieldU64( + sfReferenceCount, oldSourceSle->getFieldU64(sfReferenceCount) - 1); + ctx_.view().update(oldSourceSle); + } + + AccountID const owner = contractSle->getAccountID(sfOwner); + STAmount const contractBalance = (*caSle)[sfBalance]; + if (auto const res = deleteDoApply(ctx_, contractBalance, contractAccount, owner); + !isTesSuccess(res)) + return res; + + return tesSUCCESS; +} + +} // namespace xrpl diff --git a/src/libxrpl/tx/transactors/contract/ContractModify.cpp b/src/libxrpl/tx/transactors/contract/ContractModify.cpp new file mode 100644 index 0000000000..e84e121cb4 --- /dev/null +++ b/src/libxrpl/tx/transactors/contract/ContractModify.cpp @@ -0,0 +1,352 @@ +#include +#include +#include +#include +#include +#include + +namespace xrpl { + +XRPAmount +ContractModify::calculateBaseFee(ReadView const& view, STTx const& tx) +{ + XRPAmount const maxAmount{std::numeric_limits::max()}; + XRPAmount createFee{0}; + + if (tx.isFieldPresent(sfCreateCode)) + createFee = XRPAmount{contract::contractCreateFee(tx.getFieldVL(sfCreateCode).size())}; + + if (createFee > maxAmount - view.fees().increment) + { + JLOG(debugLog().trace()) << "ContractModify: Create fee overflow detected."; + return XRPAmount{INITIAL_XRP}; + } + + auto baseFee = Transactor::calculateBaseFee(view, tx); + if (baseFee > maxAmount - createFee) + { + JLOG(debugLog().trace()) << "ContractModify: Total fee overflow detected."; + return XRPAmount{INITIAL_XRP}; + } + + return createFee + baseFee; +} + +NotTEC +ContractModify::preflight(PreflightContext const& ctx) +{ + auto const flags = ctx.tx.getFlags(); + if (flags & tfUniversalMask) + { + JLOG(ctx.j.trace()) << "ContractModify: only tfUniversalMask is allowed."; + return temINVALID_FLAG; + } + + // Either ContractCode or ContractHash must be present. + if (ctx.tx.isFieldPresent(sfContractCode) && ctx.tx.isFieldPresent(sfContractHash)) + { + JLOG(ctx.j.trace()) << "ContractModify: Both ContractCode and ContractHash present"; + return temMALFORMED; + } + + // Validate Functions & Function Parameters. + if (auto const res = contract::preflightFunctions(ctx.tx, ctx.j); !isTesSuccess(res)) + return res; + + // Validate Instance Parameters. + if (auto const res = contract::preflightInstanceParameters(ctx.tx, ctx.j); !isTesSuccess(res)) + return res; + + // Validate Instance Parameter Values. + if (auto const res = contract::preflightInstanceParameterValues(ctx.tx, ctx.j); + !isTesSuccess(res)) + return res; + + if (ctx.tx.isFieldPresent(sfOwner)) + { + if (ctx.tx.getAccountID(sfOwner) == ctx.tx.getAccountID(sfAccount)) + return temMALFORMED; + + if (ctx.tx.getAccountID(sfOwner) == ctx.tx.getAccountID(sfContractAccount)) + return temMALFORMED; + } + + return tesSUCCESS; +} + +TER +ContractModify::preclaim(PreclaimContext const& ctx) +{ + AccountID const account = ctx.tx.getAccountID(sfAccount); + AccountID const contractAccount = + ctx.tx.isFieldPresent(sfContractAccount) ? ctx.tx.getAccountID(sfContractAccount) : account; + + auto const contractAccountSle = ctx.view.read(keylet::account(contractAccount)); + if (!contractAccountSle) + { + JLOG(ctx.j.trace()) << "ContractModify: Contract Account does not exist."; + return tecNO_TARGET; + } + + uint256 const contractID = contractAccountSle->getFieldH256(sfContractID); + auto const contractSle = ctx.view.read(keylet::contract(contractID)); + if (!contractSle) + { + JLOG(ctx.j.trace()) << "ContractModify: Contract does not exist."; + return tecNO_TARGET; + } + + if (ctx.tx.isFieldPresent(sfContractAccount) && contractSle->getAccountID(sfOwner) != account) + { + JLOG(ctx.j.trace()) << "ContractModify: Cannot modify a contract that " + "does not belong to the account."; + return tecNO_PERMISSION; + } + + std::uint32_t flags = contractSle->getFlags(); + + // Check if the contract is immutable. + if (flags & tfImmutable) + { + JLOG(ctx.j.trace()) << "ContractModify: Contract is immutable."; + return tecNO_PERMISSION; + } + + // Check if the contract code is immutable. + if (flags & tfCodeImmutable && ctx.tx.isFieldPresent(sfContractCode)) + { + JLOG(ctx.j.trace()) << "ContractModify: ContractCode is immutable."; + return tecNO_PERMISSION; + } + + // Check if the contract ABI is immutable. + if (flags & tfABIImmutable) + { + if (!ctx.tx.isFieldPresent(sfContractCode)) + { + JLOG(ctx.j.trace()) << "ContractModify: ContractCode must be " + "present when modifying ABI."; + return tecNO_PERMISSION; + } + + if (!ctx.tx.isFieldPresent(sfFunctions)) + { + JLOG(ctx.j.trace()) << "ContractModify: Functions must be present " + "when modifying ABI."; + return tecNO_PERMISSION; + } + + JLOG(ctx.j.trace()) << "ContractModify: ABI is immutable."; + return tecNO_PERMISSION; + } + + // Can only include 1 of the 3 flags: tfCodeImmutable, tfABIImmutable, + // tfImmutable. + if ((flags & (tfCodeImmutable | tfABIImmutable | tfImmutable)) > tfImmutable) + { + JLOG(ctx.j.trace()) << "ContractModify: Cannot set more than one immutability flag."; + return temINVALID_FLAG; + } + + bool isInstall = ctx.tx.isFieldPresent(sfContractHash); + auto contractHash = ctx.tx.at(~sfContractHash); + if (ctx.tx.isFieldPresent(sfContractCode)) + { + xrpl::Blob wasmBytes = ctx.tx.getFieldVL(sfContractCode); + if (wasmBytes.empty()) + { + JLOG(ctx.j.trace()) << "ContractModify: ContractCode provided is empty."; + return temMALFORMED; + } + + contractHash = xrpl::sha512Half_s(xrpl::Slice(wasmBytes.data(), wasmBytes.size())); + if (ctx.view.exists(keylet::contractSource(*contractHash))) + isInstall = true; + + // Iterate through the functions and validate them? + // HostFunctions mock; + // auto const re = preflightEscrowWasm(wasmBytes, "finish", {}, &mock, + // ctx.j); if (!isTesSuccess(re)) + // { + // JLOG(ctx.j.debug()) << "EscrowCreate.FinishFunction bad WASM"; + // return re; + // } + } + + // The ABI provided in Functions doesn't match the code. + + if (isInstall) + { + auto const sle = ctx.view.read(keylet::contractSource(*contractHash)); + if (!sle) + { + JLOG(ctx.j.trace()) << "ContractModify: ContractSource ledger object not found for " + "the provided ContractHash."; + return tefINTERNAL; // LCOV_EXCL_LINE + } + + if (sle->isFieldPresent(sfInstanceParameters) && + !ctx.tx.isFieldPresent(sfInstanceParameterValues)) + { + JLOG(ctx.j.trace()) << "ContractModify: ContractHash is present, but " + "InstanceParameterValues is missing."; + return temMALFORMED; + } + + auto const& instanceParams = sle->getFieldArray(sfInstanceParameters); + auto const& instanceParamValues = ctx.tx.getFieldArray(sfInstanceParameterValues); + if (auto const isValid = + contract::validateParameterMapping(instanceParams, instanceParamValues, ctx.j); + !isValid) + { + JLOG(ctx.j.trace()) << "ContractModify: InstanceParameters do not match what's in " + "the existing ContractSource ledger object."; + return temMALFORMED; + } + } + + if (ctx.tx.isFieldPresent(sfOwner)) + { + auto const ownerSle = ctx.view.read(keylet::account(ctx.tx.getAccountID(sfOwner))); + if (!ownerSle) + { + JLOG(ctx.j.trace()) << "ContractModify: New owner account does not exist."; + return tecNO_TARGET; + } + } + + return tesSUCCESS; +} + +TER +ContractModify::doApply() +{ + AccountID const account = ctx_.tx.getAccountID(sfAccount); + AccountID const contractAccount = ctx_.tx.isFieldPresent(sfContractAccount) + ? ctx_.tx.getAccountID(sfContractAccount) + : account; + + auto const contractAccountSle = ctx_.view().read(keylet::account(contractAccount)); + if (!contractAccountSle) + { + JLOG(ctx_.journal.trace()) << "ContractModify: Account does not exist."; + return tefINTERNAL; + } + + uint256 const contractID = contractAccountSle->getFieldH256(sfContractID); + auto const contractSle = ctx_.view().peek(keylet::contract(contractID)); + if (!contractSle) + { + JLOG(ctx_.journal.trace()) << "ContractModify: Contract does not exist."; + return tefINTERNAL; + } + + auto currentSourceSle = + ctx_.view().peek(keylet::contractSource(contractSle->getFieldH256(sfContractHash))); + if (!currentSourceSle) + { + JLOG(ctx_.journal.trace()) << "ContractModify: ContractSource does not exist."; + return tefINTERNAL; + } + + if (ctx_.tx.isFieldPresent(sfContractCode)) + { + JLOG(ctx_.journal.trace()) << "ContractModify: Modifying ContractCode/ContractHash."; + xrpl::Blob wasmBytes = ctx_.tx.getFieldVL(sfContractCode); + auto const contractHash = + xrpl::sha512Half_s(xrpl::Slice(wasmBytes.data(), wasmBytes.size())); + auto const sourceKeylet = keylet::contractSource(contractHash); + auto sourceSle = ctx_.view().peek(sourceKeylet); + if (!sourceSle) + { + JLOG(ctx_.journal.trace()) << "ContractModify: Creating new ContractSource."; + // create the new ContractSource + sourceSle = std::make_shared(sourceKeylet); + sourceSle->at(sfContractHash) = contractHash; + sourceSle->at(sfContractCode) = makeSlice(wasmBytes); + sourceSle->setFieldArray(sfFunctions, ctx_.tx.getFieldArray(sfFunctions)); + if (ctx_.tx.isFieldPresent(sfInstanceParameters)) + sourceSle->setFieldArray( + sfInstanceParameters, ctx_.tx.getFieldArray(sfInstanceParameters)); + sourceSle->at(sfReferenceCount) = 1; + ctx_.view().insert(sourceSle); + } + + // update the Contract + contractSle->setFieldH256(sfContractHash, contractHash); + if (ctx_.tx.isFieldPresent(sfInstanceParameterValues)) + contractSle->setFieldArray( + sfInstanceParameterValues, ctx_.tx.getFieldArray(sfInstanceParameterValues)); + + ctx_.view().update(contractSle); + + // update the existing ContractSource + if (currentSourceSle->getFieldU64(sfReferenceCount) == 1) + { + // remove the old ContractSource if no more references + ctx_.view().erase(currentSourceSle); + } + else + { + // decrement the reference count + currentSourceSle->setFieldU64( + sfReferenceCount, currentSourceSle->getFieldU64(sfReferenceCount) - 1); + ctx_.view().update(currentSourceSle); + } + } + else if (ctx_.tx.isFieldPresent(sfContractHash)) + { + auto sourceSle = + ctx_.view().peek(keylet::contractSource(ctx_.tx.getFieldH256(sfContractHash))); + if (!sourceSle) + { + JLOG(ctx_.journal.trace()) << "ContractModify: ContractSource does not exist."; + return tefINTERNAL; + } + + // set new contract hash + contractSle->setFieldH256(sfContractHash, ctx_.tx.getFieldH256(sfContractHash)); + + // set new instance parameter values if present + if (ctx_.tx.isFieldPresent(sfInstanceParameterValues)) + contractSle->setFieldArray( + sfInstanceParameterValues, ctx_.tx.getFieldArray(sfInstanceParameterValues)); + + ctx_.view().update(contractSle); + + sourceSle->setFieldU64(sfReferenceCount, sourceSle->getFieldU64(sfReferenceCount) + 1); + ctx_.view().update(sourceSle); + + // update the existing ContractSource + if (currentSourceSle->getFieldU64(sfReferenceCount) == 1) + { + // remove the old ContractSource if no more references + ctx_.view().erase(currentSourceSle); + } + else + { + // decrement the reference count + currentSourceSle->setFieldU64( + sfReferenceCount, currentSourceSle->getFieldU64(sfReferenceCount) - 1); + ctx_.view().update(currentSourceSle); + } + } + else if (ctx_.tx.isFieldPresent(sfInstanceParameterValues)) + { + // only updating instance parameter values + contractSle->setFieldArray( + sfInstanceParameterValues, ctx_.tx.getFieldArray(sfInstanceParameterValues)); + + ctx_.view().update(contractSle); + } + + if (ctx_.tx.isFieldPresent(sfOwner)) + { + contractSle->setAccountID(sfOwner, ctx_.tx.getAccountID(sfOwner)); + ctx_.view().update(contractSle); + } + + return tesSUCCESS; +} + +} // namespace xrpl diff --git a/src/libxrpl/tx/transactors/contract/ContractUserDelete.cpp b/src/libxrpl/tx/transactors/contract/ContractUserDelete.cpp new file mode 100644 index 0000000000..22181c02d6 --- /dev/null +++ b/src/libxrpl/tx/transactors/contract/ContractUserDelete.cpp @@ -0,0 +1,32 @@ +#include +#include +#include + +namespace xrpl { + +NotTEC +ContractUserDelete::preflight(PreflightContext const& ctx) +{ + auto const flags = ctx.tx.getFlags(); + if (flags & tfUniversalMask) + { + JLOG(ctx.j.trace()) << "ContractUserDelete: tfUniversalMask is not allowed."; + return temINVALID_FLAG; + } + + return tesSUCCESS; +} + +TER +ContractUserDelete::preclaim(PreclaimContext const& ctx) +{ + return tesSUCCESS; +} + +TER +ContractUserDelete::doApply() +{ + return tesSUCCESS; +} + +} // namespace xrpl diff --git a/src/libxrpl/tx/transactors/escrow/EscrowHelpers.h b/src/libxrpl/tx/transactors/escrow/EscrowHelpers.h index da6867e06a..df4b7c651e 100644 --- a/src/libxrpl/tx/transactors/escrow/EscrowHelpers.h +++ b/src/libxrpl/tx/transactors/escrow/EscrowHelpers.h @@ -227,8 +227,11 @@ escrowUnlockApplyHelper( journal); } +// calculateAdditionalReserve computes the owner count impact of an Escrow. +// An escrow without a FinishFunction costs 1 reserve. With a FinishFunction, +// each additional 500 bytes beyond the first 500 adds another reserve slot. template -static uint32_t +inline uint32_t calculateAdditionalReserve(T const& finishFunction) { if (!finishFunction) diff --git a/src/libxrpl/tx/transactors/nft/NFTokenAcceptOffer.cpp b/src/libxrpl/tx/transactors/nft/NFTokenAcceptOffer.cpp index e061dbe7ec..6e6ae925e0 100644 --- a/src/libxrpl/tx/transactors/nft/NFTokenAcceptOffer.cpp +++ b/src/libxrpl/tx/transactors/nft/NFTokenAcceptOffer.cpp @@ -1,10 +1,10 @@ #include +#include #include #include #include #include #include -#include namespace xrpl { @@ -415,7 +415,7 @@ NFTokenAcceptOffer::acceptOffer(std::shared_ptr const& offer) } // Now transfer the NFT: - return transferNFToken(buyer, seller, nftokenID); + return nft::transferNFToken(ctx_.view(), buyer, seller, nftokenID); } TER @@ -538,7 +538,7 @@ NFTokenAcceptOffer::doApply() } // Now transfer the NFT: - return transferNFToken(buyer, seller, nftokenID); + return nft::transferNFToken(ctx_.view(), buyer, seller, nftokenID); } if (bo) diff --git a/src/libxrpl/tx/transactors/nft/NFTokenBurn.cpp b/src/libxrpl/tx/transactors/nft/NFTokenBurn.cpp index 5a31e35470..9f425380d8 100644 --- a/src/libxrpl/tx/transactors/nft/NFTokenBurn.cpp +++ b/src/libxrpl/tx/transactors/nft/NFTokenBurn.cpp @@ -1,8 +1,8 @@ +#include #include #include #include #include -#include namespace xrpl { diff --git a/src/libxrpl/tx/transactors/nft/NFTokenCancelOffer.cpp b/src/libxrpl/tx/transactors/nft/NFTokenCancelOffer.cpp index df0561e076..2d6c59df4a 100644 --- a/src/libxrpl/tx/transactors/nft/NFTokenCancelOffer.cpp +++ b/src/libxrpl/tx/transactors/nft/NFTokenCancelOffer.cpp @@ -1,8 +1,8 @@ #include +#include #include #include #include -#include #include diff --git a/src/libxrpl/tx/transactors/nft/NFTokenCreateOffer.cpp b/src/libxrpl/tx/transactors/nft/NFTokenCreateOffer.cpp index f5fdc89550..adc92fa11f 100644 --- a/src/libxrpl/tx/transactors/nft/NFTokenCreateOffer.cpp +++ b/src/libxrpl/tx/transactors/nft/NFTokenCreateOffer.cpp @@ -1,8 +1,8 @@ #include +#include #include #include #include -#include namespace xrpl { @@ -38,6 +38,10 @@ NFTokenCreateOffer::preflight(PreflightContext const& ctx) TER NFTokenCreateOffer::preclaim(PreclaimContext const& ctx) { + auto const sle = ctx.view.read(keylet::account(ctx.tx[sfAccount])); + auto const balance = sle ? (*sle)[sfBalance] : XRPAmount{0}; + JLOG(ctx.j.error()) << "NFTokenCreateOffer::preclaim.Balance: " << balance; + if (hasExpired(ctx.view, ctx.tx[~sfExpiration])) return tecEXPIRED; diff --git a/src/libxrpl/tx/transactors/nft/NFTokenModify.cpp b/src/libxrpl/tx/transactors/nft/NFTokenModify.cpp index 8ccd4e9552..250f9c64d1 100644 --- a/src/libxrpl/tx/transactors/nft/NFTokenModify.cpp +++ b/src/libxrpl/tx/transactors/nft/NFTokenModify.cpp @@ -1,7 +1,7 @@ +#include #include #include #include -#include namespace xrpl { diff --git a/src/libxrpl/tx/transactors/system/LedgerStateFix.cpp b/src/libxrpl/tx/transactors/system/LedgerStateFix.cpp index 0ce0720ba0..73e2e854e0 100644 --- a/src/libxrpl/tx/transactors/system/LedgerStateFix.cpp +++ b/src/libxrpl/tx/transactors/system/LedgerStateFix.cpp @@ -1,8 +1,8 @@ #include +#include #include #include #include -#include #include namespace xrpl { diff --git a/src/libxrpl/tx/wasm/ContractContext.cpp b/src/libxrpl/tx/wasm/ContractContext.cpp new file mode 100644 index 0000000000..46dc773298 --- /dev/null +++ b/src/libxrpl/tx/wasm/ContractContext.cpp @@ -0,0 +1,32 @@ +#include +#include +#include +#include + +namespace xrpl { + +std::vector +getParameterValueVec(STArray const& functionParameters) +{ + std::vector param_map; + for (auto const& param : functionParameters) + { + auto const& value = param.getFieldData(sfParameterValue); + param_map.emplace_back(value); + } + return param_map; +} + +std::vector +getParameterTypeVec(STArray const& functionParameters) +{ + std::vector param_map; + for (auto const& param : functionParameters) + { + auto const& type = param.getFieldDataType(sfParameterType); + param_map.emplace_back(type); + } + return param_map; +} + +} // namespace xrpl diff --git a/src/libxrpl/tx/wasm/HostFuncImplNFT.cpp b/src/libxrpl/tx/wasm/HostFuncImplNFT.cpp index ac002d723f..5a0dfa0193 100644 --- a/src/libxrpl/tx/wasm/HostFuncImplNFT.cpp +++ b/src/libxrpl/tx/wasm/HostFuncImplNFT.cpp @@ -1,6 +1,6 @@ +#include #include #include -#include #include namespace xrpl { diff --git a/src/libxrpl/tx/wasm/HostFuncWrapper.cpp b/src/libxrpl/tx/wasm/HostFuncWrapper.cpp index e420565727..95ebe23f39 100644 --- a/src/libxrpl/tx/wasm/HostFuncWrapper.cpp +++ b/src/libxrpl/tx/wasm/HostFuncWrapper.cpp @@ -2,7 +2,9 @@ #include #include #include +#include #include +#include #include #include #include @@ -1792,6 +1794,610 @@ floatLog_wrap(void* env, wasm_val_vec_t const* params, wasm_val_vec_t* results) return returnResult(runtime, params, results, hf->floatLog(*x, *rounding), i); } +// Contract-specific host function wrappers + +wasm_trap_t* +instanceParam_wrap(void* env, wasm_val_vec_t const* params, wasm_val_vec_t* results) +{ + auto* hf = getHF(env); + auto const* rt = reinterpret_cast(hf->getRT()); + int index = 0; + if (params->data[3].of.i32 > maxWasmDataLength) + { + return hfResult(results, HostFunctionError::DATA_FIELD_TOO_LARGE); + } + + auto const iindex = getDataInt32(rt, params, index); + if (!iindex) + { + return hfResult(results, iindex.error()); + } + + auto const stTypeId = getDataInt32(rt, params, index); + if (!stTypeId) + { + return hfResult(results, stTypeId.error()); + } + + return returnResult(rt, params, results, hf->instanceParam(*iindex, *stTypeId), index); +} + +wasm_trap_t* +functionParam_wrap(void* env, wasm_val_vec_t const* params, wasm_val_vec_t* results) +{ + auto* hf = getHF(env); + auto const* rt = reinterpret_cast(hf->getRT()); + int index = 0; + if (params->data[3].of.i32 > maxWasmDataLength) + { + return hfResult(results, HostFunctionError::DATA_FIELD_TOO_LARGE); + } + + auto const iindex = getDataInt32(rt, params, index); + if (!iindex) + { + return hfResult(results, iindex.error()); + } + + auto const stTypeId = getDataInt32(rt, params, index); + if (!stTypeId) + { + return hfResult(results, stTypeId.error()); + } + + return returnResult(rt, params, results, hf->functionParam(*iindex, *stTypeId), index); +} + +wasm_trap_t* +getDataObjectField_wrap(void* env, wasm_val_vec_t const* params, wasm_val_vec_t* results) +{ + auto* hf = getHF(env); + auto const* rt = reinterpret_cast(hf->getRT()); + int index = 0; + if (params->data[1].of.i32 > maxWasmDataLength) + { + return hfResult(results, HostFunctionError::DATA_FIELD_TOO_LARGE); + } + + auto const acc = getDataAccountID(rt, params, index); + if (!acc) + { + return hfResult(results, acc.error()); + } + + if (params->data[3].of.i32 > maxWasmDataLength) + { + return hfResult(results, HostFunctionError::DATA_FIELD_TOO_LARGE); + } + + auto const key = getDataString(rt, params, index); + if (!key) + { + return hfResult(results, key.error()); + } + + if (params->data[5].of.i32 > maxWasmDataLength) + { + return hfResult(results, HostFunctionError::DATA_FIELD_TOO_LARGE); + } + + return returnResult(rt, params, results, hf->getDataObjectField(*acc, *key), index); +} + +wasm_trap_t* +getDataNestedObjectField_wrap(void* env, wasm_val_vec_t const* params, wasm_val_vec_t* results) +{ + auto* hf = getHF(env); + auto const* rt = reinterpret_cast(hf->getRT()); + int index = 0; + if (params->data[1].of.i32 > maxWasmDataLength) + { + return hfResult(results, HostFunctionError::DATA_FIELD_TOO_LARGE); + } + + auto const acc = getDataAccountID(rt, params, index); + if (!acc) + { + return hfResult(results, acc.error()); + } + + if (params->data[3].of.i32 > maxWasmDataLength) + { + return hfResult(results, HostFunctionError::DATA_FIELD_TOO_LARGE); + } + + auto const key = getDataString(rt, params, index); + if (!key) + { + return hfResult(results, key.error()); + } + + if (params->data[5].of.i32 > maxWasmDataLength) + { + return hfResult(results, HostFunctionError::DATA_FIELD_TOO_LARGE); + } + + auto const nested = getDataString(rt, params, index); + if (!nested) + { + return hfResult(results, nested.error()); + } + + if (params->data[7].of.i32 > maxWasmDataLength) + { + return hfResult(results, HostFunctionError::DATA_FIELD_TOO_LARGE); + } + + return returnResult( + rt, params, results, hf->getDataNestedObjectField(*acc, *key, *nested), index); +} + +wasm_trap_t* +getDataArrayElementField_wrap(void* env, wasm_val_vec_t const* params, wasm_val_vec_t* results) +{ + auto* hf = getHF(env); + auto const* rt = reinterpret_cast(hf->getRT()); + int index = 0; + if (params->data[1].of.i32 > maxWasmDataLength) + { + return hfResult(results, HostFunctionError::DATA_FIELD_TOO_LARGE); + } + + auto const acc = getDataAccountID(rt, params, index); + if (!acc) + { + return hfResult(results, acc.error()); + } + + if (params->data[3].of.i32 > maxWasmDataLength) + { + return hfResult(results, HostFunctionError::DATA_FIELD_TOO_LARGE); + } + + auto const key = getDataString(rt, params, index); + if (!key) + { + return hfResult(results, key.error()); + } + + auto const elemIndex = getDataInt32(rt, params, index); + if (!elemIndex) + { + return hfResult(results, elemIndex.error()); + } + + if (params->data[6].of.i32 > maxWasmDataLength) + { + return hfResult(results, HostFunctionError::DATA_FIELD_TOO_LARGE); + } + + return returnResult( + rt, params, results, hf->getDataArrayElementField(*acc, *elemIndex, *key), index); +} + +wasm_trap_t* +getDataNestedArrayElementField_wrap( + void* env, + wasm_val_vec_t const* params, + wasm_val_vec_t* results) +{ + auto* hf = getHF(env); + auto const* rt = reinterpret_cast(hf->getRT()); + int index = 0; + if (params->data[1].of.i32 > maxWasmDataLength) + { + return hfResult(results, HostFunctionError::DATA_FIELD_TOO_LARGE); + } + + auto const acc = getDataAccountID(rt, params, index); + if (!acc) + { + return hfResult(results, acc.error()); + } + + if (params->data[3].of.i32 > maxWasmDataLength) + { + return hfResult(results, HostFunctionError::DATA_FIELD_TOO_LARGE); + } + + auto const key = getDataString(rt, params, index); + if (!key) + { + return hfResult(results, key.error()); + } + + auto const elemIndex = getDataInt32(rt, params, index); + if (!elemIndex) + { + return hfResult(results, elemIndex.error()); + } + + if (params->data[6].of.i32 > maxWasmDataLength) + { + return hfResult(results, HostFunctionError::DATA_FIELD_TOO_LARGE); + } + + auto const nested = getDataString(rt, params, index); + if (!nested) + { + return hfResult(results, nested.error()); + } + + if (params->data[8].of.i32 > maxWasmDataLength) + { + return hfResult(results, HostFunctionError::DATA_FIELD_TOO_LARGE); + } + + return returnResult( + rt, + params, + results, + hf->getDataNestedArrayElementField(*acc, *key, *elemIndex, *nested), + index); +} + +wasm_trap_t* +setDataObjectField_wrap(void* env, wasm_val_vec_t const* params, wasm_val_vec_t* results) +{ + auto* hf = getHF(env); + auto const* rt = reinterpret_cast(hf->getRT()); + int index = 0; + if (params->data[1].of.i32 > maxWasmDataLength) + { + return hfResult(results, HostFunctionError::DATA_FIELD_TOO_LARGE); + } + + auto const acc = getDataAccountID(rt, params, index); + if (!acc) + { + return hfResult(results, acc.error()); + } + + if (params->data[3].of.i32 > maxWasmDataLength) + { + return hfResult(results, HostFunctionError::DATA_FIELD_TOO_LARGE); + } + + auto const key = getDataString(rt, params, index); + if (!key) + { + return hfResult(results, key.error()); + } + + if (params->data[5].of.i32 > maxWasmDataLength) + { + return hfResult(results, HostFunctionError::DATA_FIELD_TOO_LARGE); + } + + auto const data = getDataSlice(rt, params, index); + if (!data) + { + return hfResult(results, data.error()); + } + + SerialIter valueSit(data->data(), data->size()); + STJson::Value const value = STJson::makeValueFromVLWithType(valueSit); + return returnResult(rt, params, results, hf->setDataObjectField(*acc, *key, value), index); +} + +wasm_trap_t* +setDataNestedObjectField_wrap(void* env, wasm_val_vec_t const* params, wasm_val_vec_t* results) +{ + auto* hf = getHF(env); + auto const* rt = reinterpret_cast(hf->getRT()); + int index = 0; + if (params->data[1].of.i32 > maxWasmDataLength) + { + return hfResult(results, HostFunctionError::DATA_FIELD_TOO_LARGE); + } + + auto const acc = getDataAccountID(rt, params, index); + if (!acc) + { + return hfResult(results, acc.error()); + } + + if (params->data[3].of.i32 > maxWasmDataLength) + { + return hfResult(results, HostFunctionError::DATA_FIELD_TOO_LARGE); + } + + auto const nested = getDataString(rt, params, index); + if (!nested) + { + return hfResult(results, nested.error()); + } + + if (params->data[5].of.i32 > maxWasmDataLength) + { + return hfResult(results, HostFunctionError::DATA_FIELD_TOO_LARGE); + } + + auto const key = getDataString(rt, params, index); + if (!key) + { + return hfResult(results, key.error()); + } + + if (params->data[7].of.i32 > maxWasmDataLength) + { + return hfResult(results, HostFunctionError::DATA_FIELD_TOO_LARGE); + } + + auto const data = getDataSlice(rt, params, index); + if (!data) + { + return hfResult(results, data.error()); + } + + SerialIter valueSit(data->data(), data->size()); + STJson::Value const value = STJson::makeValueFromVLWithType(valueSit); + return returnResult( + rt, params, results, hf->setDataNestedObjectField(*acc, *nested, *key, value), index); +} + +wasm_trap_t* +setDataArrayElementField_wrap(void* env, wasm_val_vec_t const* params, wasm_val_vec_t* results) +{ + auto* hf = getHF(env); + auto const* rt = reinterpret_cast(hf->getRT()); + int index = 0; + if (params->data[1].of.i32 > maxWasmDataLength) + { + return hfResult(results, HostFunctionError::DATA_FIELD_TOO_LARGE); + } + + auto const acc = getDataAccountID(rt, params, index); + if (!acc) + { + return hfResult(results, acc.error()); + } + + if (params->data[3].of.i32 > maxWasmDataLength) + { + return hfResult(results, HostFunctionError::DATA_FIELD_TOO_LARGE); + } + + auto const key = getDataString(rt, params, index); + if (!key) + { + return hfResult(results, key.error()); + } + + auto const elemIndex = getDataInt32(rt, params, index); + if (!elemIndex) + { + return hfResult(results, elemIndex.error()); + } + + if (params->data[6].of.i32 > maxWasmDataLength) + { + return hfResult(results, HostFunctionError::DATA_FIELD_TOO_LARGE); + } + + auto const data = getDataSlice(rt, params, index); + if (!data) + { + return hfResult(results, data.error()); + } + + SerialIter valueSit(data->data(), data->size()); + STJson::Value const value = STJson::makeValueFromVLWithType(valueSit); + return returnResult( + rt, params, results, hf->setDataArrayElementField(*acc, *elemIndex, *key, value), index); +} + +wasm_trap_t* +setDataNestedArrayElementField_wrap( + void* env, + wasm_val_vec_t const* params, + wasm_val_vec_t* results) +{ + auto* hf = getHF(env); + auto const* rt = reinterpret_cast(hf->getRT()); + int index = 0; + if (params->data[1].of.i32 > maxWasmDataLength) + { + return hfResult(results, HostFunctionError::DATA_FIELD_TOO_LARGE); + } + + auto const acc = getDataAccountID(rt, params, index); + if (!acc) + { + return hfResult(results, acc.error()); + } + + if (params->data[3].of.i32 > maxWasmDataLength) + { + return hfResult(results, HostFunctionError::DATA_FIELD_TOO_LARGE); + } + + auto const key = getDataString(rt, params, index); + if (!key) + { + return hfResult(results, key.error()); + } + + auto const elemIndex = getDataInt32(rt, params, index); + if (!elemIndex) + { + return hfResult(results, elemIndex.error()); + } + + if (params->data[6].of.i32 > maxWasmDataLength) + { + return hfResult(results, HostFunctionError::DATA_FIELD_TOO_LARGE); + } + + auto const nested = getDataString(rt, params, index); + if (!nested) + { + return hfResult(results, nested.error()); + } + + if (params->data[8].of.i32 > maxWasmDataLength) + { + return hfResult(results, HostFunctionError::DATA_FIELD_TOO_LARGE); + } + + auto const data = getDataSlice(rt, params, index); + if (!data) + { + return hfResult(results, data.error()); + } + + SerialIter valueSit(data->data(), data->size()); + STJson::Value const value = STJson::makeValueFromVLWithType(valueSit); + return returnResult( + rt, + params, + results, + hf->setDataNestedArrayElementField(*acc, *key, *elemIndex, *nested, value), + index); +} + +wasm_trap_t* +buildTxn_wrap(void* env, wasm_val_vec_t const* params, wasm_val_vec_t* results) +{ + auto* hf = getHF(env); + auto const* rt = reinterpret_cast(hf->getRT()); + int index = 0; + if (params->data[1].of.i32 > maxWasmDataLength) + { + return hfResult(results, HostFunctionError::DATA_FIELD_TOO_LARGE); + } + + auto const txnType = getDataInt32(rt, params, index); + if (!txnType) + { + return hfResult(results, txnType.error()); + } + + return returnResult(rt, params, results, hf->buildTxn(*txnType), index); +} + +wasm_trap_t* +addTxnField_wrap(void* env, wasm_val_vec_t const* params, wasm_val_vec_t* results) +{ + auto* hf = getHF(env); + auto const* rt = reinterpret_cast(hf->getRT()); + int index = 0; + if (params->data[3].of.i32 > maxWasmDataLength) + { + return hfResult(results, HostFunctionError::DATA_FIELD_TOO_LARGE); + } + + auto const txnIndex = getDataInt32(rt, params, index); + if (!txnIndex) + { + return hfResult(results, txnIndex.error()); + } + + auto const fname = getDataSField(rt, params, index); + if (!fname) + { + return hfResult(results, fname.error()); + } + + auto const data = getDataSlice(rt, params, index); + if (!data) + { + return hfResult(results, data.error()); + } + + return returnResult(rt, params, results, hf->addTxnField(*txnIndex, *fname, *data), index); +} + +wasm_trap_t* +emitBuiltTxn_wrap(void* env, wasm_val_vec_t const* params, wasm_val_vec_t* results) +{ + auto* hf = getHF(env); + auto const* rt = reinterpret_cast(hf->getRT()); + int index = 0; + if (params->data[1].of.i32 > maxWasmDataLength) + { + return hfResult(results, HostFunctionError::DATA_FIELD_TOO_LARGE); + } + + auto const txnIndex = getDataInt32(rt, params, index); + if (!txnIndex) + { + return hfResult(results, txnIndex.error()); + } + + return returnResult(rt, params, results, hf->emitBuiltTxn(*txnIndex), index); +} + +wasm_trap_t* +emitTxn_wrap(void* env, wasm_val_vec_t const* params, wasm_val_vec_t* results) +{ + auto* hf = getHF(env); + auto const* rt = reinterpret_cast(hf->getRT()); + int index = 0; + if (params->data[1].of.i32 > maxWasmDataLength) + { + return hfResult(results, HostFunctionError::DATA_FIELD_TOO_LARGE); + } + + auto const slice = getDataSlice(rt, params, index); + if (!slice) + { + return hfResult(results, slice.error()); + } + + std::shared_ptr stpTrans; + try + { + stpTrans = std::make_shared(SerialIter{*slice}); + } + catch (std::exception& e) + { + std::cout << "Error creating STTx: " << e.what() << std::endl; + return hfResult(results, HostFunctionError::INTERNAL); + } + + return returnResult(rt, params, results, hf->emitTxn(stpTrans), index); +} + +wasm_trap_t* +emitEvent_wrap(void* env, wasm_val_vec_t const* params, wasm_val_vec_t* results) +{ + auto* hf = getHF(env); + auto const* rt = reinterpret_cast(hf->getRT()); + int index = 0; + if (params->data[1].of.i32 > maxWasmDataLength) + { + return hfResult(results, HostFunctionError::DATA_FIELD_TOO_LARGE); + } + + auto const name = getDataString(rt, params, index); + if (!name) + { + return hfResult(results, name.error()); + } + + auto const data = getDataSlice(rt, params, index); + if (!data) + { + return hfResult(results, data.error()); + } + + std::shared_ptr parsed; + try + { + parsed = STJson::fromBlob(data->data(), data->size()); + } + catch (std::exception const&) + { + return hfResult(results, HostFunctionError::INVALID_PARAMS); + } + + if (!parsed) + return hfResult(results, HostFunctionError::INVALID_PARAMS); + + return returnResult(rt, params, results, hf->emitEvent(*name, *parsed), index); +} + // LCOV_EXCL_START namespace test { diff --git a/src/libxrpl/tx/wasm/WasmVM.cpp b/src/libxrpl/tx/wasm/WasmVM.cpp index c8a1e27048..e0f5aeb063 100644 --- a/src/libxrpl/tx/wasm/WasmVM.cpp +++ b/src/libxrpl/tx/wasm/WasmVM.cpp @@ -96,6 +96,28 @@ createWasmImport(HostFunctions& hfs) setCommonHostFunctions(&hfs, i); WASM_IMPORT_FUNC2(i, updateData, "update_data", &hfs, 1000); + // clang-format off + // Contract-specific host functions + WASM_IMPORT_FUNC2(i, instanceParam, "instance_param", &hfs, 100); + WASM_IMPORT_FUNC2(i, functionParam, "function_param", &hfs, 100); + + WASM_IMPORT_FUNC2(i, getDataObjectField, "get_data_object_field", &hfs, 500); + WASM_IMPORT_FUNC2(i, getDataNestedObjectField, "get_data_nested_object_field", &hfs, 500); + WASM_IMPORT_FUNC2(i, getDataArrayElementField, "get_data_array_element_field", &hfs, 500); + WASM_IMPORT_FUNC2(i, getDataNestedArrayElementField, "get_data_nested_array_element_field", &hfs, 500); + + WASM_IMPORT_FUNC2(i, setDataObjectField, "set_data_object_field", &hfs, 500); + WASM_IMPORT_FUNC2(i, setDataNestedObjectField, "set_data_nested_object_field", &hfs, 500); + WASM_IMPORT_FUNC2(i, setDataArrayElementField, "set_data_array_element_field", &hfs, 500); + WASM_IMPORT_FUNC2(i, setDataNestedArrayElementField, "set_data_nested_array_element_field", &hfs, 500); + + WASM_IMPORT_FUNC2(i, buildTxn, "build_txn", &hfs, 200); + WASM_IMPORT_FUNC2(i, addTxnField, "add_txn_field", &hfs, 200); + WASM_IMPORT_FUNC2(i, emitBuiltTxn, "emit_built_txn", &hfs, 500); + WASM_IMPORT_FUNC2(i, emitTxn, "emit_txn", &hfs, 500); + WASM_IMPORT_FUNC2(i, emitEvent, "emit_event", &hfs, 500); + // clang-format on + return i; } diff --git a/src/test/app/ContractHostFuncImpl_test.cpp b/src/test/app/ContractHostFuncImpl_test.cpp new file mode 100644 index 0000000000..928070a34f --- /dev/null +++ b/src/test/app/ContractHostFuncImpl_test.cpp @@ -0,0 +1,1379 @@ +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace xrpl { +namespace test { + +static ApplyContext +createApplyContext( + test::jtx::Env& env, + OpenView& ov, + STTx const& tx = STTx(ttCONTRACT_CALL, [](STObject&) {})) +{ + ApplyContext ac{ + env.app(), ov, tx, tesSUCCESS, env.current()->fees().base, tapNONE, env.journal}; + return ac; +} + +struct ContractHostFuncImpl_test : public beast::unit_test::suite +{ + ContractContext + createContractContext( + ApplyContext& ac, + jtx::Account const& contract, + jtx::Account const& otxn, + uint256 const& contractHash = uint256{1}) + { + using namespace jtx; + xrpl::ContractDataMap dataMap; + xrpl::ContractEventMap eventMap; + std::vector instanceParameters; + std::vector functionParameters; + + uint256 const& txId = uint256{2}; + + auto const nextSequence = + ac.view().read(keylet::account(contract.id()))->getFieldU32(sfSequence); + + auto const k = keylet::contract(contractHash, otxn, 0); + return ContractContext{ + .applyCtx = ac, + .instanceParameters = instanceParameters, + .functionParameters = functionParameters, + .built_txns = {}, + .expected_etxn_count = 0, + .generation = 0, + .burden = 0, + .result = + { + .contractHash = contractHash, + .contractKeylet = k, + .contractSourceKeylet = k, + .contractAccountKeylet = k, + .contractAccount = contract.id(), + .nextSequence = nextSequence, + .otxnAccount = otxn.id(), + .otxnId = txId, + .exitCode = -1, + .dataMap = dataMap, + .eventMap = eventMap, + .changedDataCount = 0, + }, + }; + } + + // Helper function to create STJson::Value from different types + STJson::Value + createJsonValue(SerializedTypeID type, std::function addData) + { + Serializer s; + s.add8(static_cast(type)); + addData(s); + + SerialIter sit(s.peekData().data(), s.peekData().size()); + return STJson::makeValueFromVLWithType(sit); + } + + void + testInstanceParam() + { + testcase("instanceParam"); + using namespace test::jtx; + + Env env{*this}; + Account const alice("alice"); + Account const contract("contract"); + Account const otxn("otxn"); + env.fund(XRP(10000), alice, contract, otxn); + env.close(); + + OpenView ov{*env.current()}; + ApplyContext ac = createApplyContext(env, ov); + + auto contractCtx = createContractContext(ac, contract, otxn); + + // Add test instance parameters for all supported types + + // UINT8 + contractCtx.instanceParameters.push_back( + ParameterValueVec{STData(sfParameterValue, uint8_t{0xFF})}); + + // UINT16 + contractCtx.instanceParameters.push_back( + ParameterValueVec{STData(sfParameterValue, uint16_t{0xFFFF})}); + + // UINT32 + contractCtx.instanceParameters.push_back( + ParameterValueVec{STData(sfParameterValue, uint32_t{0xFFFFFFFF})}); + + // UINT64 + contractCtx.instanceParameters.push_back( + ParameterValueVec{STData(sfParameterValue, uint64_t{0x8000000000000000})}); + + // UINT128 + contractCtx.instanceParameters.push_back( + ParameterValueVec{STData(sfParameterValue, uint128{1})}); + + // UINT160 + contractCtx.instanceParameters.push_back( + ParameterValueVec{STData(sfParameterValue, uint160{1})}); + + // UINT192 + contractCtx.instanceParameters.push_back( + ParameterValueVec{STData(sfParameterValue, uint192{1})}); + + // UINT256 + contractCtx.instanceParameters.push_back( + ParameterValueVec{STData(sfParameterValue, uint256{1})}); + + // VL (Variable Length) + contractCtx.instanceParameters.push_back( + ParameterValueVec{STData(sfParameterValue, Blob{0x01, 0x02, 0x03, 0x04, 0x05})}); + + // ACCOUNT + contractCtx.instanceParameters.push_back( + ParameterValueVec{STData(sfParameterValue, alice.id())}); + + // AMOUNT (XRP) + contractCtx.instanceParameters.push_back( + ParameterValueVec{STData(sfParameterValue, STAmount{100000})}); + + // AMOUNT (IOU) + contractCtx.instanceParameters.push_back( + ParameterValueVec{ + STData(sfParameterValue, STAmount{Issue{Currency{1}, AccountID{2}}, 1000})}); + + // AMOUNT (MPT) + MPTIssue const mpt{MPTIssue{makeMptID(1, AccountID(0x4985601))}}; + contractCtx.instanceParameters.push_back( + ParameterValueVec{STData(sfParameterValue, STAmount{mpt, 1000})}); + + // NUMBER + contractCtx.instanceParameters.push_back( + ParameterValueVec{STData(sfParameterValue, numberFromJson(sfNumber, "42.5"))}); + + // ISSUE + // STIssue{sfAsset2, MPTIssue{mptId} + auto const iouAsset = env.master["USD"]; + contractCtx.instanceParameters.push_back( + ParameterValueVec{STData(sfParameterValue, STIssue{sfAsset, iouAsset.issue()})}); + + // CURRENCY + contractCtx.instanceParameters.push_back( + ParameterValueVec{ + STData(sfParameterValue, STCurrency{sfBaseAsset, iouAsset.issue().currency})}); + + ContractHostFunctionsImpl cfs(contractCtx); + + // Test UINT8 + { + auto result = cfs.instanceParam(0, STI_UINT8); + BEAST_EXPECT(result.has_value()); + if (result.has_value()) + { + auto& bytes = result.value(); + BEAST_EXPECT(bytes.size() == 1); + BEAST_EXPECT(bytes[0] == 0xFF); + } + } + + // Test UINT16 + { + auto result = cfs.instanceParam(1, STI_UINT16); + BEAST_EXPECT(result.has_value()); + if (result.has_value()) + { + auto& bytes = result.value(); + BEAST_EXPECT(bytes.size() == 2); + BEAST_EXPECT(bytes[0] == 0xFF); + BEAST_EXPECT(bytes[1] == 0xFF); + } + } + + // Test UINT32 + { + auto result = cfs.instanceParam(2, STI_UINT32); + BEAST_EXPECT(result.has_value()); + if (result.has_value()) + { + auto& bytes = result.value(); + BEAST_EXPECT(bytes.size() == 4); + BEAST_EXPECT(bytes[0] == 0xFF); + BEAST_EXPECT(bytes[1] == 0xFF); + BEAST_EXPECT(bytes[2] == 0xFF); + BEAST_EXPECT(bytes[3] == 0xFF); + } + } + + // Test UINT64 + { + auto result = cfs.instanceParam(3, STI_UINT64); + BEAST_EXPECT(result.has_value()); + if (result.has_value()) + { + auto& bytes = result.value(); + BEAST_EXPECT(bytes.size() == 8); + BEAST_EXPECT(bytes[0] == 0x00); + BEAST_EXPECT(bytes[7] == 0x80); + } + } + + // Test UINT128 + { + auto result = cfs.instanceParam(4, STI_UINT128); + BEAST_EXPECT(result.has_value()); + if (result.has_value()) + { + auto& bytes = result.value(); + BEAST_EXPECT(bytes.size() == sizeof(uint128)); + BEAST_EXPECT(bytes[15] == 0x01); + } + } + + // Test UINT160 + { + auto result = cfs.instanceParam(5, STI_UINT160); + BEAST_EXPECT(result.has_value()); + if (result.has_value()) + { + auto& bytes = result.value(); + BEAST_EXPECT(bytes.size() == sizeof(uint160)); + BEAST_EXPECT(bytes[19] == 0x01); + } + } + + // Test UINT192 + { + auto result = cfs.instanceParam(6, STI_UINT192); + BEAST_EXPECT(result.has_value()); + if (result.has_value()) + { + auto& bytes = result.value(); + BEAST_EXPECT(bytes.size() == sizeof(uint192)); + BEAST_EXPECT(bytes[23] == 0x01); + } + } + + // Test UINT256 + { + auto result = cfs.instanceParam(7, STI_UINT256); + BEAST_EXPECT(result.has_value()); + if (result.has_value()) + { + auto& bytes = result.value(); + BEAST_EXPECT(bytes.size() == sizeof(uint256)); + BEAST_EXPECT(bytes[31] == 0x01); + } + } + + // Test VL + { + auto result = cfs.instanceParam(8, STI_VL); + BEAST_EXPECT(result.has_value()); + if (result.has_value()) + { + auto& bytes = result.value(); + BEAST_EXPECT(bytes.size() == 5); + BEAST_EXPECT(bytes[0] == 0x01); + BEAST_EXPECT(bytes[4] == 0x05); + } + } + + // Test ACCOUNT + { + auto result = cfs.instanceParam(9, STI_ACCOUNT); + BEAST_EXPECT(result.has_value()); + if (result.has_value()) + { + auto& bytes = result.value(); + BEAST_EXPECT(bytes.size() == 20); + } + } + + // Test AMOUNT (XRP) + { + auto result = cfs.instanceParam(10, STI_AMOUNT); + BEAST_EXPECT(result.has_value()); + if (result.has_value()) + { + auto& bytes = result.value(); + BEAST_EXPECT(bytes.size() == 8); // Native amount + } + } + + // Test AMOUNT (IOU) + { + auto result = cfs.instanceParam(11, STI_AMOUNT); + BEAST_EXPECT(result.has_value()); + if (result.has_value()) + { + auto& bytes = result.value(); + BEAST_EXPECT(bytes.size() == 48); // IOU amount + } + } + + // Test AMOUNT (MPT) + { + auto result = cfs.instanceParam(12, STI_AMOUNT); + BEAST_EXPECT(result.has_value()); + if (result.has_value()) + { + auto& bytes = result.value(); + BEAST_EXPECT(bytes.size() == 33); // MPT amount + } + } + + // Test NUMBER + { + auto result = cfs.instanceParam(13, STI_NUMBER); + BEAST_EXPECT(result.has_value()); + if (result.has_value()) + { + auto& bytes = result.value(); + BEAST_EXPECT(bytes.size() == 12); + } + } + + // Test ISSUE + { + auto result = cfs.instanceParam(14, STI_ISSUE); + BEAST_EXPECT(result.has_value()); + if (result.has_value()) + { + auto& bytes = result.value(); + BEAST_EXPECT(bytes.size() == 40); + } + } + + // Test CURRENCY + { + auto result = cfs.instanceParam(15, STI_CURRENCY); + BEAST_EXPECT(result.has_value()); + if (result.has_value()) + { + auto& bytes = result.value(); + BEAST_EXPECT(bytes.size() == 20); + } + } + + // Test index out of bounds + { + auto result = cfs.instanceParam(16, STI_UINT32); + BEAST_EXPECT(!result.has_value()); + if (!result.has_value()) + BEAST_EXPECT(result.error() == HostFunctionError::INDEX_OUT_OF_BOUNDS); + } + + // Test type mismatch + { + auto result = cfs.instanceParam(0, STI_UINT64); // Index 0 is UINT8 + BEAST_EXPECT(!result.has_value()); + if (!result.has_value()) + BEAST_EXPECT(result.error() == HostFunctionError::INVALID_PARAMS); + } + + // Test unsupported types + { + auto result = cfs.instanceParam(2, STI_PATHSET); + BEAST_EXPECT(!result.has_value()); + if (!result.has_value()) + BEAST_EXPECT(result.error() == HostFunctionError::INVALID_PARAMS); + } + } + + void + testFunctionParam() + { + testcase("functionParam"); + using namespace test::jtx; + + Env env{*this}; + Account const alice("alice"); + Account const contract("contract"); + Account const otxn("otxn"); + env.fund(XRP(10000), alice, contract, otxn); + env.close(); + + OpenView ov{*env.current()}; + ApplyContext ac = createApplyContext(env, ov); + + auto contractCtx = createContractContext(ac, contract, otxn); + + // Add test function parameters (same as instance parameters for + // testing) [Similar parameter setup as instanceParam test...] + + ContractHostFunctionsImpl cfs(contractCtx); + + // [Similar tests as instanceParam but using functionParam method...] + } + + void + testContractDataFromKey() + { + testcase("contractDataFromKey"); + using namespace test::jtx; + + Env env{*this}; + Account const alice("alice"); + Account const bob("bob"); + Account const contract("contract"); + Account const otxn("otxn"); + env.fund(XRP(10000), alice, bob, contract, otxn); + env.close(); + + OpenView ov{*env.current()}; + ApplyContext ac = createApplyContext(env, ov); + + auto contractCtx = createContractContext(ac, contract, otxn); + + ContractHostFunctionsImpl cfs(contractCtx); + + // Test setDataObjectField - string value + { + // Create a properly formatted STJson::Value for a VL (string) + auto value = createJsonValue(STI_VL, [](Serializer& s) { + s.addVL(Blob{0x61, 0x62, 0x63}); // "abc" + }); + + auto setResult = cfs.setDataObjectField(alice.id(), "name", value); + BEAST_EXPECT(!setResult.has_value()); + if (!setResult.has_value()) + BEAST_EXPECT(setResult.error() == HostFunctionError::INTERNAL); + + // Verify data was cached and can be retrieved + auto getResult = cfs.getDataObjectField(alice.id(), "name"); + BEAST_EXPECT(getResult.has_value()); + if (getResult.has_value()) + { + auto& bytes = getResult.value(); + BEAST_EXPECT(bytes.size() > 0); + } + } + + // Test setDataObjectField - numeric value (UINT32) + { + auto value = createJsonValue(STI_UINT32, [](Serializer& s) { s.add32(30); }); + + auto setResult = cfs.setDataObjectField(alice.id(), "age", value); + BEAST_EXPECT(!setResult.has_value()); + if (!setResult.has_value()) + BEAST_EXPECT(setResult.error() == HostFunctionError::INTERNAL); + + auto getResult = cfs.getDataObjectField(alice.id(), "age"); + BEAST_EXPECT(getResult.has_value()); + } + + // Test setDataObjectField - UINT8 value (for boolean-like) + { + auto value = createJsonValue(STI_UINT8, [](Serializer& s) { + s.add8(1); // true + }); + + auto setResult = cfs.setDataObjectField(alice.id(), "verified", value); + BEAST_EXPECT(!setResult.has_value()); + if (!setResult.has_value()) + BEAST_EXPECT(setResult.error() == HostFunctionError::INTERNAL); + + auto getResult = cfs.getDataObjectField(alice.id(), "verified"); + BEAST_EXPECT(getResult.has_value()); + } + + // Test getting non-existent key + { + auto getResult = cfs.getDataObjectField(alice.id(), "nonexistent"); + BEAST_EXPECT(!getResult.has_value()); + if (!getResult.has_value()) + BEAST_EXPECT(getResult.error() == HostFunctionError::INVALID_FIELD); + } + + // Test updating existing key + { + auto value1 = createJsonValue(STI_UINT32, [](Serializer& s) { s.add32(100); }); + auto setResult1 = cfs.setDataObjectField(bob.id(), "balance", value1); + BEAST_EXPECT(!setResult1.has_value()); + + auto value2 = createJsonValue(STI_UINT32, [](Serializer& s) { s.add32(200); }); + auto setResult2 = cfs.setDataObjectField(bob.id(), "balance", value2); + BEAST_EXPECT(!setResult2.has_value()); + + auto getResult = cfs.getDataObjectField(bob.id(), "balance"); + BEAST_EXPECT(getResult.has_value()); + } + + // Test multiple keys for same account + { + auto value1 = createJsonValue(STI_VL, [](Serializer& s) { + Blob data = {'v', 'a', 'l', 'u', 'e', '1'}; + s.addVL(data); + }); + auto setResult1 = cfs.setDataObjectField(alice.id(), "field1", value1); + BEAST_EXPECT(!setResult1.has_value()); + + auto value2 = createJsonValue(STI_VL, [](Serializer& s) { + Blob data = {'v', 'a', 'l', 'u', 'e', '2'}; + s.addVL(data); + }); + auto setResult2 = cfs.setDataObjectField(alice.id(), "field2", value2); + BEAST_EXPECT(!setResult2.has_value()); + + auto value3 = createJsonValue(STI_UINT32, [](Serializer& s) { s.add32(123); }); + auto setResult3 = cfs.setDataObjectField(alice.id(), "field3", value3); + BEAST_EXPECT(!setResult3.has_value()); + + // Verify all keys exist + auto getResult1 = cfs.getDataObjectField(alice.id(), "field1"); + BEAST_EXPECT(getResult1.has_value()); + + auto getResult2 = cfs.getDataObjectField(alice.id(), "field2"); + BEAST_EXPECT(getResult2.has_value()); + + auto getResult3 = cfs.getDataObjectField(alice.id(), "field3"); + BEAST_EXPECT(getResult3.has_value()); + } + } + + void + testNestedContractDataFromKey() + { + testcase("nestedContractDataFromKey"); + using namespace test::jtx; + + Env env{*this}; + Account const alice("alice"); + Account const bob("bob"); + Account const contract("contract"); + Account const otxn("otxn"); + env.fund(XRP(10000), alice, bob, contract, otxn); + env.close(); + + OpenView ov{*env.current()}; + ApplyContext ac = createApplyContext(env, ov); + + auto contractCtx = createContractContext(ac, contract, otxn); + + ContractHostFunctionsImpl cfs(contractCtx); + + // Test setDataNestedObjectField + { + auto value = createJsonValue(STI_VL, [](Serializer& s) { + Blob data = {'A', 'l', 'i', 'c', 'e'}; + s.addVL(data); + }); + + auto setResult = + cfs.setDataNestedObjectField(alice.id(), "profile", "firstName", value); + BEAST_EXPECT(!setResult.has_value()); + if (!setResult.has_value()) + BEAST_EXPECT(setResult.error() == HostFunctionError::INTERNAL); + + // Add more nested fields + auto value2 = createJsonValue(STI_VL, [](Serializer& s) { + Blob data = {'S', 'm', 'i', 't', 'h'}; + s.addVL(data); + }); + auto setResult2 = + cfs.setDataNestedObjectField(alice.id(), "profile", "lastName", value2); + BEAST_EXPECT(!setResult2.has_value()); + + auto value3 = createJsonValue(STI_UINT32, [](Serializer& s) { s.add32(25); }); + auto setResult3 = cfs.setDataNestedObjectField(alice.id(), "profile", "age", value3); + BEAST_EXPECT(!setResult3.has_value()); + + // Retrieve nested fields + auto getResult1 = cfs.getDataNestedObjectField(alice.id(), "profile", "firstName"); + BEAST_EXPECT(getResult1.has_value()); + + auto getResult2 = cfs.getDataNestedObjectField(alice.id(), "profile", "lastName"); + BEAST_EXPECT(getResult2.has_value()); + + auto getResult3 = cfs.getDataNestedObjectField(alice.id(), "profile", "age"); + BEAST_EXPECT(getResult3.has_value()); + } + + // Test nested objects with different parent keys + { + auto value1 = createJsonValue(STI_VL, [](Serializer& s) { + Blob data = {'d', 'a', 'r', 'k'}; + s.addVL(data); + }); + auto setResult1 = cfs.setDataNestedObjectField(alice.id(), "settings", "theme", value1); + BEAST_EXPECT(!setResult1.has_value()); + + auto value2 = createJsonValue(STI_UINT8, [](Serializer& s) { + s.add8(1); // true + }); + auto setResult2 = + cfs.setDataNestedObjectField(alice.id(), "settings", "notifications", value2); + BEAST_EXPECT(!setResult2.has_value()); + + auto value3 = createJsonValue(STI_VL, [](Serializer& s) { + Blob data = {'e', 'n'}; + s.addVL(data); + }); + auto setResult3 = + cfs.setDataNestedObjectField(alice.id(), "preferences", "language", value3); + BEAST_EXPECT(!setResult3.has_value()); + + // Verify nested data retrieval + auto getResult1 = cfs.getDataNestedObjectField(alice.id(), "settings", "theme"); + BEAST_EXPECT(getResult1.has_value()); + + auto getResult2 = cfs.getDataNestedObjectField(alice.id(), "settings", "notifications"); + BEAST_EXPECT(getResult2.has_value()); + + auto getResult3 = cfs.getDataNestedObjectField(alice.id(), "preferences", "language"); + BEAST_EXPECT(getResult3.has_value()); + } + + // Test getting non-existent nested key + { + auto getResult = cfs.getDataNestedObjectField(alice.id(), "nonexistent", "key"); + BEAST_EXPECT(!getResult.has_value()); + if (!getResult.has_value()) + BEAST_EXPECT(getResult.error() == HostFunctionError::INVALID_FIELD); + + auto getResult2 = cfs.getDataNestedObjectField(alice.id(), "profile", "nonexistent"); + BEAST_EXPECT(!getResult2.has_value()); + if (!getResult2.has_value()) + BEAST_EXPECT(getResult2.error() == HostFunctionError::INVALID_FIELD); + } + + // Test updating existing nested key + { + auto value1 = createJsonValue(STI_UINT32, [](Serializer& s) { s.add32(100); }); + auto setResult1 = cfs.setDataNestedObjectField(bob.id(), "stats", "score", value1); + BEAST_EXPECT(!setResult1.has_value()); + + auto value2 = createJsonValue(STI_UINT32, [](Serializer& s) { s.add32(150); }); + auto setResult2 = cfs.setDataNestedObjectField(bob.id(), "stats", "score", value2); + BEAST_EXPECT(!setResult2.has_value()); + + auto getResult = cfs.getDataNestedObjectField(bob.id(), "stats", "score"); + BEAST_EXPECT(getResult.has_value()); + } + } + + void + testNestedContractDataFromArrayKey() + { + testcase("nestedContractDataFromArrayKey"); + using namespace test::jtx; + + Env env{*this}; + Account const alice("alice"); + Account const bob("bob"); + Account const contract("contract"); + Account const otxn("otxn"); + env.fund(XRP(10000), alice, bob, contract, otxn); + env.close(); + + OpenView ov{*env.current()}; + ApplyContext ac = createApplyContext(env, ov); + + auto contractCtx = createContractContext(ac, contract, otxn); + + ContractHostFunctionsImpl cfs(contractCtx); + + // Test setDataNestedObjectField + { + auto value = createJsonValue(STI_VL, [](Serializer& s) { + Blob data = {'A', 'l', 'i', 'c', 'e'}; + s.addVL(data); + }); + + auto setResult = + cfs.setDataNestedObjectField(alice.id(), "profile", "firstName", value); + BEAST_EXPECT(!setResult.has_value()); + if (!setResult.has_value()) + BEAST_EXPECT(setResult.error() == HostFunctionError::INTERNAL); + + // Add more nested fields + auto value2 = createJsonValue(STI_VL, [](Serializer& s) { + Blob data = {'S', 'm', 'i', 't', 'h'}; + s.addVL(data); + }); + auto setResult2 = + cfs.setDataNestedObjectField(alice.id(), "profile", "lastName", value2); + BEAST_EXPECT(!setResult2.has_value()); + + auto value3 = createJsonValue(STI_UINT32, [](Serializer& s) { s.add32(25); }); + auto setResult3 = cfs.setDataNestedObjectField(alice.id(), "profile", "age", value3); + BEAST_EXPECT(!setResult3.has_value()); + + // Retrieve nested fields + auto getResult1 = cfs.getDataNestedObjectField(alice.id(), "profile", "firstName"); + BEAST_EXPECT(getResult1.has_value()); + + auto getResult2 = cfs.getDataNestedObjectField(alice.id(), "profile", "lastName"); + BEAST_EXPECT(getResult2.has_value()); + + auto getResult3 = cfs.getDataNestedObjectField(alice.id(), "profile", "age"); + BEAST_EXPECT(getResult3.has_value()); + } + + // Test nested objects with different parent keys + { + auto value1 = createJsonValue(STI_VL, [](Serializer& s) { + Blob data = {'d', 'a', 'r', 'k'}; + s.addVL(data); + }); + auto setResult1 = cfs.setDataNestedObjectField(alice.id(), "settings", "theme", value1); + BEAST_EXPECT(!setResult1.has_value()); + + auto value2 = createJsonValue(STI_UINT8, [](Serializer& s) { + s.add8(1); // true + }); + auto setResult2 = + cfs.setDataNestedObjectField(alice.id(), "settings", "notifications", value2); + BEAST_EXPECT(!setResult2.has_value()); + + auto value3 = createJsonValue(STI_VL, [](Serializer& s) { + Blob data = {'e', 'n'}; + s.addVL(data); + }); + auto setResult3 = + cfs.setDataNestedObjectField(alice.id(), "preferences", "language", value3); + BEAST_EXPECT(!setResult3.has_value()); + + // Verify nested data retrieval + auto getResult1 = cfs.getDataNestedObjectField(alice.id(), "settings", "theme"); + BEAST_EXPECT(getResult1.has_value()); + + auto getResult2 = cfs.getDataNestedObjectField(alice.id(), "settings", "notifications"); + BEAST_EXPECT(getResult2.has_value()); + + auto getResult3 = cfs.getDataNestedObjectField(alice.id(), "preferences", "language"); + BEAST_EXPECT(getResult3.has_value()); + } + + // Test getting non-existent nested key + { + auto getResult = cfs.getDataNestedObjectField(alice.id(), "nonexistent", "key"); + BEAST_EXPECT(!getResult.has_value()); + if (!getResult.has_value()) + BEAST_EXPECT(getResult.error() == HostFunctionError::INVALID_FIELD); + + auto getResult2 = cfs.getDataNestedObjectField(alice.id(), "profile", "nonexistent"); + BEAST_EXPECT(!getResult2.has_value()); + if (!getResult2.has_value()) + BEAST_EXPECT(getResult2.error() == HostFunctionError::INVALID_FIELD); + } + + // Test updating existing nested key + { + auto value1 = createJsonValue(STI_UINT32, [](Serializer& s) { s.add32(100); }); + auto setResult1 = cfs.setDataNestedObjectField(bob.id(), "stats", "score", value1); + BEAST_EXPECT(!setResult1.has_value()); + + auto value2 = createJsonValue(STI_UINT32, [](Serializer& s) { s.add32(150); }); + auto setResult2 = cfs.setDataNestedObjectField(bob.id(), "stats", "score", value2); + BEAST_EXPECT(!setResult2.has_value()); + + auto getResult = cfs.getDataNestedObjectField(bob.id(), "stats", "score"); + BEAST_EXPECT(getResult.has_value()); + } + } + + void + testBuildTxn() + { + testcase("buildTxn"); + using namespace test::jtx; + + Env env{*this}; + Account const alice("alice"); + Account const contract("contract"); + Account const otxn("otxn"); + env.fund(XRP(10000), alice, contract, otxn); + env.close(); + + OpenView ov{*env.current()}; + ApplyContext ac = createApplyContext(env, ov); + + auto contractCtx = createContractContext(ac, contract, otxn); + + ContractHostFunctionsImpl cfs(contractCtx); + + // Test building a Payment transaction + { + auto result = cfs.buildTxn(ttPAYMENT); + BEAST_EXPECT(result.has_value()); + if (result.has_value()) + { + auto txIndex = result.value(); + BEAST_EXPECT(txIndex == 0); + BEAST_EXPECT(contractCtx.built_txns.size() == 1); + + // Verify the transaction has required fields + auto& txn = contractCtx.built_txns[0]; + BEAST_EXPECT(txn.isFieldPresent(sfTransactionType)); + BEAST_EXPECT(txn.getFieldU16(sfTransactionType) == ttPAYMENT); + BEAST_EXPECT(txn.isFieldPresent(sfAccount)); + BEAST_EXPECT(txn.getAccountID(sfAccount) == contract.id()); + BEAST_EXPECT(txn.isFieldPresent(sfSequence)); + BEAST_EXPECT(txn.getFieldU32(sfSequence) == 1); + BEAST_EXPECT(txn.isFieldPresent(sfFee)); + BEAST_EXPECT(txn.getFieldAmount(sfFee) == XRP(0)); + BEAST_EXPECT(txn.isFieldPresent(sfFlags)); + BEAST_EXPECT(txn.getFieldU32(sfFlags) == 536870912); + + // Verify sequence incremented + BEAST_EXPECT(contractCtx.result.nextSequence == 2); + } + } + + // Test building an AccountSet transaction + { + auto result = cfs.buildTxn(ttACCOUNT_SET); + BEAST_EXPECT(result.has_value()); + if (result.has_value()) + { + auto txIndex = result.value(); + BEAST_EXPECT(txIndex == 1); + BEAST_EXPECT(contractCtx.built_txns.size() == 2); + + auto& txn = contractCtx.built_txns[1]; + BEAST_EXPECT(txn.getFieldU16(sfTransactionType) == ttACCOUNT_SET); + BEAST_EXPECT(txn.getFieldU32(sfSequence) == 2); + BEAST_EXPECT(contractCtx.result.nextSequence == 3); + } + } + + // Test building a TrustSet transaction + { + auto result = cfs.buildTxn(ttTRUST_SET); + BEAST_EXPECT(result.has_value()); + if (result.has_value()) + { + auto txIndex = result.value(); + BEAST_EXPECT(txIndex == 2); + BEAST_EXPECT(contractCtx.built_txns.size() == 3); + + auto& txn = contractCtx.built_txns[2]; + BEAST_EXPECT(txn.getFieldU16(sfTransactionType) == ttTRUST_SET); + } + } + + // Test building multiple transactions in sequence + { + auto initialSize = contractCtx.built_txns.size(); + auto initialSeq = contractCtx.result.nextSequence; + + for (int i = 0; i < 5; ++i) + { + auto result = cfs.buildTxn(ttPAYMENT); + BEAST_EXPECT(result.has_value()); + if (result.has_value()) + { + BEAST_EXPECT(result.value() == initialSize + i); + } + } + + BEAST_EXPECT(contractCtx.built_txns.size() == initialSize + 5); + BEAST_EXPECT(contractCtx.result.nextSequence == initialSeq + 5); + } + } + + void + testAddTxnField() + { + testcase("addTxnField"); + using namespace test::jtx; + + Env env{*this}; + Account const alice("alice"); + Account const bob("bob"); + Account const contract("contract"); + Account const otxn("otxn"); + env.fund(XRP(10000), alice, bob, contract, otxn); + env.close(); + + OpenView ov{*env.current()}; + ApplyContext ac = createApplyContext(env, ov); + + auto contractCtx = createContractContext(ac, contract, otxn); + + ContractHostFunctionsImpl cfs(contractCtx); + + // Build a Payment transaction to add fields to + auto buildResult = cfs.buildTxn(ttPAYMENT); + BEAST_EXPECT(buildResult.has_value()); + if (!buildResult.has_value()) + return; + + uint32_t txIndex = buildResult.value(); + + // Test adding Destination (AccountID) with 0x14 prefix + { + AccountID data = bob.id(); + // Prepend the required type byte 0x14 before the 20-byte AccountID + Blob buf; + buf.reserve(1 + data.size()); + buf.push_back(0x14); + buf.insert(buf.end(), data.begin(), data.end()); + auto const s = Slice{buf.data(), buf.size()}; + auto result = cfs.addTxnField(txIndex, sfDestination, s); + BEAST_EXPECT(result.has_value()); + if (result.has_value()) + { + BEAST_EXPECT(result.value() == 0); + auto& txn = contractCtx.built_txns[txIndex]; + BEAST_EXPECT(txn.isFieldPresent(sfDestination)); + BEAST_EXPECT(txn.getAccountID(sfDestination) == bob.id()); + } + } + + // Test adding Amount (STAmount - XRP) + { + STAmount amount{XRP(1000)}; + Serializer s; + amount.add(s); + auto result = cfs.addTxnField(txIndex, sfAmount, s.slice()); + BEAST_EXPECT(result.has_value()); + if (result.has_value()) + { + BEAST_EXPECT(result.value() == 0); + auto& txn = contractCtx.built_txns[txIndex]; + BEAST_EXPECT(txn.isFieldPresent(sfAmount)); + BEAST_EXPECT(txn.getFieldAmount(sfAmount) == amount); + } + } + + // Test adding SendMax (STAmount - IOU) + { + auto const USD = alice["USD"]; + STAmount sendMax{USD.issue(), 500}; + Serializer s; + sendMax.add(s); + auto result = cfs.addTxnField(txIndex, sfSendMax, s.slice()); + BEAST_EXPECT(result.has_value()); + if (result.has_value()) + { + BEAST_EXPECT(result.value() == 0); + auto& txn = contractCtx.built_txns[txIndex]; + BEAST_EXPECT(txn.isFieldPresent(sfSendMax)); + BEAST_EXPECT(txn.getFieldAmount(sfSendMax) == sendMax); + } + } + + // Test adding DestinationTag (UInt32) + { + uint32_t tag = 12345; + Serializer s; + s.add32(tag); + auto result = cfs.addTxnField(txIndex, sfDestinationTag, s.slice()); + BEAST_EXPECT(result.has_value()); + if (result.has_value()) + { + BEAST_EXPECT(result.value() == 0); + auto& txn = contractCtx.built_txns[txIndex]; + BEAST_EXPECT(txn.isFieldPresent(sfDestinationTag)); + BEAST_EXPECT(txn.getFieldU32(sfDestinationTag) == tag); + } + } + + // Build a TrustSet transaction for testing additional fields + auto trustBuildResult = cfs.buildTxn(ttTRUST_SET); + BEAST_EXPECT(trustBuildResult.has_value()); + if (trustBuildResult.has_value()) + { + uint32_t trustIndex = trustBuildResult.value(); + + // Test adding LimitAmount (STAmount for TrustSet) + { + auto const EUR = alice["EUR"]; + STAmount limit{EUR.issue(), 10000}; + Serializer s; + limit.add(s); + auto result = cfs.addTxnField(trustIndex, sfLimitAmount, s.slice()); + BEAST_EXPECT(result.has_value()); + if (result.has_value()) + { + BEAST_EXPECT(result.value() == 0); + auto& txn = contractCtx.built_txns[trustIndex]; + BEAST_EXPECT(txn.isFieldPresent(sfLimitAmount)); + BEAST_EXPECT(txn.getFieldAmount(sfLimitAmount) == limit); + } + } + + // Test adding QualityIn (UInt32) + { + uint32_t quality = 1000000; + Serializer s; + s.add32(quality); + auto result = cfs.addTxnField(trustIndex, sfQualityIn, s.slice()); + BEAST_EXPECT(result.has_value()); + if (result.has_value()) + { + BEAST_EXPECT(result.value() == 0); + auto& txn = contractCtx.built_txns[trustIndex]; + BEAST_EXPECT(txn.isFieldPresent(sfQualityIn)); + BEAST_EXPECT(txn.getFieldU32(sfQualityIn) == quality); + } + } + } + + // Build an AccountSet transaction for testing more fields + auto accSetResult = cfs.buildTxn(ttACCOUNT_SET); + BEAST_EXPECT(accSetResult.has_value()); + if (accSetResult.has_value()) + { + uint32_t accSetIndex = accSetResult.value(); + + // Test adding Domain (Blob/VL) + { + Blob domain = {'e', 'x', 'a', 'm', 'p', 'l', 'e', '.', 'c', 'o', 'm'}; + Serializer s; + s.addVL(domain); + auto result = cfs.addTxnField(accSetIndex, sfDomain, s.slice()); + BEAST_EXPECT(result.has_value()); + if (result.has_value()) + { + BEAST_EXPECT(result.value() == 0); + auto& txn = contractCtx.built_txns[accSetIndex]; + BEAST_EXPECT(txn.isFieldPresent(sfDomain)); + BEAST_EXPECT(txn.getFieldVL(sfDomain) == domain); + } + } + + // Test adding TransferRate (UInt32) + { + uint32_t fee = 500; + Serializer s; + s.add32(fee); + auto result = cfs.addTxnField(accSetIndex, sfTransferRate, s.slice()); + BEAST_EXPECT(result.has_value()); + if (result.has_value()) + { + BEAST_EXPECT(result.value() == 0); + auto& txn = contractCtx.built_txns[accSetIndex]; + BEAST_EXPECT(txn.isFieldPresent(sfTransferRate)); + BEAST_EXPECT(txn.getFieldU32(sfTransferRate) == fee); + } + } + + // Test adding SetFlag (UInt32) + { + uint32_t flag = 8; // asfRequireAuth + Serializer s; + s.add32(flag); + auto result = cfs.addTxnField(accSetIndex, sfSetFlag, s.slice()); + BEAST_EXPECT(result.has_value()); + if (result.has_value()) + { + BEAST_EXPECT(result.value() == 0); + auto& txn = contractCtx.built_txns[accSetIndex]; + BEAST_EXPECT(txn.isFieldPresent(sfSetFlag)); + BEAST_EXPECT(txn.getFieldU32(sfSetFlag) == flag); + } + } + } + + // Test error cases + + // Test adding field to non-existent transaction + { + Serializer s; + s.add32(123); + auto result = cfs.addTxnField(9999, sfDestinationTag, s.slice()); + BEAST_EXPECT(!result.has_value()); + // The implementation will likely throw or return an error + // when accessing out-of-bounds index + } + + // // Test adding invalid field for transaction type + // // Note: The current implementation checks against ttCONTRACT_CALL + // // format which might need adjustment for proper field validation + // { + // auto paymentResult = cfs.buildTxn(ttPAYMENT); + // if (paymentResult.has_value()) + // { + // uint32_t payIndex = paymentResult.value(); + + // // Try to add a field that doesn't belong to Payment + // // This test might need adjustment based on actual field + // // validation + // Serializer s; + // s.add32(100); + // // Using an obscure field that likely isn't in Payment format + // auto result = cfs.addTxnField(payIndex, sfTickSize, + // s.slice()); + // // The result depends on implementation's field validation + // BEAST_EXPECT(result.has_value()); + // } + // } + } + + void + testEmitBuiltTxn() + { + testcase("emitBuiltTxn"); + using namespace test::jtx; + + Env env{*this}; + Account const alice("alice"); + Account const bob("bob"); + Account const carol("carol"); + Account const contract("contract"); + env.fund(XRP(10000), alice, bob, carol, contract); + env.close(); + + OpenView ov{*env.current()}; + ApplyContext ac = createApplyContext(env, ov); + + auto contractCtx = createContractContext(ac, contract, alice); + + ContractHostFunctionsImpl cfs(contractCtx); + + // Test emitting a valid Payment transaction + { + // Build a Payment transaction + auto buildResult = cfs.buildTxn(ttPAYMENT); + BEAST_EXPECT(buildResult.has_value()); + if (!buildResult.has_value()) + return; + + uint32_t txIndex = buildResult.value(); + + // Add required fields for Payment + AccountID destAccount = bob.id(); + Blob destBuf; + destBuf.reserve(1 + destAccount.size()); + destBuf.push_back(0x14); // Type prefix for AccountID + destBuf.insert(destBuf.end(), destAccount.begin(), destAccount.end()); + auto destResult = + cfs.addTxnField(txIndex, sfDestination, Slice{destBuf.data(), destBuf.size()}); + BEAST_EXPECT(destResult.has_value()); + + // Add Amount field + STAmount amount{XRP(100)}; + Serializer amtSerializer; + amount.add(amtSerializer); + auto amtResult = cfs.addTxnField(txIndex, sfAmount, amtSerializer.slice()); + BEAST_EXPECT(amtResult.has_value()); + + // Emit the transaction + auto emitResult = cfs.emitBuiltTxn(txIndex); + BEAST_EXPECT(emitResult.has_value()); + if (emitResult.has_value()) + { + // Check that the transaction was added to emitted transactions + BEAST_EXPECT(contractCtx.result.emittedTxns.size() == 1); + + // The result should be a TER code converted to int + int32_t terCode = emitResult.value(); + // We expect a success code + BEAST_EXPECT(terCode == 0); + } + } + + // // Test emitting multiple transactions + // { + // // Build first transaction - Payment + // auto build1 = cfs.buildTxn(ttPAYMENT); + // BEAST_EXPECT(build1.has_value()); + // if (build1.has_value()) + // { + // uint32_t tx1 = build1.value(); + + // // Add fields for first payment + // AccountID dest1 = alice.id(); + // Blob dest1Buf; + // dest1Buf.reserve(1 + dest1.size()); + // dest1Buf.push_back(0x14); + // dest1Buf.insert(dest1Buf.end(), dest1.begin(), dest1.end()); + // auto const result = cfs.addTxnField( + // tx1, + // sfDestination, + // Slice{dest1Buf.data(), dest1Buf.size()}); + // BEAST_EXPECT(result.has_value()); + + // STAmount amt1{XRP(50)}; + // Serializer amt1Ser; + // amt1.add(amt1Ser); + // cfs.addTxnField(tx1, sfAmount, amt1Ser.slice()); + // } + + // // Build second transaction - Another Payment + // auto build2 = cfs.buildTxn(ttPAYMENT); + // BEAST_EXPECT(build2.has_value()); + // if (build2.has_value()) + // { + // uint32_t tx2 = build2.value(); + + // // Add fields for second payment + // AccountID dest2 = carol.id(); + // Blob dest2Buf; + // dest2Buf.reserve(1 + dest2.size()); + // dest2Buf.push_back(0x14); + // dest2Buf.insert(dest2Buf.end(), dest2.begin(), dest2.end()); + // auto const result = cfs.addTxnField( + // tx2, + // sfDestination, + // Slice{dest2Buf.data(), dest2Buf.size()}); + // BEAST_EXPECT(result.has_value()); + + // STAmount amt2{XRP(75)}; + // Serializer amt2Ser; + // amt2.add(amt2Ser); + // cfs.addTxnField(tx2, sfAmount, amt2Ser.slice()); + // } + + // // Emit both transactions + // if (build1.has_value()) + // { + // auto emit1 = cfs.emitBuiltTxn(build1.value()); + // BEAST_EXPECT(emit1.has_value()); + // } + + // if (build2.has_value()) + // { + // auto emit2 = cfs.emitBuiltTxn(build2.value()); + // BEAST_EXPECT(emit2.has_value()); + // } + + // // Check that both were added to emitted transactions + // // (Note: actual count depends on previous test state) + // BEAST_EXPECT(contractCtx.result.emittedTxns.size() >= 2); + // } + + // // Test emitting transaction with invalid index + // { + // auto emitResult = cfs.emitBuiltTxn(9999); + // BEAST_EXPECT(!emitResult.has_value()); + // if (!emitResult.has_value()) + // { + // BEAST_EXPECT( + // emitResult.error() == + // HostFunctionError::INDEX_OUT_OF_BOUNDS); + // } + // } + + // // Test emitting AccountSet transaction + // { + // auto buildResult = cfs.buildTxn(ttACCOUNT_SET); + // BEAST_EXPECT(buildResult.has_value()); + // if (buildResult.has_value()) + // { + // uint32_t txIndex = buildResult.value(); + + // // Add optional fields for AccountSet + // uint32_t setFlag = 8; // asfRequireAuth + // Serializer flagSer; + // flagSer.add32(setFlag); + // auto flagResult = + // cfs.addTxnField(txIndex, sfSetFlag, flagSer.slice()); + // BEAST_EXPECT(flagResult.has_value()); + + // // Emit the transaction + // auto emitResult = cfs.emitBuiltTxn(txIndex); + // BEAST_EXPECT(emitResult.has_value()); + // } + // } + + // // Test emitting TrustSet transaction + // { + // auto buildResult = cfs.buildTxn(ttTRUST_SET); + // BEAST_EXPECT(buildResult.has_value()); + // if (buildResult.has_value()) + // { + // uint32_t txIndex = buildResult.value(); + + // // Add LimitAmount field (required for TrustSet) + // auto const USD = alice["USD"]; + // STAmount limit{USD.issue(), 1000}; + // Serializer limitSer; + // limit.add(limitSer); + // auto limitResult = + // cfs.addTxnField(txIndex, sfLimitAmount, + // limitSer.slice()); + // BEAST_EXPECT(limitResult.has_value()); + + // // Emit the transaction + // auto emitResult = cfs.emitBuiltTxn(txIndex); + // BEAST_EXPECT(emitResult.has_value()); + // } + // } + + // // Test emitting transaction without required fields + // { + // // Build a Payment but don't add required fields + // auto buildResult = cfs.buildTxn(ttPAYMENT); + // BEAST_EXPECT(buildResult.has_value()); + // if (buildResult.has_value()) + // { + // uint32_t txIndex = buildResult.value(); + + // // Try to emit without Destination and Amount + // auto emitResult = cfs.emitBuiltTxn(txIndex); + // BEAST_EXPECT(emitResult.has_value()); + // if (emitResult.has_value()) + // { + // // Should get an error code indicating missing fields + // int32_t terCode = emitResult.value(); + // // The transaction should fail validation + // BEAST_EXPECT(terCode != + // static_cast(tesSUCCESS)); + // } + // } + // } + + // // Test sequence of build and emit operations + // { + // auto initialEmittedCount = contractCtx.result.emittedTxns.size(); + + // // Build several transactions + // std::vector indices; + // for (int i = 0; i < 3; ++i) + // { + // auto buildResult = cfs.buildTxn(ttACCOUNT_SET); + // BEAST_EXPECT(buildResult.has_value()); + // if (buildResult.has_value()) + // { + // indices.push_back(buildResult.value()); + // } + // } + + // // Emit them in reverse order + // for (auto it = indices.rbegin(); it != indices.rend(); ++it) + // { + // auto emitResult = cfs.emitBuiltTxn(*it); + // BEAST_EXPECT(emitResult.has_value()); + // } + + // // Check that all were emitted + // BEAST_EXPECT( + // contractCtx.result.emittedTxns.size() == + // initialEmittedCount + indices.size()); + // } + } + + void + run() override + { + using namespace test::jtx; + // testInstanceParam(); + // testFunctionParam(); + // testContractDataFromKey(); + // testNestedContractDataFromKey(); + testNestedContractDataFromArrayKey(); + // testBuildTxn(); + // testAddTxnField(); + // testEmitBuiltTxn(); + } +}; + +BEAST_DEFINE_TESTSUITE(ContractHostFuncImpl, app, xrpl); + +} // namespace test +} // namespace xrpl diff --git a/src/test/app/Contract_test.cpp b/src/test/app/Contract_test.cpp new file mode 100644 index 0000000000..ca3cb59974 --- /dev/null +++ b/src/test/app/Contract_test.cpp @@ -0,0 +1,2025 @@ +#include +#include +#include + +#include +#include +#include + +namespace xrpl { +namespace test { + +class Contract_test : public beast::unit_test::suite +{ + struct TestLedgerData + { + int index; + std::string txType; + std::string result; + }; + + Json::Value + getLastLedger(jtx::Env& env) + { + Json::Value params; + params[jss::ledger_index] = env.closed()->seq(); + params[jss::transactions] = true; + params[jss::expand] = true; + return env.rpc("json", "ledger", to_string(params)); + } + + Json::Value + getTxByIndex(Json::Value const& jrr, int const index) + { + for (auto const& txn : jrr[jss::result][jss::ledger][jss::transactions]) + { + if (txn[jss::metaData][sfTransactionIndex.jsonName] == index) + return txn; + } + return {}; + } + + void + validateClosedLedger(jtx::Env& env, std::vector const& ledgerResults) + { + auto const jrr = getLastLedger(env); + auto const transactions = jrr[jss::result][jss::ledger][jss::transactions]; + BEAST_EXPECT(transactions.size() == ledgerResults.size()); + for (TestLedgerData const& ledgerResult : ledgerResults) + { + auto const txn = getTxByIndex(jrr, ledgerResult.index); + BEAST_EXPECT(txn.isMember(jss::metaData)); + Json::Value const meta = txn[jss::metaData]; + BEAST_EXPECT(txn[sfTransactionType.jsonName] == ledgerResult.txType); + BEAST_EXPECT(meta[sfTransactionResult.jsonName] == ledgerResult.result); + } + } + + static std::pair> + contractSourceKeyAndSle(ReadView const& view, uint256 const& contractHash) + { + auto const k = keylet::contractSource(contractHash); + return {k.key, view.read(k)}; + } + + static std::pair> + contractKeyAndSle( + ReadView const& view, + uint256 const& contractHash, + AccountID const& owner, + std::uint32_t const& seq) + { + auto const k = keylet::contract(contractHash, owner, seq); + return {k.key, view.read(k)}; + } + + Json::Value + getContractCreateTx(Json::Value const& jrr) + { + for (auto const& txn : jrr[jss::result][jss::ledger][jss::transactions]) + { + if (txn[jss::TransactionType] == jss::ContractCreate) + return txn; + } + return {}; + } + + uint256 + getContractHash(Blob const& wasmBytes) + { + return xrpl::sha512Half_s(xrpl::Slice(wasmBytes.data(), wasmBytes.size())); + } + + void + validateFunctions(std::shared_ptr const& sle, Json::Value const& functions) + { + auto const stored = sle->getFieldArray(sfFunctions); + BEAST_EXPECT(stored.size() == functions.size()); + for (std::size_t i = 0; i < stored.size(); ++i) + { + auto const sIPV = stored[i].getJson(JsonOptions::none); + auto const& eIPV = functions[i]["Function"]; + + // Compare function name. + BEAST_EXPECT(sIPV.isMember("FunctionName")); + BEAST_EXPECT(eIPV.isMember("FunctionName")); + BEAST_EXPECT(sIPV["FunctionName"].asString() == eIPV["FunctionName"].asString()); + + // Compare parameters if present. + if (eIPV.isMember("Parameters")) + { + BEAST_EXPECT(sIPV.isMember("Parameters")); + BEAST_EXPECT(sIPV["Parameters"].isArray()); + BEAST_EXPECT(eIPV["Parameters"].isArray()); + BEAST_EXPECT(sIPV["Parameters"].size() == eIPV["Parameters"].size()); + + for (std::size_t j = 0; j < sIPV["Parameters"].size(); ++j) + { + auto const& sParam = sIPV["Parameters"][j]; + auto const& eParam = eIPV["Parameters"][j]["Parameter"]; + + // Compare ParameterFlag if present. + if (sParam.isMember("ParameterFlag")) + { + BEAST_EXPECT(eParam.isMember("ParameterFlag")); + BEAST_EXPECT( + sParam["ParameterFlag"].asUInt() == eParam["ParameterFlag"].asUInt()); + } + + // Compare ParameterName if present. + if (sParam.isMember("ParameterName")) + { + BEAST_EXPECT(eParam.isMember("ParameterName")); + BEAST_EXPECT( + sParam["ParameterName"].asString() == + eParam["ParameterName"].asString()); + } + + // Compare ParameterType if present. + if (sParam.isMember("ParameterType")) + { + BEAST_EXPECT(eParam.isMember("ParameterType")); + BEAST_EXPECT( + sParam["ParameterType"]["type"].asString() == + eParam["ParameterType"]["type"].asString()); + } + } + } + } + } + + void + validateInstanceParams( + std::shared_ptr const& sle, + Json::Value const& instanceParamValues) + { + // Convert stored SLE array to JSON and compare against expected JSON. + auto const stored = sle->getFieldArray(sfInstanceParameterValues); + BEAST_EXPECT(stored.size() == instanceParamValues.size()); + + for (std::size_t i = 0; i < stored.size(); ++i) + { + // Convert the STObject entry to JSON for easy comparison. + auto const sIPV = stored[i].getJson(JsonOptions::none); + auto const& eIPV = instanceParamValues[i]["InstanceParameterValue"]; + + // Compare flag if present. + BEAST_EXPECT(sIPV.isMember("ParameterFlag")); + BEAST_EXPECT(eIPV.isMember("ParameterFlag")); + BEAST_EXPECT(sIPV["ParameterFlag"].asUInt() == eIPV["ParameterFlag"].asUInt()); + + // Compare ParameterValue contents (name/type/value) when present. + BEAST_EXPECT(sIPV.isMember("ParameterValue")); + BEAST_EXPECT(eIPV.isMember("ParameterValue")); + auto const& sPV = sIPV["ParameterValue"]; + auto const& ePV = eIPV["ParameterValue"]; + + if (ePV.isMember("name")) + BEAST_EXPECT( + sPV.isMember("name") && sPV["name"].asString() == ePV["name"].asString()); + + if (ePV.isMember("type")) + BEAST_EXPECT( + sPV.isMember("type") && sPV["type"].asString() == ePV["type"].asString()); + + if (ePV.isMember("value")) + { + // value can be number, string, or object; compare generically + BEAST_EXPECT(sPV.isMember("value")); + BEAST_EXPECT(sPV["value"] == ePV["value"]); + } + } + } + + void + validateInstanceParamValues( + std::shared_ptr const& sle, + Json::Value const& instanceParamValues) + { + // Convert stored SLE array to JSON and compare against expected JSON. + auto const stored = sle->getFieldArray(sfInstanceParameterValues); + BEAST_EXPECT(stored.size() == instanceParamValues.size()); + + for (std::size_t i = 0; i < stored.size(); ++i) + { + // Convert the STObject entry to JSON for easy comparison. + auto const sIPV = stored[i].getJson(JsonOptions::none); + auto const& eIPV = instanceParamValues[i]["InstanceParameterValue"]; + + // Compare flag if present. + BEAST_EXPECT(sIPV.isMember("ParameterFlag")); + BEAST_EXPECT(eIPV.isMember("ParameterFlag")); + BEAST_EXPECT(sIPV["ParameterFlag"].asUInt() == eIPV["ParameterFlag"].asUInt()); + + // Compare ParameterValue contents (name/type/value) when present. + BEAST_EXPECT(sIPV.isMember("ParameterValue")); + BEAST_EXPECT(eIPV.isMember("ParameterValue")); + auto const& sPV = sIPV["ParameterValue"]; + auto const& ePV = eIPV["ParameterValue"]; + + if (ePV.isMember("type")) + BEAST_EXPECT( + sPV.isMember("type") && sPV["type"].asString() == ePV["type"].asString()); + + if (ePV.isMember("value")) + { + // value can be number, string, or object; compare generically + BEAST_EXPECT(sPV.isMember("value")); + BEAST_EXPECT(sPV["value"] == ePV["value"]); + } + } + } + + void + validateContract( + jtx::Env& env, + Keylet const& k, + AccountID const& contractAccount, + AccountID const& owner, + std::uint32_t const& flags, + std::uint32_t const& seq, + uint256 const& contractHash, + std::optional const& instanceParamValues = std::nullopt, + std::optional const& uri = std::nullopt) + { + auto const sle = env.current()->read(k); + if (!sle) + { + fail(); + return; + } + BEAST_EXPECT(sle); + BEAST_EXPECT(sle->getAccountID(sfContractAccount) == contractAccount); + BEAST_EXPECT(sle->getAccountID(sfOwner) == owner); + BEAST_EXPECT(sle->getFieldU32(sfFlags) == flags); + BEAST_EXPECT(sle->getFieldU32(sfSequence) == seq); + BEAST_EXPECT(sle->getFieldH256(sfContractHash) == contractHash); + // if (instanceParamValues) + // validateInstanceParamValues(sle, *instanceParamValues); + // if (uri) + // { + // std::cout << "URI: " << *uri << std::endl; + // BEAST_EXPECT(sle->getFieldVL(sfURI) == strUnHex(*uri)); + // } + } + + void + validateContractSource( + jtx::Env& env, + Blob const& wasmBytes, + uint256 const& contractHash, + std::uint64_t const& referenceCount, + Json::Value const& functions, + std::optional const& instanceParams = std::nullopt) + { + auto const [id, sle] = contractSourceKeyAndSle(*env.current(), contractHash); + BEAST_EXPECT(sle); + BEAST_EXPECT(sle->getFieldVL(sfContractCode) == wasmBytes); + BEAST_EXPECT(sle->getFieldH256(sfContractHash) == contractHash); + BEAST_EXPECT(sle->getFieldU64(sfReferenceCount) == referenceCount); + validateFunctions(sle, functions); + } + + template + std::tuple + setContract(jtx::Env& env, TER const& result, Args&&... args) + { + auto jt = env.jt(std::forward(args)...); + env(jt, jtx::ter(result)); + env.close(); + + // { + // Json::Value params; + // params[jss::ledger_index] = env.current()->seq() - 1; + // params[jss::transactions] = true; + // params[jss::expand] = true; + // auto const jrr = env.rpc("json", "ledger", to_string(params)); + // std::cout << jrr << std::endl; + // } + + // if (jt.jv.isMember(sfContractHash.jsonName)) + // { + // auto const accountID = + // parseBase58(jt.jv[sfContractAccount].asString()); + // jtx::Account const contractAccount{ + // "Contract pseudo-account", + // *accountID}; + // return std::make_pair(contractAccount, + // uint256(jt.jv[sfContractHash])); + // } + + auto const wasmBytes = strUnHex(jt.jv[sfContractCode.jsonName].asString()); + // std::cout << "WASM Size: " << wasmBytes->size() << std::endl; + uint256 const contractHash = + xrpl::sha512Half_s(xrpl::Slice(wasmBytes->data(), wasmBytes->size())); + auto const accountID = parseBase58(jt.jv[sfAccount].asString()); + auto const [contractKey, sle] = contractKeyAndSle( + *env.current(), contractHash, *accountID, jt.jv[sfSequence.jsonName].asUInt()); + jtx::Account const contractAccount{ + "Contract pseudo-account", sle->getAccountID(sfContractAccount)}; + return std::make_tuple(contractAccount, contractHash, jt.jv); + } + + std::string const BaseContractWasm = + "0061736D01000000010E0260057F7F7F7F7F017F6000017F02120108686F73745F" + "6C696205747261636500000302010105030100110619037F01418080C0000B7F00" + "419E80C0000B7F0041A080C0000B072C04066D656D6F7279020004626173650001" + "0A5F5F646174615F656E6403010B5F5F686561705F6261736503020A6C016A0101" + "7F23808080800041206B2200248080808000200041186A410028009080C0800036" + "0200200041106A410029008880C080003703002000410029008080C08000370308" + "419480C08000410A200041086A411441011080808080001A200041206A24808080" + "800041000B0B270100418080C0000B1EAE123A8556F3CF91154711376AFB0F894F" + "832B3D20204163636F756E743A"; + + std::string const Base2ContractWasm = + "0061736D01000000010E0260057F7F7F7F7F017F6000017F02120108686F73745F6C69" + "6205747261636500000302010105030100110619037F01418080C0000B7F0041A380C0" + "000B7F0041B080C0000B072C04066D656D6F72790200046261736500010A5F5F646174" + "615F656E6403010B5F5F686561705F6261736503020A1B011900418080C08000412341" + "00410041001080808080001A41000B0B2C0100418080C0000B23242424242420535441" + "5254494E47204241534520455845435554494F4E202424242424"; + + void + testCreatePreflight(FeatureBitset features) + { + testcase("create preflight"); + + using namespace jtx; + + // temDISABLED: Feature not enabled + { + test::jtx::Env env{*this, features - featureSmartContract}; + + auto const alice = Account{"alice"}; + env.fund(XRP(10'000), alice); + env.close(); + + env(contract::create(alice, BaseContractWasm), ter(temDISABLED)); + } + + // temINVALID_FLAG: tfContractMask is not allowed. + { + test::jtx::Env env{*this, features}; + + auto const alice = Account{"alice"}; + env.fund(XRP(10'000), alice); + env.close(); + + env(contract::create(alice, BaseContractWasm), + txflags(tfBurnable), + ter(temINVALID_FLAG)); + } + + // temMALFORMED: Neither ContractCode nor ContractHash present + { + test::jtx::Env env{*this, features}; + + auto const alice = Account{"alice"}; + env.fund(XRP(10'000), alice); + env.close(); + + Json::Value jv; + jv[jss::TransactionType] = jss::ContractCreate; + jv[jss::Account] = alice.human(); + jv[jss::Fee] = to_string(XRP(10).value()); + // Missing both ContractCode and ContractHash + + env(jv, ter(temMALFORMED)); + } + + // temMALFORMED: Both ContractCode and ContractHash present + { + test::jtx::Env env{*this, features}; + + auto const alice = Account{"alice"}; + env.fund(XRP(10'000), alice); + env.close(); + + Json::Value jv; + jv[jss::TransactionType] = jss::ContractCreate; + jv[jss::Account] = alice.human(); + jv[jss::Fee] = to_string(XRP(10).value()); + jv[sfContractCode.jsonName] = BaseContractWasm; + jv[sfContractHash.jsonName] = + "D955DAC2E77519F05AD151A5D3C99FC8125FB39D58FF9F106F1ACA4491902C" + "25"; + + env(jv, ter(temMALFORMED)); + } + + // temARRAY_EMPTY: ContractCode present but Functions missing + { + test::jtx::Env env{*this, features}; + + auto const alice = Account{"alice"}; + env.fund(XRP(10'000), alice); + env.close(); + + Json::Value jv; + jv[jss::TransactionType] = jss::ContractCreate; + jv[jss::Account] = alice.human(); + jv[jss::Fee] = to_string(XRP(10).value()); + jv[sfContractCode.jsonName] = BaseContractWasm; + // Missing Functions array + + env(jv, ter(temARRAY_EMPTY)); + } + + // temARRAY_EMPTY: ContractCode present but Functions missing + { + test::jtx::Env env{*this, features}; + + auto const alice = Account{"alice"}; + env.fund(XRP(10'000), alice); + env.close(); + + Json::Value jv; + jv[jss::TransactionType] = jss::ContractCreate; + jv[jss::Account] = alice.human(); + jv[jss::Fee] = to_string(XRP(10).value()); + jv[sfContractCode.jsonName] = BaseContractWasm; + jv[sfFunctions.jsonName] = Json::arrayValue; // Empty array + + env(jv, ter(temARRAY_EMPTY)); + } + + // temARRAY_TOO_LARGE: Functions array too large + { + test::jtx::Env env{*this, features}; + + auto const alice = Account{"alice"}; + env.fund(XRP(10'000), alice); + env.close(); + + env(contract::create(alice, BaseContractWasm), + contract::add_function("func1", {}), + contract::add_function("func2", {}), + contract::add_function("func3", {}), + contract::add_function("func4", {}), + contract::add_function("func5", {}), + contract::add_function("func6", {}), + contract::add_function("func7", {}), + contract::add_function("func8", {}), + contract::add_function("func9", {}), + contract::add_function("func10", {}), + contract::add_function("func11", {}), + contract::add_function("func12", {}), + contract::add_function("func13", {}), + ter(temARRAY_TOO_LARGE)); + } + + // temREDUNDANT: Duplicate function name + { + test::jtx::Env env{*this, features}; + + auto const alice = Account{"alice"}; + env.fund(XRP(10'000), alice); + env.close(); + + env(contract::create(alice, BaseContractWasm), + contract::add_function("test", {}), + contract::add_function("test", {}), // Duplicate + ter(temREDUNDANT)); + } + + // temARRAY_TOO_LARGE: Function Parameters array too large + { + test::jtx::Env env{*this, features}; + + auto const alice = Account{"alice"}; + env.fund(XRP(10'000), alice); + env.close(); + + env(contract::create(alice, BaseContractWasm), + contract::add_function( + "test", + { + {0, "param1", "UINT8"}, {0, "param2", "UINT8"}, + {0, "param3", "UINT8"}, {0, "param4", "UINT8"}, + {0, "param5", "UINT8"}, {0, "param6", "UINT8"}, + {0, "param7", "UINT8"}, {0, "param8", "UINT8"}, + {0, "param9", "UINT8"}, {0, "param10", "UINT8"}, + {0, "param11", "UINT8"}, {0, "param12", "UINT8"}, + {0, "param13", "UINT8"}, {0, "param14", "UINT8"}, + {0, "param15", "UINT8"}, {0, "param16", "UINT8"}, + {0, "param17", "UINT8"}, {0, "param18", "UINT8"}, + {0, "param19", "UINT8"}, {0, "param20", "UINT8"}, + {0, "param21", "UINT8"}, {0, "param22", "UINT8"}, + {0, "param23", "UINT8"}, {0, "param24", "UINT8"}, + {0, "param25", "UINT8"}, {0, "param26", "UINT8"}, + {0, "param27", "UINT8"}, {0, "param28", "UINT8"}, + {0, "param29", "UINT8"}, {0, "param30", "UINT8"}, + {0, "param31", "UINT8"}, {0, "param32", "UINT8"}, + {0, "param33", "UINT8"}, // 33rd parameter + }), + ter(temARRAY_TOO_LARGE)); + } + + // temMALFORMED: Function Parameter is missing flag + { + test::jtx::Env env{*this, features}; + + auto const alice = Account{"alice"}; + env.fund(XRP(10'000), alice); + env.close(); + + Json::Value jv; + jv[jss::TransactionType] = jss::ContractCreate; + jv[jss::Account] = alice.human(); + jv[jss::Fee] = to_string(XRP(10).value()); + jv[sfContractCode.jsonName] = BaseContractWasm; + jv[sfFunctions.jsonName] = Json::arrayValue; + + Json::Value func; + func[sfFunction.jsonName][sfFunctionName.jsonName] = strHex(std::string("test")); + func[sfFunction.jsonName][sfParameters.jsonName] = Json::arrayValue; + + Json::Value param; + param[sfParameter.jsonName][sfParameterType.jsonName]["type"] = "UINT8"; + func[sfFunction.jsonName][sfParameters.jsonName].append(param); + + jv[sfFunctions.jsonName].append(func); + env(jv, ter(temMALFORMED)); + } + + // temMALFORMED: Function Parameter is missing type + { + test::jtx::Env env{*this, features}; + + auto const alice = Account{"alice"}; + env.fund(XRP(10'000), alice); + env.close(); + + Json::Value jv; + jv[jss::TransactionType] = jss::ContractCreate; + jv[jss::Account] = alice.human(); + jv[jss::Fee] = to_string(XRP(10).value()); + jv[sfContractCode.jsonName] = BaseContractWasm; + jv[sfFunctions.jsonName] = Json::arrayValue; + + Json::Value func; + func[sfFunction.jsonName][sfFunctionName.jsonName] = strHex(std::string("test")); + func[sfFunction.jsonName][sfParameters.jsonName] = Json::arrayValue; + + Json::Value param; + param[sfParameter.jsonName][sfParameterFlag.jsonName] = 0; + // Missing sfParameterType + func[sfFunction.jsonName][sfParameters.jsonName].append(param); + + jv[sfFunctions.jsonName].append(func); + env(jv, ter(temMALFORMED)); + } + + // temINVALID_FLAG: Invalid parameter flag in Function. + { + test::jtx::Env env{*this, features}; + + auto const alice = Account{"alice"}; + env.fund(XRP(10'000), alice); + env.close(); + + env(contract::create(alice, BaseContractWasm), + contract::add_function("test", {{0xFF000000, "param", "UINT8"}}), // Invalid flag + ter(temINVALID_FLAG)); + } + + // temARRAY_EMPTY: InstanceParameters empty array + { + test::jtx::Env env{*this, features}; + + auto const alice = Account{"alice"}; + env.fund(XRP(10'000), alice); + env.close(); + + Json::Value jv; + jv[jss::TransactionType] = jss::ContractCreate; + jv[jss::Account] = alice.human(); + jv[jss::Fee] = to_string(XRP(10).value()); + jv[sfContractCode.jsonName] = BaseContractWasm; + jv[sfFunctions.jsonName] = Json::arrayValue; + Json::Value func; + func[sfFunction.jsonName][sfFunctionName.jsonName] = strHex(std::string("test")); + jv[sfFunctions.jsonName].append(func); + jv[sfInstanceParameters.jsonName] = Json::arrayValue; // Empty array + + env(jv, ter(temARRAY_EMPTY)); + } + + // temARRAY_TOO_LARGE: InstanceParameters array is too large + { + test::jtx::Env env{*this, features}; + + auto const alice = Account{"alice"}; + env.fund(XRP(10'000), alice); + env.close(); + + env(contract::create(alice, BaseContractWasm), + contract::add_function("test", {}), + contract::add_instance_param(0, "param1", "UINT8", 1), + contract::add_instance_param(0, "param2", "UINT8", 2), + contract::add_instance_param(0, "param3", "UINT8", 3), + contract::add_instance_param(0, "param4", "UINT8", 4), + contract::add_instance_param(0, "param5", "UINT8", 5), + contract::add_instance_param(0, "param6", "UINT8", 6), + contract::add_instance_param(0, "param7", "UINT8", 7), + contract::add_instance_param(0, "param8", "UINT8", 8), + contract::add_instance_param(0, "param9", "UINT8", 9), + contract::add_instance_param(0, "param10", "UINT8", 10), + contract::add_instance_param(0, "param11", "UINT8", 11), + contract::add_instance_param(0, "param12", "UINT8", 12), + contract::add_instance_param(0, "param13", "UINT8", 13), + contract::add_instance_param(0, "param14", "UINT8", 14), + contract::add_instance_param(0, "param15", "UINT8", 15), + contract::add_instance_param(0, "param16", "UINT8", 16), + contract::add_instance_param(0, "param17", "UINT8", 17), + contract::add_instance_param(0, "param18", "UINT8", 18), + contract::add_instance_param(0, "param19", "UINT8", 19), + contract::add_instance_param(0, "param20", "UINT8", 20), + contract::add_instance_param(0, "param21", "UINT8", 21), + contract::add_instance_param(0, "param22", "UINT8", 22), + contract::add_instance_param(0, "param23", "UINT8", 23), + contract::add_instance_param(0, "param24", "UINT8", 24), + contract::add_instance_param(0, "param25", "UINT8", 25), + contract::add_instance_param(0, "param26", "UINT8", 26), + contract::add_instance_param(0, "param27", "UINT8", 27), + contract::add_instance_param(0, "param28", "UINT8", 28), + contract::add_instance_param(0, "param29", "UINT8", 29), + contract::add_instance_param(0, "param30", "UINT8", 30), + contract::add_instance_param(0, "param31", "UINT8", 31), + contract::add_instance_param(0, "param32", "UINT8", 32), + contract::add_instance_param(0, "param33", "UINT8", 33), + ter(temARRAY_TOO_LARGE)); + } + + // temARRAY_EMPTY: InstanceParameterValues is missing + { + test::jtx::Env env{*this, features}; + + auto const alice = Account{"alice"}; + env.fund(XRP(10'000), alice); + env.close(); + + Json::Value jv; + jv[jss::TransactionType] = jss::ContractCreate; + jv[jss::Account] = alice.human(); + jv[jss::Fee] = to_string(XRP(10).value()); + jv[sfContractCode.jsonName] = BaseContractWasm; + jv[sfFunctions.jsonName] = Json::arrayValue; + Json::Value func; + func[sfFunction.jsonName][sfFunctionName.jsonName] = strHex(std::string("test")); + jv[sfFunctions.jsonName].append(func); + jv[sfInstanceParameterValues.jsonName] = Json::arrayValue; // Empty array + + env(jv, ter(temARRAY_EMPTY)); + } + + // // Test 18: InstanceParameterValues array is too large. + // { + // test::jtx::Env env{*this, features}; + + // auto const alice = Account{"alice"}; + // env.fund(XRP(10'000), alice); + // env.close(); + + // Json::Value jv; + // jv[jss::TransactionType] = jss::ContractCreate; + // jv[jss::Account] = alice.human(); + // jv[jss::Fee] = to_string(XRP(10).value()); + // jv[sfContractCode.jsonName] = BaseContractWasm; + // jv[sfFunctions.jsonName] = Json::arrayValue; + // Json::Value func; + // func[sfFunction.jsonName][sfFunctionName.jsonName] = "test"; + // func[sfFunction.jsonName][sfParameters.jsonName] = + // Json::arrayValue; jv[sfFunctions.jsonName].append(func); + // jv[sfInstanceParameterValues.jsonName] = Json::arrayValue; + + // // Add more than maxContractParams + // for (int i = 0; i < 257; ++i) + // { + // Json::Value param; + // param[sfInstanceParameterValue.jsonName] + // [sfParameterFlag.jsonName] = 0; + // param[sfInstanceParameterValue.jsonName] + // [sfParameterValue.jsonName]["type"] = "UINT8"; + // param[sfInstanceParameterValue.jsonName] + // [sfParameterValue.jsonName]["value"] = i; + // jv[sfInstanceParameterValues.jsonName].append(param); + // } + + // env(jv, ter(temARRAY_TOO_LARGE)); + // } + + // // Test 19: InstanceParameterValue missing flag + // { + // test::jtx::Env env{*this, features}; + + // auto const alice = Account{"alice"}; + // env.fund(XRP(10'000), alice); + // env.close(); + + // Json::Value jv; + // jv[jss::TransactionType] = jss::ContractCreate; + // jv[jss::Account] = alice.human(); + // jv[jss::Fee] = to_string(XRP(10).value()); + // jv[sfContractCode.jsonName] = BaseContractWasm; + // jv[sfFunctions.jsonName] = Json::arrayValue; + // Json::Value func; + // func[sfFunction.jsonName][sfFunctionName.jsonName] = "test"; + // func[sfFunction.jsonName][sfParameters.jsonName] = + // Json::arrayValue; jv[sfFunctions.jsonName].append(func); + // jv[sfInstanceParameterValues.jsonName] = Json::arrayValue; + + // Json::Value param; + // // Missing sfParameterFlag + // param[sfInstanceParameterValue.jsonName][sfParameterValue.jsonName] + // ["type"] = "UINT8"; + // param[sfInstanceParameterValue.jsonName][sfParameterValue.jsonName] + // ["value"] = 1; + // jv[sfInstanceParameterValues.jsonName].append(param); + + // env(jv, ter(temMALFORMED)); + // } + + // // Test 20: InstanceParameterValue missing value + // { + // test::jtx::Env env{*this, features}; + + // auto const alice = Account{"alice"}; + // env.fund(XRP(10'000), alice); + // env.close(); + + // Json::Value jv; + // jv[jss::TransactionType] = jss::ContractCreate; + // jv[jss::Account] = alice.human(); + // jv[jss::Fee] = to_string(XRP(10).value()); + // jv[sfContractCode.jsonName] = BaseContractWasm; + // jv[sfFunctions.jsonName] = Json::arrayValue; + // Json::Value func; + // func[sfFunction.jsonName][sfFunctionName.jsonName] = "test"; + // func[sfFunction.jsonName][sfParameters.jsonName] = + // Json::arrayValue; jv[sfFunctions.jsonName].append(func); + // jv[sfInstanceParameterValues.jsonName] = Json::arrayValue; + + // Json::Value param; + // param[sfInstanceParameterValue.jsonName][sfParameterFlag.jsonName] + // = + // 0; + // // Missing sfParameterValue + // jv[sfInstanceParameterValues.jsonName].append(param); + + // env(jv, ter(temMALFORMED)); + // } + + // // Test 21: InstanceParameterValue invalid flag + // { + // test::jtx::Env env{*this, features}; + + // auto const alice = Account{"alice"}; + // env.fund(XRP(10'000), alice); + // env.close(); + + // env(contract::create(alice, BaseContractWasm), + // contract::add_function("test", {}), + // contract::add_instance_param( + // 0xFF000000, "param", "UINT8", 1), // Invalid flag + // ter(temINVALID_FLAG)); + // } + + // // Test 22: Success - ContractCode with Functions + // { + // test::jtx::Env env{*this, features}; + + // auto const alice = Account{"alice"}; + // env.fund(XRP(10'000), alice); + // env.close(); + + // env(contract::create(alice, BaseContractWasm), + // contract::add_function("base", {}), + // fee(XRP(200)), + // ter(tesSUCCESS)); + // } + + // // Test 23: Success - ContractCode with Functions and parameters + // { + // test::jtx::Env env{*this, features}; + + // auto const alice = Account{"alice"}; + // env.fund(XRP(10'000), alice); + // env.close(); + + // env(contract::create(alice, BaseContractWasm), + // contract::add_function( + // "base", {{0, "param1", "UINT8"}, {0, "param2", + // "UINT32"}}), + // fee(XRP(200)), + // ter(tesSUCCESS)); + // } + + // // Test 24: Success - with InstanceParameters and + // // InstanceParameterValues + // { + // test::jtx::Env env{*this, features}; + + // auto const alice = Account{"alice"}; + // env.fund(XRP(10'000), alice); + // env.close(); + + // env(contract::create(alice, BaseContractWasm), + // contract::add_instance_param(0, "uint8", "UINT8", 1), + // contract::add_instance_param(0, "uint32", "UINT32", 100), + // contract::add_function("base", {}), + // fee(XRP(200)), + // ter(tesSUCCESS)); + // } + } + + void + testCreatePreclaim(FeatureBitset features) + { + testcase("create preclaim"); + + using namespace jtx; + + // temMALFORMED: ContractHash provided but no corresponding + // ContractSource exists + { + test::jtx::Env env{*this, features}; + + auto const alice = Account{"alice"}; + env.fund(XRP(10'000), alice); + env.close(); + + env(contract::create( + alice, + uint256{"D955DAC2E77519F05AD151A5D3C99FC8125FB39D58FF9F106F" + "1ACA4491902C25"}), + fee(XRP(200)), + ter(temMALFORMED)); + } + + // temMALFORMED: ContractCode provided is empty + { + test::jtx::Env env{*this, features}; + + auto const alice = Account{"alice"}; + env.fund(XRP(10'000), alice); + env.close(); + + env(contract::create(alice, ""), // Empty code + contract::add_function("test", {}), + fee(XRP(200)), + ter(temMALFORMED)); + } + + // tesSUCCESS: ContractCode provided, ContractSource doesn't exist yet + // (first create) + { + test::jtx::Env env{*this, features}; + + auto const alice = Account{"alice"}; + env.fund(XRP(10'000), alice); + env.close(); + + env(contract::create(alice, BaseContractWasm), + contract::add_function("base", {}), + fee(XRP(200)), + ter(tesSUCCESS)); + + // TODO: Validate + } + + // tesSUCCESS: ContractCode provided, ContractSource already exists + // (install) + { + test::jtx::Env env{*this, features}; + + auto const alice = Account{"alice"}; + env.fund(XRP(10'000), alice); + env.close(); + + // First create + env(contract::create(alice, BaseContractWasm), + contract::add_instance_param(0, "uint8", "UINT8", 1), + contract::add_function("base", {}), + fee(XRP(200)), + ter(tesSUCCESS)); + env.close(); + + // Second create with same code (install) + env(contract::create(alice, BaseContractWasm), + contract::add_instance_param(0, "uint8", "UINT8", 2), // Different value + contract::add_function("base", {}), + fee(XRP(200)), + ter(tesSUCCESS)); + + // TODO: Validate + } + + // tesSUCCESS: ContractHash provided with valid ContractSource + { + test::jtx::Env env{*this, features}; + + auto const alice = Account{"alice"}; + env.fund(XRP(10'000), alice); + env.close(); + + // First create to establish ContractSource + env(contract::create(alice, BaseContractWasm), + contract::add_instance_param(0, "uint8", "UINT8", 1), + contract::add_function("base", {}), + fee(XRP(200)), + ter(tesSUCCESS)); + env.close(); + + // Get the hash of the contract + auto const wasmBytes = strUnHex(BaseContractWasm); + uint256 const contractHash = getContractHash(*wasmBytes); + + // Install using ContractHash + env(contract::create(alice, contractHash), + contract::add_instance_param(0, "uint8", "UINT8", 2), + fee(XRP(200)), + ter(tesSUCCESS)); + + // TODO: Validate + } + + // temMALFORMED: Install with InstanceParameterValues that don't + // match + // ContractSource + { + test::jtx::Env env{*this, features}; + + auto const alice = Account{"alice"}; + env.fund(XRP(10'000), alice); + env.close(); + + // First create with specific InstanceParameters + env(contract::create(alice, BaseContractWasm), + contract::add_instance_param(0, "uint8", "UINT8", 1), + contract::add_instance_param(0, "uint32", "UINT32", 100), + contract::add_function("base", {}), + fee(XRP(200)), + ter(tesSUCCESS)); + env.close(); + + // Get the hash + auto const wasmBytes = strUnHex(BaseContractWasm); + uint256 const contractHash = getContractHash(*wasmBytes); + + // Try to install with mismatched InstanceParameterValues + // Only providing one parameter when ContractSource expects two + env(contract::create(alice, contractHash), + contract::add_instance_param(0, "uint8", "UINT8", 2), + fee(XRP(200)), + ter(temMALFORMED)); + } + + // temMALFORMED: ContractHash provided but ContractSource doesn't + // exist + // (should fail in preclaim) + { + test::jtx::Env env{*this, features}; + + auto const alice = Account{"alice"}; + env.fund(XRP(10'000), alice); + env.close(); + + // Use Base2ContractWasm hash which hasn't been created yet + auto const wasmBytes = strUnHex(Base2ContractWasm); + uint256 const contractHash = getContractHash(*wasmBytes); + + env(contract::create(alice, contractHash), fee(XRP(200)), ter(temMALFORMED)); + } + + // tesSUCCESS: ContractCode with InstanceParameters for first + // creation + { + test::jtx::Env env{*this, features}; + + auto const alice = Account{"alice"}; + env.fund(XRP(10'000), alice); + env.close(); + + env(contract::create(alice, Base2ContractWasm), + contract::add_instance_param(0, "uint8", "UINT8", 255), + contract::add_instance_param(tfSendAmount, "amount", "AMOUNT", XRP(100)), + contract::add_function("base", {}), + fee(XRP(200)), + ter(tesSUCCESS)); + } + + // tesSUCCESS: Multiple installs of same contract + { + test::jtx::Env env{*this, features}; + + auto const alice = Account{"alice"}; + auto const bob = Account{"bob"}; + env.fund(XRP(10'000), alice, bob); + env.close(); + + // Alice creates first instance + env(contract::create(alice, BaseContractWasm), + contract::add_instance_param(0, "uint8", "UINT8", 1), + contract::add_function("base", {}), + fee(XRP(200)), + ter(tesSUCCESS)); + env.close(); + + // Bob installs same contract + env(contract::create(bob, BaseContractWasm), + contract::add_instance_param(0, "uint8", "UINT8", 2), + contract::add_function("base", {}), + fee(XRP(200)), + ter(tesSUCCESS)); + env.close(); + + // Alice installs another instance + env(contract::create(alice, BaseContractWasm), + contract::add_instance_param(0, "uint8", "UINT8", 3), + contract::add_function("base", {}), + fee(XRP(200)), + ter(tesSUCCESS)); + } + } + + void + testCreateDoApply(FeatureBitset features) + { + testcase("create doApply"); + + using namespace jtx; + + //---------------------------------------------------------------------- + // doApply.ContractCode.tesSUCCESS + + { + test::jtx::Env env{*this, features}; + + auto const alice = Account{"alice"}; + env.fund(XRP(10'000), alice); + env.close(); + + // auto const seq = env.current()->seq(); + auto const [contractAccount, contractHash, jv] = setContract( + env, + tesSUCCESS, + contract::create(alice, BaseContractWasm), + contract::add_instance_param(0, "uint8", "UINT8", 1), + contract::add_function("base", {{0, "uint8", "UINT8"}}), + token::uri("https://example.com/contract"), + fee(XRP(200))); + + // validate contract + // validateContract( + // env, + // contractAccount.id(), + // alice.id(), + // 0, + // seq, + // contractHash, + // jv[sfInstanceParameterValues], + // to_string(jv[sfURI])); + + // validate contract source + // validateContractSource( + // env, *wasmBytes, contractHash, 1, jv[sfFunctions]); + } + + //---------------------------------------------------------------------- + // doApply.ContractHash.tesSUCCESS + + { + test::jtx::Env env{*this, features}; + + auto const alice = Account{"alice"}; + env.fund(XRP(10'000), alice); + env.close(); + + // auto const wasmBytes = strUnHex(BaseContractWasm); + // uint256 const contractHash = getContractHash(*wasmBytes); + + // Create Contract. + { + // auto const seq = env.current()->seq(); + auto const [contractAccount, contractHash, jv] = setContract( + env, + tesSUCCESS, + contract::create(alice, BaseContractWasm), + contract::add_instance_param(0, "uint8", "UINT8", 1), + contract::add_function("base", {{0, "uint8", "UINT8"}}), + fee(XRP(200))); + + // validate contract + // validateContract( + // env, + // contractAccount.id(), + // alice.id(), + // 0, + // seq, + // contractHash, + // jv[sfInstanceParameterValues]); + + // validate contract source + // validateContractSource( + // env, *wasmBytes, contractHash, 1, jv[sfFunctions]); + } + + // Install Contract. + { + // auto const seq = env.current()->seq(); + auto const [contractAccount, contractHash, jv] = setContract( + env, + tesSUCCESS, + contract::create(alice, BaseContractWasm), + contract::add_instance_param(0, "uint8", "UINT8", 1), + contract::add_function("base", {{0, "uint8", "UINT8"}}), + fee(XRP(200))); + + // validate contract + // validateContract( + // env, + // contractAccount.id(), + // alice.id(), + // 0, + // seq, + // contractHash, + // jv[sfInstanceParameterValues]); + + // validate contract source + // validateContractSource( + // env, *wasmBytes, contractHash, 2, jv[sfFunctions]); + } + } + } + + void + testModifyDoApply(FeatureBitset features) + { + testcase("modify doApply"); + + using namespace jtx; + + //---------------------------------------------------------------------- + // doApply.ContractCode.tesSUCCESS + + { + test::jtx::Env env{*this, features}; + + jtx::Account const alice = Account{"alice"}; + env.fund(XRP(10'000), alice); + env.close(); + + // Create initial contract + auto const seq = env.seq(alice); + auto const [contractAccount, contractHash, jv] = setContract( + env, + tesSUCCESS, + contract::create(alice, BaseContractWasm), + contract::add_instance_param(0, "uint8", "UINT8", 1), + contract::add_function("base", {{0, "uint8", "UINT8"}}), + fee(XRP(200))); + + // Modify contract + auto jt = env.jt( + contract::modify(alice, contractAccount, Base2ContractWasm), + contract::add_instance_param(0, "uint16", "UINT16", 1), + contract::add_function("base", {{0, "uint16", "UINT16"}}), + fee(XRP(200))); + env(jt, ter(tesSUCCESS)); + env.close(); + + // old contract source is deleted + auto const [sourceKey, sourceSle] = + contractSourceKeyAndSle(*env.current(), contractHash); + BEAST_EXPECT(!sourceSle); + + // new contract source exists + auto const wasmBytes = strUnHex(Base2ContractWasm); + uint256 const newContractHash = + xrpl::sha512Half_s(xrpl::Slice(wasmBytes->data(), wasmBytes->size())); + auto const [contractKey, contractSle] = + contractSourceKeyAndSle(*env.current(), newContractHash); + BEAST_EXPECT(contractSle); + + // validate modified contract + auto const k = keylet::contract(contractHash, alice, seq); + validateContract( + env, + k, + contractAccount.id(), + alice.id(), + 0, + seq, + newContractHash, + jt.jv[sfInstanceParameterValues]); + } + + //---------------------------------------------------------------------- + // doApply.ContractHash.tesSUCCESS + + { + test::jtx::Env env{*this, features}; + + jtx::Account const alice = Account{"alice"}; + env.fund(XRP(10'000), alice); + env.close(); + + // Create initial contract + // auto const seq = env.seq(alice); + auto const [contractAccount, contractHash, jv] = setContract( + env, + tesSUCCESS, + contract::create(alice, BaseContractWasm), + contract::add_instance_param(0, "uint8", "UINT8", 1), + contract::add_function("base", {{0, "uint8", "UINT8"}}), + fee(XRP(200))); + + auto const [contractAccount2, contractHash2, jv2] = setContract( + env, + tesSUCCESS, + contract::create(alice, Base2ContractWasm), + contract::add_instance_param(0, "uint8", "UINT8", 1), + contract::add_function("base", {{0, "uint8", "UINT8"}}), + fee(XRP(200))); + + // Modify contract + auto jt = env.jt( + contract::modify(alice, contractAccount, contractHash2), + contract::add_instance_param(0, "uint8", "UINT8", 1), + contract::add_function("base", {{0, "uint8", "UINT8"}}), + fee(XRP(200))); + env(jt, ter(tesSUCCESS)); + env.close(); + + // old contract source is deleted + auto const [oldSourceKey, oldSourceSle] = + contractSourceKeyAndSle(*env.current(), contractHash); + BEAST_EXPECT(!oldSourceSle); + + // new contract source exists + auto const [sourceKey, sourceSle] = + contractSourceKeyAndSle(*env.current(), contractHash2); + BEAST_EXPECT(sourceSle); + BEAST_EXPECT(sourceSle->getFieldU64(sfReferenceCount) == 2); + + // // validate modified contract + // auto const k = keylet::contract(contractHash, seq); + // validateContract( + // env, + // k, + // contractAccount.id(), + // alice.id(), + // 0, + // seq, + // newContractHash, + // jt.jv[sfInstanceParameterValues]); + } + + //---------------------------------------------------------------------- + // doApply.ContractOwner.tesSUCCESS + + { + test::jtx::Env env{*this, features}; + + jtx::Account const alice = Account{"alice"}; + jtx::Account const bob = Account{"bob"}; + env.fund(XRP(10'000), alice, bob); + env.close(); + + // Create initial contract + auto const seq = env.seq(alice); + auto const [contractAccount, contractHash, jv] = setContract( + env, + tesSUCCESS, + contract::create(alice, BaseContractWasm), + contract::add_instance_param(0, "uint8", "UINT8", 1), + contract::add_function("base", {{0, "uint8", "UINT8"}}), + fee(XRP(200))); + + // Modify contract + auto jt = env.jt(contract::modify(alice, contractAccount, bob), fee(XRP(200))); + env(jt, ter(tesSUCCESS)); + env.close(); + + // validate modified contract + auto const k = keylet::contract(contractHash, alice, seq); + validateContract( + env, + k, + contractAccount.id(), + bob.id(), + 0, + seq, + contractHash, + jv[sfInstanceParameterValues]); + } + } + + void + testDeleteDoApply(FeatureBitset features) + { + testcase("delete doApply"); + + using namespace jtx; + + //------------------------------------------------------------------------- + // doApply.tesSUCCESS - Single Reference + { + test::jtx::Env env{*this, features}; + + jtx::Account const alice = Account{"alice"}; + env.fund(XRP(10'000), alice); + env.close(); + + auto const seq = env.current()->seq(); + auto const [contractAccount, contractHash, jv] = setContract( + env, + tesSUCCESS, + contract::create(alice, BaseContractWasm), + contract::add_instance_param(0, "uint8", "UINT8", 1), + contract::add_function("base", {{0, "uint8", "UINT8"}}), + fee(XRP(200))); + + env(contract::del(alice, contractAccount), ter(tesSUCCESS)); + env.close(); + + // Pseudo Account is deleted + auto const pseudoAccountKey = keylet::account(contractAccount); + BEAST_EXPECT(!env.le(pseudoAccountKey)); + + // Contract instance is deleted + auto const wasmBytes = strUnHex(BaseContractWasm); + auto const contractKey = keylet::contract(contractHash, alice, seq); + BEAST_EXPECT(!env.le(contractKey)); + + // ContractSource is deleted - because it had a single reference + auto const contractSourceKey = keylet::contractSource(contractHash); + BEAST_EXPECT(!env.le(contractSourceKey)); + } + + // doApply.tesSUCCESS - Multiple Reference + { + test::jtx::Env env{*this, features}; + + jtx::Account const alice = Account{"alice"}; + env.fund(XRP(10'000), alice); + env.close(); + + auto const seq = env.current()->seq(); + auto const [contractAccount, contractHash, jv] = setContract( + env, + tesSUCCESS, + contract::create(alice, BaseContractWasm), + contract::add_instance_param(0, "uint8", "UINT8", 1), + contract::add_function("base", {{0, "uint8", "UINT8"}}), + fee(XRP(200))); + + auto const seq2 = env.current()->seq(); + auto const [contractAccount2, contractHash2, jv2] = setContract( + env, + tesSUCCESS, + contract::create(alice, BaseContractWasm), + contract::add_instance_param(0, "uint8", "UINT8", 1), + contract::add_function("base", {{0, "uint8", "UINT8"}}), + fee(XRP(200))); + + env(contract::del(alice, contractAccount), ter(tesSUCCESS)); + env.close(); + + // Pseudo Account is deleted + auto const pseudoAccountKey = keylet::account(contractAccount); + BEAST_EXPECT(!env.le(pseudoAccountKey)); + + // Contract instance is deleted + auto const wasmBytes = strUnHex(BaseContractWasm); + auto const contractKey = keylet::contract(contractHash, alice, seq); + BEAST_EXPECT(!env.le(contractKey)); + + // Ensure ContractSource still exists + auto const contractSourceKey = keylet::contractSource(contractHash); + BEAST_EXPECT(env.le(contractSourceKey)); + BEAST_EXPECT(env.le(contractSourceKey)->getFieldU64(sfReferenceCount) == 1); + + // Pseudo Account of second instance still exists + auto const pseudoAccountKey2 = keylet::account(contractAccount2); + BEAST_EXPECT(env.le(pseudoAccountKey2)); + + // Ensure second contract instance still exists + auto const contractKey2 = keylet::contract(contractHash2, alice, seq2); + BEAST_EXPECT(env.le(contractKey2)); + } + } + + void + testUserDeletePreflight(FeatureBitset features) + { + testcase("user delete preflight"); + + using namespace jtx; + + // temDISABLED: Feature not enabled + { + test::jtx::Env env{*this, features - featureSmartContract}; + + auto const alice = Account{"alice"}; + env.fund(XRP(10'000), alice); + env.close(); + + env(contract::userDelete(alice, BaseContractWasm), ter(temDISABLED)); + } + } + + std::string + loadContractWasmStr(std::string const& contract_name = "") + { + std::string const& dir = "e2e-tests"; + std::string name = "/Users/darkmatter/projects/ledger-works/xrpl-wasm-std/" + dir + "/" + + contract_name + "/target/wasm32v1-none/release/" + contract_name + ".wasm"; + if (!boost::filesystem::exists(name)) + { + std::cout << "File does not exist: " << name << "\n"; + return ""; + } + + std::ifstream file(name, std::ios::binary); + + if (!file) + { + std::cout << "Failed to open file: " << name << "\n"; + return ""; + } + + // Read the file into a vector + std::vector buffer( + (std::istreambuf_iterator(file)), std::istreambuf_iterator()); + + // Check if the buffer is empty + if (buffer.empty()) + { + std::cout << "File is empty or could not be read properly.\n"; + return ""; + } + + return strHex(buffer); + } + + void + testContractDataSimple(FeatureBitset features) + { + testcase("contract data simple"); + + using namespace jtx; + + test::jtx::Env env{*this, features}; + + auto const alice = Account{"alice"}; + auto const bob = Account{"bob"}; + env.fund(XRP(10'000), alice, bob); + env.close(); + + std::string contractDataWasmHex = loadContractWasmStr("contract_data"); + auto const [contractAccount, contractHash, _] = setContract( + env, + tesSUCCESS, + contract::create(alice, contractDataWasmHex), + contract::add_instance_param(tfSendAmount, "value", "AMOUNT", XRP(2000)), + contract::add_function("object_simple_create", {}), + contract::add_function("object_simple_update", {}), + fee(XRP(2000))); + + env(contract::call(alice, contractAccount, "object_simple_create"), + escrow::comp_allowance(1'000'000), + ter(tesSUCCESS)); + env.close(); + + env(contract::call(alice, contractAccount, "object_simple_update"), + escrow::comp_allowance(1'000'000), + ter(tesSUCCESS)); + env.close(); + } + + void + testContractDataNested(FeatureBitset features) + { + testcase("contract data nested"); + + using namespace jtx; + + test::jtx::Env env{*this, features}; + + auto const alice = Account{"alice"}; + auto const bob = Account{"bob"}; + env.fund(XRP(10'000), alice, bob); + env.close(); + + std::string contractDataWasmHex = loadContractWasmStr("contract_data"); + auto const [contractAccount, contractHash, _] = setContract( + env, + tesSUCCESS, + contract::create(alice, contractDataWasmHex), + contract::add_instance_param(tfSendAmount, "value", "AMOUNT", XRP(2000)), + contract::add_function("object_nested_create", {}), + contract::add_function("object_nested_update", {}), + fee(XRP(2000))); + + env(contract::call(alice, contractAccount, "object_nested_create"), + escrow::comp_allowance(1'000'000), + ter(tesSUCCESS)); + env.close(); + + env(contract::call(alice, contractAccount, "object_nested_update"), + escrow::comp_allowance(1'000'000), + ter(tesSUCCESS)); + env.close(); + } + + void + testContractDataArray(FeatureBitset features) + { + testcase("contract data array"); + + using namespace jtx; + + test::jtx::Env env{*this, features}; + + auto const alice = Account{"alice"}; + auto const bob = Account{"bob"}; + env.fund(XRP(10'000), alice, bob); + env.close(); + + std::string contractDataWasmHex = loadContractWasmStr("contract_data"); + auto const [contractAccount, contractHash, _] = setContract( + env, + tesSUCCESS, + contract::create(alice, contractDataWasmHex), + contract::add_instance_param(tfSendAmount, "value", "AMOUNT", XRP(2000)), + contract::add_function("object_with_arrays_create", {}), + contract::add_function("object_with_arrays_update", {}), + fee(XRP(2000))); + + env(contract::call(alice, contractAccount, "object_with_arrays_create"), + escrow::comp_allowance(1'000'000), + ter(tesSUCCESS)); + env.close(); + + env(contract::call(alice, contractAccount, "object_with_arrays_update"), + escrow::comp_allowance(1'000'000), + ter(tesSUCCESS)); + env.close(); + } + + void + testContractDataNestedArray(FeatureBitset features) + { + testcase("contract data nested array"); + + using namespace jtx; + + test::jtx::Env env{*this, features}; + + auto const alice = Account{"alice"}; + auto const bob = Account{"bob"}; + env.fund(XRP(10'000), alice, bob); + env.close(); + + std::string contractDataWasmHex = loadContractWasmStr("contract_data"); + auto const [contractAccount, contractHash, _] = setContract( + env, + tesSUCCESS, + contract::create(alice, contractDataWasmHex), + contract::add_instance_param(tfSendAmount, "value", "AMOUNT", XRP(2000)), + contract::add_function("object_with_nested_arrays_create", {}), + contract::add_function("object_with_nested_arrays_update", {}), + fee(XRP(2000))); + + env(contract::call(alice, contractAccount, "object_with_nested_arrays_create"), + escrow::comp_allowance(1'000'000), + ter(tesSUCCESS)); + env.close(); + + env(contract::call(alice, contractAccount, "object_with_nested_arrays_update"), + escrow::comp_allowance(1'000'000), + ter(tesSUCCESS)); + env.close(); + } + + void + testInstanceParameters(FeatureBitset features) + { + testcase("instance parameters"); + + using namespace jtx; + + Env env{*this, features}; + + auto const alice = Account{"alice"}; + auto const bob = Account{"bob"}; + auto const gw = Account{"gw"}; + auto const USD = gw["USD"]; + env.fund(XRP(10'000), alice, bob); + env.close(); + + // Test Instance Parameter (1 of 2) + // uint8, uint16, uint32, uint64, uint128, uint160, uint192, uint256 + { + std::string wasmHex = loadContractWasmStr("instance_params_uint"); + auto const [contractAccount, contractHash, _] = setContract( + env, + tesSUCCESS, + contract::create(alice, wasmHex), + contract::add_instance_param(0, "uint8", "UINT8", 255), + contract::add_instance_param(0, "uint16", "UINT16", 65535), + contract::add_instance_param( + 0, "uint32", "UINT32", static_cast(4294967295)), + contract::add_instance_param(0, "uint64", "UINT64", "FFFFFFFFFFFFFFFF"), + contract::add_instance_param( + 0, "uint128", "UINT128", "00000000000000000000000000000001"), + contract::add_instance_param( + 0, "uint160", "UINT160", "0000000000000000000000000000000000000001"), + contract::add_instance_param( + 0, "uint192", "UINT192", "000000000000000000000000000000000000000000000001"), + contract::add_instance_param( + 0, + "uint256", + "UINT256", + "D955DAC2E77519F05AD151A5D3C99FC8125FB39D58FF9F106F1ACA4491" + "902C" + "25"), + contract::add_function("instance_params_uint", {}), + fee(XRP(200))); + + // { + // Json::Value params; + // params[jss::ledger_index] = env.current()->seq() - 1; + // params[jss::transactions] = true; + // params[jss::expand] = true; + // auto const jrr = env.rpc("json", "ledger", + // to_string(params)); std::cout << jrr << std::endl; + // } + + env(contract::call(alice, contractAccount, "instance_params_uint"), + escrow::comp_allowance(1000000), + ter(tesSUCCESS)); + env.close(); + } + + { + // Test Instance Parameter (2 of 2) + // vl, account, amount (XRP), amount (IOU), number, currency, issue + std::string wasmHex = loadContractWasmStr("instance_params_other"); + auto const [contractAccount, contractHash, _] = setContract( + env, + tesSUCCESS, + contract::create(alice, wasmHex), + contract::add_instance_param(0, "vl", "VL", "DEADBEEF"), + contract::add_instance_param(0, "account", "ACCOUNT", alice.human()), + contract::add_instance_param( + 0, "amountXRP", "AMOUNT", XRP(1).value().getJson(JsonOptions::none)), + contract::add_instance_param( + 0, "amountIOU", "AMOUNT", USD(1.2).value().getJson(JsonOptions::none)), + contract::add_instance_param(0, "number", "NUMBER", "1.2"), + contract::add_function("instance_params_other", {}), + fee(XRP(200))); + + // { + // Json::Value params; + // params[jss::ledger_index] = env.current()->seq() - 1; + // params[jss::transactions] = true; + // params[jss::expand] = true; + // auto const jrr = env.rpc("json", "ledger", + // to_string(params)); std::cout << jrr << std::endl; + // } + + env(contract::call(alice, contractAccount, "instance_params_other"), + escrow::comp_allowance(1000000), + ter(tesSUCCESS)); + env.close(); + } + } + + void + testFunctionParameters(FeatureBitset features) + { + testcase("function parameters"); + + using namespace jtx; + + Env env{*this, features}; + + auto const alice = Account{"alice"}; + auto const bob = Account{"bob"}; + auto const gw = Account{"gw"}; + auto const USD = gw["USD"]; + env.fund(XRP(10'000), alice, bob); + env.close(); + + std::string wasmHex = loadContractWasmStr("function_params"); + auto const [contractAccount, contractHash, _] = setContract( + env, + tesSUCCESS, + contract::create(alice, wasmHex), + contract::add_instance_param(tfSendAmount, "amount", "AMOUNT", XRP(2000)), + contract::add_instance_param(0, "uint8", "UINT8", 1), + contract::add_function( + "function_params_uint", + { + {0, "uint8", "UINT8"}, + {0, "uint16", "UINT16"}, + {0, "uint32", "UINT32"}, + {0, "uint64", "UINT64"}, + {0, "uint128", "UINT128"}, + {0, "uint160", "UINT160"}, + {0, "uint192", "UINT192"}, + {0, "uint256", "UINT256"}, + }), + contract::add_function( + "function_params_other", + { + {0, "vl", "VL"}, + {0, "account", "ACCOUNT"}, + {0, "amountXRP", "AMOUNT"}, + {0, "amountIOU", "AMOUNT"}, + {0, "number", "NUMBER"}, + // {0, "issue", "ISSUE"}, + // {0, "currency", "CURRENCY"} + }), + fee(XRP(200))); + + // { + // Json::Value params; + // params[jss::ledger_index] = env.current()->seq() - 1; + // params[jss::transactions] = true; + // params[jss::expand] = true; + // auto const jrr = env.rpc("json", "ledger", to_string(params)); + // std::cout << jrr << std::endl; + // } + + env(contract::call(alice, contractAccount, "function_params_uint"), + escrow::comp_allowance(1000000), + contract::add_param(0, "uint8", "UINT8", 255), + contract::add_param(0, "uint16", "UINT16", 65535), + contract::add_param(0, "uint32", "UINT32", static_cast(4294967295)), + contract::add_param(0, "uint64", "UINT64", "FFFFFFFFFFFFFFFF"), + contract::add_param(0, "uint128", "UINT128", "00000000000000000000000000000001"), + contract::add_param( + 0, "uint160", "UINT160", "0000000000000000000000000000000000000001"), + contract::add_param( + 0, "uint192", "UINT192", "000000000000000000000000000000000000000000000001"), + contract::add_param( + 0, + "uint256", + "UINT256", + "D955DAC2E77519F05AD151A5D3C99FC8125FB39D58FF9F106F1ACA4491902C" + "25"), + ter(tesSUCCESS)); + + env(contract::call(alice, contractAccount, "function_params_other"), + escrow::comp_allowance(1000000), + contract::add_param(0, "vl", "VL", "DEADBEEF"), + contract::add_param(0, "account", "ACCOUNT", alice.human()), + contract::add_param( + 0, "amountXRP", "AMOUNT", XRP(1).value().getJson(JsonOptions::none)), + contract::add_param( + 0, "amountIOU", "AMOUNT", USD(1.2).value().getJson(JsonOptions::none)), + contract::add_param(0, "number", "NUMBER", "1.2"), + // contract::add_param(0, "issue", "ISSUE", + // to_json(USD(1).value().issue())), contract::add_param(0, + // "currency", "CURRENCY", "USD"), + ter(tesSUCCESS)); + env.close(); + } + + void + testEmit(FeatureBitset features) + { + testcase("emit"); + + using namespace jtx; + + test::jtx::Env env{*this, features}; + + auto const alice = Account{"alice"}; + auto const bob = Account{"bob"}; + env.fund(XRP(10'000), alice, bob); + env.close(); + + std::string emitTxWasmHex = loadContractWasmStr("emit_txn"); + auto const [contractAccount, contractHash, _] = setContract( + env, + tesSUCCESS, + contract::create(alice, emitTxWasmHex), + contract::add_instance_param(tfSendAmount, "value", "AMOUNT", XRP(2000)), + contract::add_function("emit", {}), + fee(XRP(200))); + + // { + // Json::Value params; + // params[jss::ledger_index] = env.current()->seq() - 1; + // params[jss::transactions] = true; + // params[jss::expand] = true; + // auto const jrr = env.rpc("json", "ledger", to_string(params)); + // std::cout << jrr << std::endl; + // } + + env(contract::call(alice, contractAccount, "emit"), + escrow::comp_allowance(1000000), + ter(tesSUCCESS)); + env.close(); + } + + void + testEvents(FeatureBitset features) + { + testcase("events"); + + using namespace std::chrono_literals; + using namespace jtx; + + test::jtx::Env env{*this, features}; + + auto const alice = Account{"alice"}; + auto const bob = Account{"bob"}; + auto const gw = Account{"gw"}; + auto const USD = gw["USD"]; + env.fund(XRP(10'000), alice, bob); + env.close(); + + auto wsc = makeWSClient(env.app().config()); + Json::Value stream; + + { + // RPC subscribe to contract events stream + stream[jss::streams] = Json::arrayValue; + stream[jss::streams].append("contract_events"); + auto jv = wsc->invoke("subscribe", stream); + if (wsc->version() == 2) + { + BEAST_EXPECT(jv.isMember(jss::jsonrpc) && jv[jss::jsonrpc] == "2.0"); + BEAST_EXPECT(jv.isMember(jss::ripplerpc) && jv[jss::ripplerpc] == "2.0"); + BEAST_EXPECT(jv.isMember(jss::id) && jv[jss::id] == 5); + } + BEAST_EXPECT(jv[jss::result][jss::status] == "success"); + } + + std::string eventsWasmHex = loadContractWasmStr("events"); + auto const [contractAccount, contractHash, _] = setContract( + env, + tesSUCCESS, + contract::create(alice, eventsWasmHex), + contract::add_instance_param(tfSendAmount, "amount", "AMOUNT", XRP(2000)), + contract::add_function("events", {}), + fee(XRP(200))); + + // { + // Json::Value params; + // params[jss::ledger_index] = env.current()->seq() - 1; + // params[jss::transactions] = true; + // params[jss::expand] = true; + // auto const jrr = env.rpc("json", "ledger", to_string(params)); + // std::cout << jrr << std::endl; + // } + + env(contract::call(alice, contractAccount, "events"), + escrow::comp_allowance(1000000), + ter(tesSUCCESS)); + env.close(); + + { + // Get contract info + Json::Value params; + params[jss::contract_account] = contractAccount.human(); + params[jss::account] = alice.human(); + auto const jrr = env.rpc("json", "contract_info", to_string(params)); + std::cout << jrr << std::endl; + } + + // Check stream update + BEAST_EXPECT(wsc->findMsg(5s, [&](auto const& jv) { + auto const data = jv[jss::data]; + // std::cout << "Event: " << data << std::endl; + BEAST_EXPECT(data["amount"] == "192"); + BEAST_EXPECT(data["currency"] == "USD"); + BEAST_EXPECT(data["destination"] == "r99mpXDsCPybsGs9XzGJmuxa8gWLTn8aCz"); + BEAST_EXPECT(data["uint128"] == "00000000000000000000000000000000"); + BEAST_EXPECT(data["uint16"] == 16); + BEAST_EXPECT(data["uint160"] == "0000000000000000000000000000000000000000"); + BEAST_EXPECT(data["uint192"] == "000000000000000000000000000000000000000000000000"); + BEAST_EXPECT( + data["uint256"] == + "00000000000000000000000000000000000000000000000000000000000000" + "00"); + BEAST_EXPECT(data["uint32"] == 32); + BEAST_EXPECT(data["uint64"] == "40"); + BEAST_EXPECT(data["uint8"] == 8); + BEAST_EXPECT(data["vl"] == "48656C6C6F2C20576F726C6421"); + return jv[jss::type] == "contractEvent" && jv[jss::name] == "event1"; + })); + + // RPC unsubscribe + auto jv = wsc->invoke("unsubscribe", stream); + if (wsc->version() == 2) + { + BEAST_EXPECT(jv.isMember(jss::jsonrpc) && jv[jss::jsonrpc] == "2.0"); + BEAST_EXPECT(jv.isMember(jss::ripplerpc) && jv[jss::ripplerpc] == "2.0"); + BEAST_EXPECT(jv.isMember(jss::id) && jv[jss::id] == 5); + } + BEAST_EXPECT(jv[jss::status] == "success"); + } + + void + testEasyMode(FeatureBitset features) + { + testcase("easy mode"); + + using namespace jtx; + + Env env{*this, features}; + + auto const alice = Account{"alice"}; + auto const bob = Account{"bob"}; + auto const gw = Account{"gw"}; + auto const USD = gw["USD"]; + env.fund(XRP(10'000), alice, bob); + env.close(); + + std::string wasmHex = loadContractWasmStr("easymode"); + auto const [contractAccount, contractHash, _] = setContract( + env, + tesSUCCESS, + contract::create(alice, wasmHex), + contract::add_instance_param(tfSendAmount, "amount", "AMOUNT", XRP(2000)), + contract::add_instance_param(0, "uint8", "UINT8", 1), + contract::add_function( + "easymode", + { + {0, "account", "ACCOUNT"}, + {0, "amount", "AMOUNT"}, + }), + fee(XRP(200))); + + // { + // Json::Value params; + // params[jss::ledger_index] = env.current()->seq() - 1; + // params[jss::transactions] = true; + // params[jss::expand] = true; + // auto const jrr = env.rpc("json", "ledger", to_string(params)); + // std::cout << jrr << std::endl; + // } + + env(contract::call(alice, contractAccount, "easymode"), + escrow::comp_allowance(1000000), + contract::add_param(0, "account", "ACCOUNT", bob.human()), + contract::add_param(0, "amount", "AMOUNT", XRP(1)), + ter(tesSUCCESS)); + env.close(); + + // { + // Json::Value params; + // params[jss::ledger_index] = env.current()->seq() - 1; + // params[jss::transactions] = true; + // params[jss::expand] = true; + // auto const jrr = env.rpc("json", "ledger", to_string(params)); + // std::cout << jrr << std::endl; + // } + } + + void + testWithFeats(FeatureBitset features) + { + testCreatePreflight(features); + testCreatePreclaim(features); + testCreateDoApply(features); + // testModifyPreflight(features); + // testModifyPreclaim(features); + testModifyDoApply(features); + // testDeletePreflight(features); + // testDeletePreclaim(features); + testDeleteDoApply(features); + // testUserDeletePreflight(features); + // testUserDeletePreclaim(features); + // testUserDeleteDoApply(features); + testContractDataSimple(features); + testContractDataNested(features); + testContractDataArray(features); + testContractDataNestedArray(features); + testInstanceParameters(features); + testFunctionParameters(features); + testEmit(features); + // testEvents(features); + } + +public: + void + run() override + { + using namespace test::jtx; + auto const sa = testable_amendments(); + testWithFeats(sa); + } +}; + +BEAST_DEFINE_TESTSUITE(Contract, app, xrpl); + +} // namespace test +} // namespace xrpl diff --git a/src/test/app/FeeVote_test.cpp b/src/test/app/FeeVote_test.cpp index 110ff56163..e4c82fb25e 100644 --- a/src/test/app/FeeVote_test.cpp +++ b/src/test/app/FeeVote_test.cpp @@ -3,6 +3,7 @@ #include #include +#include #include #include #include @@ -72,6 +73,15 @@ createFeeTx( sfExtensionSizeLimit, fields.extensionSizeLimit ? *fields.extensionSizeLimit : 0); obj.setFieldU32(sfGasPrice, fields.gasPrice ? *fields.gasPrice : 0); } + if (rules.enabled(featureSmartEscrow) || forceAllFields) + { + obj.setFieldU32( + sfExtensionComputeLimit, + fields.extensionComputeLimit ? *fields.extensionComputeLimit : 0); + obj.setFieldU32( + sfExtensionSizeLimit, fields.extensionSizeLimit ? *fields.extensionSizeLimit : 0); + obj.setFieldU32(sfGasPrice, fields.gasPrice ? *fields.gasPrice : 0); + } }; return STTx(ttFEE, fill); } diff --git a/src/test/app/FixNFTokenPageLinks_test.cpp b/src/test/app/FixNFTokenPageLinks_test.cpp index 25366534cd..c474bb4014 100644 --- a/src/test/app/FixNFTokenPageLinks_test.cpp +++ b/src/test/app/FixNFTokenPageLinks_test.cpp @@ -1,9 +1,9 @@ #include +#include #include #include #include -#include namespace xrpl { diff --git a/src/test/app/NFTokenAuth_test.cpp b/src/test/app/NFTokenAuth_test.cpp index 0e3fb24305..3792833588 100644 --- a/src/test/app/NFTokenAuth_test.cpp +++ b/src/test/app/NFTokenAuth_test.cpp @@ -1,6 +1,6 @@ #include -#include +#include namespace xrpl { diff --git a/src/test/app/NFTokenBurn_test.cpp b/src/test/app/NFTokenBurn_test.cpp index cd0df42c03..7fdaec7286 100644 --- a/src/test/app/NFTokenBurn_test.cpp +++ b/src/test/app/NFTokenBurn_test.cpp @@ -1,8 +1,9 @@ #include +#include #include #include -#include +#include #include diff --git a/src/test/app/NFTokenDir_test.cpp b/src/test/app/NFTokenDir_test.cpp index 78765cb6c0..9e4ee9d6b8 100644 --- a/src/test/app/NFTokenDir_test.cpp +++ b/src/test/app/NFTokenDir_test.cpp @@ -1,9 +1,9 @@ #include +#include #include #include #include -#include #include diff --git a/src/test/app/NFToken_test.cpp b/src/test/app/NFToken_test.cpp index 0d391147a8..f8dcfc0d0b 100644 --- a/src/test/app/NFToken_test.cpp +++ b/src/test/app/NFToken_test.cpp @@ -1,9 +1,9 @@ #include #include +#include #include #include -#include #include diff --git a/src/test/app/TestHostFunctions.h b/src/test/app/TestHostFunctions.h index 0e74cd6283..748f11bacc 100644 --- a/src/test/app/TestHostFunctions.h +++ b/src/test/app/TestHostFunctions.h @@ -3,8 +3,8 @@ #include #include +#include #include -#include #include #include diff --git a/src/test/app/wasm_fixtures/contract_data/Cargo.lock b/src/test/app/wasm_fixtures/contract_data/Cargo.lock new file mode 100644 index 0000000000..5b87de721f --- /dev/null +++ b/src/test/app/wasm_fixtures/contract_data/Cargo.lock @@ -0,0 +1,15 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "contract_data" +version = "0.0.1" +dependencies = [ + "xrpl-wasm-std", +] + +[[package]] +name = "xrpl-wasm-std" +version = "0.5.1-devnet5" +source = "git+https://github.com/Transia-RnD/craft.git?branch=dangell%2Fsmart-contracts#3c8191ae9832ea25f7d8f3e5eeb33b65181d31b5" diff --git a/src/test/app/wasm_fixtures/contract_data/Cargo.toml b/src/test/app/wasm_fixtures/contract_data/Cargo.toml new file mode 100644 index 0000000000..5638081ca0 --- /dev/null +++ b/src/test/app/wasm_fixtures/contract_data/Cargo.toml @@ -0,0 +1,21 @@ +[package] +name = "contract_data" +version = "0.0.1" +edition = "2024" + +# This empty workspace definition keeps this project independent of the parent workspace +[workspace] + +[lib] +crate-type = ["cdylib"] + +[profile.release] +lto = true +opt-level = 's' +panic = "abort" + +[dependencies] +xrpl-wasm-std = { git = "https://github.com/Transia-RnD/craft.git", branch = "dangell/smart-contracts", package = "xrpl-wasm-std" } + +[profile.dev] +panic = "abort" diff --git a/src/test/app/wasm_fixtures/contract_data/src/lib.rs b/src/test/app/wasm_fixtures/contract_data/src/lib.rs new file mode 100644 index 0000000000..965c50c06c --- /dev/null +++ b/src/test/app/wasm_fixtures/contract_data/src/lib.rs @@ -0,0 +1,529 @@ +#![cfg_attr(target_arch = "wasm32", no_std)] + +#[cfg(not(target_arch = "wasm32"))] +extern crate std; + +use xrpl_wasm_std::core::data::codec::{ + get_array_element, get_data, get_nested_array_element, get_nested_data, set_array_element, + set_data, set_nested_array_element, set_nested_data, +}; +use xrpl_wasm_std::core::types::account_id::AccountID; +use xrpl_wasm_std::host::trace::{trace, trace_num}; + +// Different accounts for different test patterns +const ACCOUNT: [u8; 20] = [ + 0xAE, 0x12, 0x3A, 0x85, 0x56, 0xF3, 0xCF, 0x91, 0x15, 0x47, 0x11, 0x37, 0x6A, 0xFB, 0x0F, 0x89, + 0x4F, 0x83, 0x2B, 0x3D, +]; + +// ============================================================================ +// TEST 1: Simple Object - Only top-level key-value pairs +// Creates: { "value_u8": 42, "value_u16": 1234, "count": 3, ... } +// ============================================================================ + +#[unsafe(no_mangle)] +pub extern "C" fn object_simple_create() -> i32 { + let _ = trace("=== TEST 1: Simple Object Create ==="); + let account = AccountID(ACCOUNT); + + // Test u8 + let _ = trace("Testing u8..."); + if let Err(e) = set_data::(&account, "value_u8", 42) { + return e; + } + if let Some(val) = get_data::(&account, "value_u8") { + let _ = trace_num("Read back u8:", val.into()); + } else { + let _ = trace("Failed to read back u8"); + return -1; + } + + // Test u16 + let _ = trace("Testing u16..."); + if let Err(e) = set_data::(&account, "value_u16", 1234) { + return e; + } + if let Some(val) = get_data::(&account, "value_u16") { + let _ = trace_num("Read back u16:", val.into()); + } else { + let _ = trace("Failed to read back u16"); + return -1; + } + + // Test u32 + let _ = trace("Testing u32..."); + if let Err(e) = set_data::(&account, "count", 3) { + return e; + } + if let Err(e) = set_data::(&account, "total", 12) { + return e; + } + if let Some(count_val) = get_data::(&account, "count") { + let _ = trace_num("Read back count:", count_val.into()); + } else { + let _ = trace("Failed to read back count"); + return -1; + } + + // Test u64 + let _ = trace("Testing u64..."); + if let Err(e) = set_data::(&account, "value_u64", 9876543210) { + return e; + } + if let Some(val) = get_data::(&account, "value_u64") { + let _ = trace_num("Read back u64:", val as i64); + } else { + let _ = trace("Failed to read back u64"); + return -1; + } + + // Test AccountID + let _ = trace("Testing AccountID..."); + const DESTINATION: [u8; 20] = [ + 0x05, 0x96, 0x91, 0x5C, 0xFD, 0xEE, 0xE3, 0xA6, 0x95, 0xB3, 0xEF, 0xD6, 0xBD, 0xA9, 0xAC, + 0x78, 0x8A, 0x36, 0x8B, 0x7B, + ]; + let destination = AccountID(DESTINATION); + if let Err(e) = set_data(&account, "destination", destination) { + return e; + } + if let Some(_dest) = get_data::(&account, "destination") { + let _ = trace("Read back AccountID successfully"); + } else { + let _ = trace("Failed to read back AccountID"); + return -1; + } + + // Test reading non-existent key + let _ = trace("Testing non-existent key..."); + if let Some(_) = get_data::(&account, "nonexistent") { + let _ = trace("ERROR: Should not have found nonexistent key"); + return -1; + } else { + let _ = trace("Correctly returned None for nonexistent key"); + } + + let _ = trace("Simple object create tests passed!"); + 0 +} + +#[unsafe(no_mangle)] +pub extern "C" fn object_simple_update() -> i32 { + let _ = trace("=== TEST 1: Simple Object Update ==="); + let account = AccountID(ACCOUNT); + + // Update u8 + let _ = trace("Updating u8 to 99..."); + if let Err(e) = set_data::(&account, "value_u8", 99) { + return e; + } + if let Some(val) = get_data::(&account, "value_u8") { + let _ = trace_num("Read back updated u8:", val.into()); + } else { + let _ = trace("Failed to read back u8"); + return -1; + } + + // Update u32 + let _ = trace("Updating count to 4..."); + if let Err(e) = set_data::(&account, "count", 4) { + return e; + } + if let Some(count_val) = get_data::(&account, "count") { + let _ = trace_num("Read back updated count:", count_val.into()); + } else { + let _ = trace("Failed to read back count"); + return -1; + } + + // Add new field + let _ = trace("Adding new field 'status'..."); + if let Err(e) = set_data::(&account, "status", 100) { + return e; + } + if let Some(val) = get_data::(&account, "status") { + let _ = trace_num("Read back new status:", val.into()); + } else { + let _ = trace("Failed to read back status"); + return -1; + } + + let _ = trace("Simple object update tests passed!"); + 0 +} + +// ============================================================================ +// TEST 2: Nested Object - Objects containing objects (depth 1) +// Creates: { "stats": {"score": 9999, "level": 5}, "key": {"subkey": 12} } +// ============================================================================ + +#[unsafe(no_mangle)] +pub extern "C" fn object_nested_create() -> i32 { + let _ = trace("=== TEST 2: Nested Object Create ==="); + let account = AccountID(ACCOUNT); + + // Test nested u8 + let _ = trace("Testing nested u8..."); + if let Err(e) = set_nested_data::(&account, "key", "subkey", 12) { + return e; + } + if let Some(nested_val) = get_nested_data::(&account, "key", "subkey") { + let _ = trace_num("Read back nested value:", nested_val.into()); + } else { + let _ = trace("Failed to read back nested value"); + return -1; + } + + // Test nested u32 + let _ = trace("Testing nested u32..."); + if let Err(e) = set_nested_data::(&account, "stats", "score", 9999) { + return e; + } + if let Some(val) = get_nested_data::(&account, "stats", "score") { + let _ = trace_num("Read back nested u32:", val.into()); + } else { + let _ = trace("Failed to read back nested u32"); + return -1; + } + + // Test multiple fields in same nested object + let _ = trace("Adding multiple fields to nested object..."); + if let Err(e) = set_nested_data::(&account, "stats", "level", 5) { + return e; + } + if let Err(e) = set_nested_data::(&account, "stats", "coins", 1000) { + return e; + } + if let Some(val) = get_nested_data::(&account, "stats", "level") { + let _ = trace_num("Read back stats.level:", val.into()); + } else { + let _ = trace("Failed to read back stats.level"); + return -1; + } + + // Test nested u64 + let _ = trace("Testing nested u64..."); + if let Err(e) = set_nested_data::(&account, "data", "timestamp", 1234567890) { + return e; + } + if let Some(val) = get_nested_data::(&account, "data", "timestamp") { + let _ = trace_num("Read back nested u64:", val as i64); + } else { + let _ = trace("Failed to read back nested u64"); + return -1; + } + + let _ = trace("Nested object create tests passed!"); + 0 +} + +#[unsafe(no_mangle)] +pub extern "C" fn object_nested_update() -> i32 { + let _ = trace("=== TEST 2: Nested Object Update ==="); + let account = AccountID(ACCOUNT); + + // Update nested value + let _ = trace("Updating nested score to 12345..."); + if let Err(e) = set_nested_data::(&account, "stats", "score", 12345) { + return e; + } + if let Some(val) = get_nested_data::(&account, "stats", "score") { + let _ = trace_num("Read back updated nested score:", val.into()); + } else { + let _ = trace("Failed to read back nested score"); + return -1; + } + + // Update another nested field + let _ = trace("Updating nested level to 10..."); + if let Err(e) = set_nested_data::(&account, "stats", "level", 10) { + return e; + } + if let Some(val) = get_nested_data::(&account, "stats", "level") { + let _ = trace_num("Read back updated level:", val.into()); + } else { + let _ = trace("Failed to read back level"); + return -1; + } + + // Add new nested field + let _ = trace("Adding new nested field..."); + if let Err(e) = set_nested_data::(&account, "config", "timeout", 30) { + return e; + } + if let Some(val) = get_nested_data::(&account, "config", "timeout") { + let _ = trace_num("Read back new config.timeout:", val.into()); + } else { + let _ = trace("Failed to read back config.timeout"); + return -1; + } + + let _ = trace("Nested object update tests passed!"); + 0 +} + +// ============================================================================ +// TEST 3: Object with Arrays - Objects containing arrays of simple values +// Creates: { "items": [10, 20, 30], "values": [100, 200] } +// Note: This uses set_array_element which creates an object with array fields +// ============================================================================ + +#[unsafe(no_mangle)] +pub extern "C" fn object_with_arrays_create() -> i32 { + let _ = trace("=== TEST 3: Object with Arrays Create ==="); + let account = AccountID(ACCOUNT); + + // Test u8 array + let _ = trace("Testing u8 array..."); + if let Err(e) = set_array_element::(&account, "array_u8", 0, 10) { + return e; + } + if let Err(e) = set_array_element::(&account, "array_u8", 1, 20) { + return e; + } + if let Err(e) = set_array_element::(&account, "array_u8", 2, 30) { + return e; + } + if let Some(val) = get_array_element::(&account, "array_u8", 0) { + let _ = trace_num("Read array_u8[0]:", val.into()); + } else { + let _ = trace("Failed to read array_u8[0]"); + return -1; + } + if let Some(val) = get_array_element::(&account, "array_u8", 1) { + let _ = trace_num("Read array_u8[1]:", val.into()); + } else { + let _ = trace("Failed to read array_u8[1]"); + return -1; + } + + // Test u16 array + let _ = trace("Testing u16 array..."); + if let Err(e) = set_array_element::(&account, "array_u16", 0, 100) { + return e; + } + if let Err(e) = set_array_element::(&account, "array_u16", 1, 200) { + return e; + } + if let Some(val) = get_array_element::(&account, "array_u16", 0) { + let _ = trace_num("Read array_u16[0]:", val.into()); + } else { + let _ = trace("Failed to read array_u16[0]"); + return -1; + } + + // Test u32 array + let _ = trace("Testing u32 array..."); + if let Err(e) = set_array_element::(&account, "array_u32", 0, 1000) { + return e; + } + if let Err(e) = set_array_element::(&account, "array_u32", 1, 2000) { + return e; + } + if let Some(val) = get_array_element::(&account, "array_u32", 0) { + let _ = trace_num("Read array_u32[0]:", val.into()); + } else { + let _ = trace("Failed to read array_u32[0]"); + return -1; + } + + // Test u64 array + let _ = trace("Testing u64 array..."); + if let Err(e) = set_array_element::(&account, "array_u64", 0, 10000) { + return e; + } + if let Err(e) = set_array_element::(&account, "array_u64", 1, 20000) { + return e; + } + if let Some(val) = get_array_element::(&account, "array_u64", 0) { + let _ = trace_num("Read array_u64[0]:", val as i64); + } else { + let _ = trace("Failed to read array_u64[0]"); + return -1; + } + + let _ = trace("Object with arrays create tests passed!"); + 0 +} + +#[unsafe(no_mangle)] +pub extern "C" fn object_with_arrays_update() -> i32 { + let _ = trace("=== TEST 3: Object with Arrays Update ==="); + let account = AccountID(ACCOUNT); + + // Update array element + let _ = trace("Updating array_u32[0] to 7777..."); + if let Err(e) = set_array_element::(&account, "array_u32", 0, 7777) { + return e; + } + if let Some(val) = get_array_element::(&account, "array_u32", 0) { + let _ = trace_num("Read back updated array_u32[0]:", val.into()); + } else { + let _ = trace("Failed to read back array_u32[0]"); + return -1; + } + + // Add new array element + let _ = trace("Adding new array element array_u16[2]..."); + if let Err(e) = set_array_element::(&account, "array_u16", 2, 300) { + return e; + } + if let Some(val) = get_array_element::(&account, "array_u16", 2) { + let _ = trace_num("Read back new array_u16[2]:", val.into()); + } else { + let _ = trace("Failed to read back array_u16[2]"); + return -1; + } + + // Add element with gap (should auto-fill with nulls) + let _ = trace("Adding array_u8[5] (skipping indices 3-4)..."); + if let Err(e) = set_array_element::(&account, "array_u8", 5, 50) { + return e; + } + if let Some(val) = get_array_element::(&account, "array_u8", 5) { + let _ = trace_num("Read back array_u8[5]:", val.into()); + } else { + let _ = trace("Failed to read back array_u8[5]"); + return -1; + } + + let _ = trace("Object with arrays update tests passed!"); + 0 +} + +// ============================================================================ +// TEST 4: Object with Nested Arrays - Objects containing arrays of objects +// Creates: { "nested_array": [{"field1": 55, "field2": 66}, {"field1": 77}] } +// This is the most complex structure allowed (depth 1) +// DA: I wouldn't use this. If you are doing this, consider redesigning your data model +// ============================================================================ + +#[unsafe(no_mangle)] +pub extern "C" fn object_with_nested_arrays_create() -> i32 { + let _ = trace("=== TEST 4: Object with Nested Arrays Create ==="); + let account = AccountID(ACCOUNT); + + // Test nested u8 array with multiple fields + let _ = trace("Testing nested u8 array..."); + if let Err(e) = set_nested_array_element::(&account, "nested_array", 0, "field1", 55) { + return e; + } + if let Err(e) = set_nested_array_element::(&account, "nested_array", 0, "field2", 66) { + return e; + } + if let Err(e) = set_nested_array_element::(&account, "nested_array", 1, "field1", 77) { + return e; + } + + if let Some(val) = get_nested_array_element::(&account, "nested_array", 0, "field1") { + let _ = trace_num("Read nested_array[0].field1:", val.into()); + } else { + let _ = trace("Failed to read nested_array[0].field1"); + return -1; + } + if let Some(val) = get_nested_array_element::(&account, "nested_array", 0, "field2") { + let _ = trace_num("Read nested_array[0].field2:", val.into()); + } else { + let _ = trace("Failed to read nested_array[0].field2"); + return -1; + } + if let Some(val) = get_nested_array_element::(&account, "nested_array", 1, "field1") { + let _ = trace_num("Read nested_array[1].field1:", val.into()); + } else { + let _ = trace("Failed to read nested_array[1].field1"); + return -1; + } + + // Test nested u32 array + let _ = trace("Testing nested u32 array..."); + if let Err(e) = set_nested_array_element::(&account, "nested_array_u32", 0, "value", 5555) + { + return e; + } + if let Err(e) = set_nested_array_element::(&account, "nested_array_u32", 1, "value", 6666) + { + return e; + } + if let Some(val) = get_nested_array_element::(&account, "nested_array_u32", 0, "value") { + let _ = trace_num("Read nested_array_u32[0].value:", val.into()); + } else { + let _ = trace("Failed to read nested_array_u32[0].value"); + return -1; + } + + // Test nested u64 array + let _ = trace("Testing nested u64 array..."); + if let Err(e) = set_nested_array_element::(&account, "items", 0, "id", 99999) { + return e; + } + if let Err(e) = set_nested_array_element::(&account, "items", 0, "price", 123456) { + return e; + } + if let Some(val) = get_nested_array_element::(&account, "items", 0, "id") { + let _ = trace_num("Read items[0].id:", val as i64); + } else { + let _ = trace("Failed to read items[0].id"); + return -1; + } + + let _ = trace("Object with nested arrays create tests passed!"); + 0 +} + +#[unsafe(no_mangle)] +pub extern "C" fn object_with_nested_arrays_update() -> i32 { + let _ = trace("=== TEST 4: Object with Nested Arrays Update ==="); + let account = AccountID(ACCOUNT); + + // Update nested array element + let _ = trace("Updating nested_array[0].field1 to 88..."); + if let Err(e) = set_nested_array_element::(&account, "nested_array", 0, "field1", 88) { + return e; + } + if let Some(val) = get_nested_array_element::(&account, "nested_array", 0, "field1") { + let _ = trace_num("Read back updated nested_array[0].field1:", val.into()); + } else { + let _ = trace("Failed to read back nested_array[0].field1"); + return -1; + } + + // Add new field to existing array element + let _ = trace("Adding field3 to nested_array[0]..."); + if let Err(e) = set_nested_array_element::(&account, "nested_array", 0, "field3", 111) { + return e; + } + if let Some(val) = get_nested_array_element::(&account, "nested_array", 0, "field3") { + let _ = trace_num("Read back nested_array[0].field3:", val.into()); + } else { + let _ = trace("Failed to read back nested_array[0].field3"); + return -1; + } + + // Add new array element + let _ = trace("Adding nested_array[2]..."); + if let Err(e) = set_nested_array_element::(&account, "nested_array", 2, "field1", 99) { + return e; + } + if let Some(val) = get_nested_array_element::(&account, "nested_array", 2, "field1") { + let _ = trace_num("Read back nested_array[2].field1:", val.into()); + } else { + let _ = trace("Failed to read back nested_array[2].field1"); + return -1; + } + + // Update u32 nested array + let _ = trace("Updating nested_array_u32[1].value to 8888..."); + if let Err(e) = set_nested_array_element::(&account, "nested_array_u32", 1, "value", 8888) + { + return e; + } + if let Some(val) = get_nested_array_element::(&account, "nested_array_u32", 1, "value") { + let _ = trace_num("Read back updated nested_array_u32[1].value:", val.into()); + } else { + let _ = trace("Failed to read back nested_array_u32[1].value"); + return -1; + } + + let _ = trace("Object with nested arrays update tests passed!"); + 0 +} diff --git a/src/test/app/wasm_fixtures/copyFixtures.py b/src/test/app/wasm_fixtures/copyFixtures.py index 3d9faddbed..e5c666c05b 100644 --- a/src/test/app/wasm_fixtures/copyFixtures.py +++ b/src/test/app/wasm_fixtures/copyFixtures.py @@ -55,7 +55,12 @@ def process_rust(project_name): ")" ) try: - subprocess.run(build_cmd, shell=True, check=True) + result = subprocess.run( + build_cmd, shell=True, check=True, capture_output=True, text=True + ) + print(f"stdout: {result.stdout}") + if result.stderr: + print(f"stderr: {result.stderr}") print(f"WASM file for {project_name} has been built and optimized.") except subprocess.CalledProcessError as e: print(f"exec error: {e}") @@ -87,7 +92,12 @@ def process_c(project_name): f"&& wasm-opt {wasm_path} {OPT} -o {wasm_path}" ) try: - subprocess.run(build_cmd, shell=True, check=True) + result = subprocess.run( + build_cmd, shell=True, check=True, capture_output=True, text=True + ) + print(f"stdout: {result.stdout}") + if result.stderr: + print(f"stderr: {result.stderr}") print( f"WASM file for {project_name} has been built with WASI support using clang." ) diff --git a/src/test/app/wasm_fixtures/emit_txn/Cargo.toml b/src/test/app/wasm_fixtures/emit_txn/Cargo.toml new file mode 100644 index 0000000000..6ae66a5914 --- /dev/null +++ b/src/test/app/wasm_fixtures/emit_txn/Cargo.toml @@ -0,0 +1,21 @@ +[package] +name = "emit_txn" +version = "0.0.1" +edition = "2024" + +# This empty workspace definition keeps this project independent of the parent workspace +[workspace] + +[lib] +crate-type = ["cdylib"] + +[profile.release] +lto = true +opt-level = 's' +panic = "abort" + +[dependencies] +xrpl-wasm-std = { git = "https://github.com/Transia-RnD/craft.git", branch = "dangell/smart-contracts", package = "xrpl-wasm-std" } + +[profile.dev] +panic = "abort" diff --git a/src/test/app/wasm_fixtures/emit_txn/src/lib.rs b/src/test/app/wasm_fixtures/emit_txn/src/lib.rs new file mode 100644 index 0000000000..a50df3e783 --- /dev/null +++ b/src/test/app/wasm_fixtures/emit_txn/src/lib.rs @@ -0,0 +1,218 @@ +#![cfg_attr(target_arch = "wasm32", no_std)] + +#[cfg(not(target_arch = "wasm32"))] +extern crate std; + +use xrpl_wasm_std::core::current_tx::contract_call::{ContractCall, get_current_contract_call}; +use xrpl_wasm_std::core::current_tx::traits::TransactionCommonFields; +use xrpl_wasm_std::core::submit::inner_objects::build_memo; +use xrpl_wasm_std::core::transaction_types::TT_PAYMENT; +use xrpl_wasm_std::core::types::account_id::AccountID; +use xrpl_wasm_std::host::{add_txn_field, build_txn, emit_built_txn}; +use xrpl_wasm_std::sfield; + +// ============================================================================ +// Constants +// ============================================================================ + +/// Custom error code for transaction failures +const CUSTOM_ERROR_CODE: i32 = -18; + +/// XRPL encoding markers +mod markers { + pub const ARRAY_END: u8 = 0xF1; + pub const OBJECT_END: u8 = 0xE1; +} + +/// Buffer sizes +mod buffer_sizes { + pub const MEMO_BUFFER: usize = 256; + pub const MEMOS_ARRAY: usize = 1024; + pub const DESTINATION: usize = 21; +} + +// ============================================================================ +// Helper Functions +// ============================================================================ + +/// Builds a complete memos array from individual memo buffers +/// +/// # Arguments +/// * `buffer` - Output buffer for the complete memos array +/// * `memo_buffers` - Slice of memo data and their lengths +/// +/// # Returns +/// Total length of the memos array including the end marker +fn build_memos_array( + buffer: &mut [u8; buffer_sizes::MEMOS_ARRAY], + memo_buffers: &[(&[u8], usize)] +) -> usize { + let mut position = 0; + + // Copy each memo into the array + for (memo_data, memo_length) in memo_buffers { + buffer[position..position + memo_length].copy_from_slice(&memo_data[..*memo_length]); + position += memo_length; + } + + // Terminate the array + buffer[position] = markers::ARRAY_END; + position + 1 +} + +/// Adds the amount field to the transaction +/// +/// # Arguments +/// * `txn_index` - Transaction builder index +/// * `amount_drops` - Amount in drops (192 in this example) +/// +/// # Returns +/// Result code from add_txn_field +unsafe fn add_amount_field(txn_index: i32) -> i32 { + // 192 drops encoded as XRPL Amount + const AMOUNT_BYTES: [u8; 8] = [ + 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0 + ]; + + add_txn_field( + txn_index, + sfield::Amount, + AMOUNT_BYTES.as_ptr(), + AMOUNT_BYTES.len() + ) +} + +/// Adds the destination field to the transaction +/// +/// # Arguments +/// * `txn_index` - Transaction builder index +/// * `destination` - Destination account ID +/// +/// # Returns +/// Result code from add_txn_field +unsafe fn add_destination_field(txn_index: i32, destination: &AccountID) -> i32 { + let mut dest_buffer = [0u8; buffer_sizes::DESTINATION]; + dest_buffer[0] = 0x14; // Length prefix for 20-byte account + dest_buffer[1..21].copy_from_slice(&destination.0); + + add_txn_field( + txn_index, + sfield::Destination, + dest_buffer.as_ptr(), + dest_buffer.len() + ) +} + +/// Adds the memos field to the transaction +/// +/// # Arguments +/// * `txn_index` - Transaction builder index +/// +/// # Returns +/// Result code from add_txn_field +unsafe fn add_memos_field(txn_index: i32) -> i32 { + use core::mem::MaybeUninit; + + // Uninitialized backing buffer (no zeroing => no memory.fill) + let mut memos_uninit: MaybeUninit<[u8; buffer_sizes::MEMOS_ARRAY]> = MaybeUninit::uninit(); + let base = memos_uninit.as_mut_ptr() as *mut u8; + + // Helper: get a 256-byte window at current position + #[inline(always)] + unsafe fn at<'a>(base: *mut u8, pos: usize) -> &'a mut [u8; buffer_sizes::MEMO_BUFFER] { + &mut *(base.add(pos) as *mut [u8; buffer_sizes::MEMO_BUFFER]) + } + + let mut pos = 0usize; + + // Write each Memo directly into the big buffer + let len1 = build_memo( + at(base, pos), + Some(b"invoice"), + Some(b"INV-2024-001"), + Some(b"text/plain") + ); + pos += len1; + + let len2 = build_memo( + at(base, pos), + Some(b"note"), + Some(b"Payment for consulting services"), + Some(b"text/plain") + ); + pos += len2; + + let len3 = build_memo( + at(base, pos), + None, + Some(b"Additional reference: Project Alpha"), + None + ); + pos += len3; + + // Terminate the array + *base.add(pos) = markers::ARRAY_END; + pos += 1; + + add_txn_field( + txn_index, + sfield::Memos, + base, + pos + ) +} + +// ============================================================================ +// Main Entry Point +// ============================================================================ + +/// Main hook function that builds and emits a payment transaction with memos +/// +/// This function: +/// 1. Retrieves the current contract call context +/// 2. Builds a payment transaction +/// 3. Adds amount, destination, and memos fields +/// 4. Emits the completed transaction +/// +/// # Returns +/// - 0 on success +/// - Negative error code on failure +#[unsafe(no_mangle)] +pub extern "C" fn emit() -> i32 { + // Get contract context + let contract_call: ContractCall = get_current_contract_call(); + let account = contract_call.get_account().unwrap(); + + // Initialize payment transaction + let txn_index = 0; + let build_result = unsafe { build_txn(TT_PAYMENT) }; + if build_result < 0 { + return CUSTOM_ERROR_CODE; + } + + // Build transaction fields + unsafe { + // Add amount field + if add_amount_field(txn_index) < 0 { + return CUSTOM_ERROR_CODE; + } + + // Add destination field + if add_destination_field(txn_index, &account) < 0 { + return CUSTOM_ERROR_CODE; + } + + // Add memos field + if add_memos_field(txn_index) < 0 { + return CUSTOM_ERROR_CODE; + } + + // Emit the completed transaction + let emission_result = emit_built_txn(txn_index); + if emission_result < 0 { + return emission_result; + } + } + + 0 // Success +} diff --git a/src/test/app/wasm_fixtures/events/Cargo.toml b/src/test/app/wasm_fixtures/events/Cargo.toml new file mode 100644 index 0000000000..ce249ac9b0 --- /dev/null +++ b/src/test/app/wasm_fixtures/events/Cargo.toml @@ -0,0 +1,21 @@ +[package] +name = "events" +version = "0.0.1" +edition = "2024" + +# This empty workspace definition keeps this project independent of the parent workspace +[workspace] + +[lib] +crate-type = ["cdylib"] + +[profile.release] +lto = true +opt-level = 's' +panic = "abort" + +[dependencies] +xrpl-wasm-std = { git = "https://github.com/Transia-RnD/craft.git", branch = "dangell/smart-contracts", package = "xrpl-wasm-std" } + +[profile.dev] +panic = "abort" diff --git a/src/test/app/wasm_fixtures/events/src/lib.rs b/src/test/app/wasm_fixtures/events/src/lib.rs new file mode 100644 index 0000000000..7cc8d0537c --- /dev/null +++ b/src/test/app/wasm_fixtures/events/src/lib.rs @@ -0,0 +1,99 @@ +#![cfg_attr(target_arch = "wasm32", no_std)] + +#[cfg(not(target_arch = "wasm32"))] +extern crate std; + +use xrpl_wasm_std::core::types::account_id::AccountID; +use xrpl_wasm_std::core::event::codec_v2::{ + EventBuffer, event_add_u8, event_add_u16, event_add_u32, event_add_u64, + event_add_u128, event_add_u160, event_add_u192, event_add_u256, event_add_amount, event_add_account, + event_add_currency, event_add_str +}; + +#[unsafe(no_mangle)] +pub extern "C" fn events() -> i32 { + let mut buf = EventBuffer::new(); + + // STI_AMOUNT + const AMOUNT: [u8; 8] = [ + 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0 + ]; + if event_add_amount(&mut buf, "amount", &AMOUNT).is_err() { + return -1; + } + + // STI_CURRENCY + const CURRENCY: [u8; 20] = [ + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x55, 0x53, 0x44, 0x00, + 0x00, 0x00, 0x00, 0x00 + ]; + if event_add_currency(&mut buf, "currency", &CURRENCY).is_err() { + return -1; + } + + // STI_ACCOUNT + const ACCOUNT: [u8; 20] = [ + 0x59, 0x69, 0x15, 0xCF, 0xDE, 0xEE, 0x3A, 0x69, + 0x5B, 0x3E, 0xFD, 0x6B, 0xDA, 0x9A, 0xC7, 0x88, + 0xA3, 0x68, 0xB7, 0xB + ]; + let account = AccountID(ACCOUNT); + if event_add_account(&mut buf, "destination", &account.0).is_err() { + return -1; + } + + // STI_UINT128 + if event_add_u128(&mut buf, "uint128", &[0u8; 16]).is_err() { + return -1; + } + + // STI_UINT16 + if event_add_u16(&mut buf, "uint16", 16).is_err() { + return -1; + } + + // STI_UINT160 + if event_add_u160(&mut buf, "uint160", &[0u8; 20]).is_err() { + return -1; + } + + // STI_UINT192 + if event_add_u192(&mut buf, "uint192", &[0u8; 24]).is_err() { + return -1; + } + + // STI_UINT256 + if event_add_u256(&mut buf, "uint256", &[0u8; 32]).is_err() { + return -1; + } + + // STI_UINT32 + if event_add_u32(&mut buf, "uint32", 32).is_err() { + return -1; + } + + // STI_UINT64 + if event_add_u64(&mut buf, "uint64", 64).is_err() { + return -1; + } + + // STI_UINT8 + if event_add_u8(&mut buf, "uint8", 8).is_err() { + return -1; + } + + // STI_VL + if event_add_str(&mut buf, "vl", "Hello, World!").is_err() { + return -1; + } + + // STI_ISSUE (XRP) + // STI_ISSUE (IOU) + // STI_ISSUE (MPT) + + if buf.emit("event1").is_err() { + return -1; + } + 0 +} diff --git a/src/test/app/wasm_fixtures/parameters/Cargo.lock b/src/test/app/wasm_fixtures/parameters/Cargo.lock new file mode 100644 index 0000000000..c42d205b52 --- /dev/null +++ b/src/test/app/wasm_fixtures/parameters/Cargo.lock @@ -0,0 +1,15 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "parameters" +version = "0.0.1" +dependencies = [ + "xrpl-wasm-std", +] + +[[package]] +name = "xrpl-wasm-std" +version = "0.5.1-devnet5" +source = "git+https://github.com/Transia-RnD/craft.git?branch=dangell%2Fsmart-contracts#3c8191ae9832ea25f7d8f3e5eeb33b65181d31b5" diff --git a/src/test/app/wasm_fixtures/parameters/Cargo.toml b/src/test/app/wasm_fixtures/parameters/Cargo.toml new file mode 100644 index 0000000000..ab2406a30f --- /dev/null +++ b/src/test/app/wasm_fixtures/parameters/Cargo.toml @@ -0,0 +1,21 @@ +[package] +name = "parameters" +version = "0.0.1" +edition = "2024" + +# This empty workspace definition keeps this project independent of the parent workspace +[workspace] + +[lib] +crate-type = ["cdylib"] + +[profile.release] +lto = true +opt-level = 's' +panic = "abort" + +[dependencies] +xrpl-wasm-std = { git = "https://github.com/Transia-RnD/craft.git", branch = "dangell/smart-contracts", package = "xrpl-wasm-std" } + +[profile.dev] +panic = "abort" diff --git a/src/test/app/wasm_fixtures/parameters/src/lib.rs b/src/test/app/wasm_fixtures/parameters/src/lib.rs new file mode 100644 index 0000000000..e03abbf4c9 --- /dev/null +++ b/src/test/app/wasm_fixtures/parameters/src/lib.rs @@ -0,0 +1,751 @@ +#![allow(unused_imports)] +#![cfg_attr(target_arch = "wasm32", no_std)] + +#[cfg(not(target_arch = "wasm32"))] +extern crate std; + +use xrpl_wasm_std::core::params::function::get_function_param; +use xrpl_wasm_std::core::params::instance::get_instance_param; +use xrpl_wasm_std::core::type_codes::{ + STI_ACCOUNT, STI_AMOUNT, STI_ARRAY, STI_CURRENCY, STI_NUMBER, STI_OBJECT, STI_UINT128, + STI_UINT16, STI_UINT160, STI_UINT192, STI_UINT256, STI_UINT32, STI_UINT64, STI_UINT8, STI_VL, +}; +use xrpl_wasm_std::core::types::account_id::AccountID; +use xrpl_wasm_std::core::types::amount::opaque_float::OpaqueFloat; +use xrpl_wasm_std::core::types::amount::opaque_float::{FLOAT_NEGATIVE_ONE, FLOAT_ONE}; +use xrpl_wasm_std::core::types::amount::token_amount::TokenAmount; +use xrpl_wasm_std::core::types::hash_256::Hash256; +use xrpl_wasm_std::core::types::number::Number; +use xrpl_wasm_std::core::types::uint_160::UInt160; +use xrpl_wasm_std::core::types::uint_192::UInt192; +use xrpl_wasm_std::host::trace::{trace, trace_data, trace_float, trace_num, DataRepr}; +use xrpl_wasm_std::host::{float_add, float_set, FLOAT_ROUNDING_MODES_TO_NEAREST}; +use xrpl_wasm_std::host::{function_param, instance_param}; + +#[unsafe(no_mangle)] +pub extern "C" fn function_params() -> i32 { + // UINT8 + let value = match get_function_param::(0) { + Ok(a) => a, + Err(err) => { + let _ = trace_num("UINT8 Parameter Error Code:", err as i64); + return -1; + } + }; + let _ = trace_num("UINT8 Value:", value as i64); + // as hex + let _ = trace_data("UINT8 Hex:", &[value], DataRepr::AsHex); + + // TODO: replace with require + if value != 255 { + let _ = trace("UINT8 Parameter Error: Invalid Value"); + return -1; + } + + // UINT16 + let value = match get_function_param::(1) { + Ok(a) => a, + Err(err) => { + let _ = trace_num("UINT16 Parameter Error Code:", err as i64); + return -1; + } + }; + let _ = trace_num("UINT16 Value:", value as i64); + // as hex + let buf = value.to_le_bytes(); + let _ = trace_data("UINT16 Hex:", &buf, DataRepr::AsHex); + + // TODO: replace with require + if value != 65535 { + let _ = trace("UINT16 Parameter Error: Invalid Value"); + return -1; + } + + // UINT32 + let value = match get_function_param::(2) { + Ok(a) => a, + Err(err) => { + let _ = trace_num("UINT32 Parameter Error Code:", err as i64); + return -1; + } + }; + // as hex + let buf = value.to_le_bytes(); + let _ = trace_data("UINT32 Hex:", &buf, DataRepr::AsHex); + + // TODO: replace with require + if value != 4294967295 { + let _ = trace("UINT32 Parameter Error: Invalid Value"); + return -1; + } + + // UINT64 + let value = match get_function_param::(3) { + Ok(a) => a, + Err(err) => { + let _ = trace_num("UINT64 Parameter Error Code:", err as i64); + return -1; + } + }; + let _ = trace_num("UINT64 Value:", value as i64); + // as hex + let buf = value.to_le_bytes(); + let _ = trace_data("UINT64 Hex:", &buf, DataRepr::AsHex); + + // TODO: replace with require + if buf != [0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF] { + let _ = trace("UINT64 Parameter Error: Invalid Value"); + return -1; + } + + // UINT128 + let value = match get_function_param::(4) { + Ok(a) => a, + Err(err) => { + let _ = trace_num("UINT128 Parameter Error Code:", err as i64); + return -1; + } + }; + // as hex + let buf = value.to_le_bytes(); + let _ = trace_data("UINT128 Hex:", &buf, DataRepr::AsHex); + + // TODO: replace with require + if buf + != [ + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x01, + ] + { + let _ = trace("UINT128 Parameter Error: Invalid Value"); + return -1; + } + + // UINT160 + let value = match get_function_param::(5) { + Ok(a) => a, + Err(err) => { + let _ = trace_num("UINT160 Parameter Error Code:", err as i64); + return -1; + } + }; + // as hex + let buf = value.as_bytes(); + let _ = trace_data("UINT160 Hex:", buf, DataRepr::AsHex); + + // TODO: replace with require + let expected190: [u8; 20] = [ + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x01, + ]; + if *buf != expected190 { + let _ = trace("UINT160 Parameter Error: Invalid Value"); + return -1; + } + + // UINT192 + let value = match get_function_param::(6) { + Ok(a) => a, + Err(err) => { + let _ = trace_num("UINT192 Parameter Error Code:", err as i64); + return -1; + } + }; + // as hex + let buf = value.as_bytes(); + let _ = trace_data("UINT192 Hex:", buf, DataRepr::AsHex); + + // TODO: replace with require + let expected192: [u8; 24] = [ + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, + ]; + if *buf != expected192 { + let _ = trace("UINT192 Parameter Error: Invalid Value"); + return -1; + } + + // UINT256 + let value = match get_function_param::(7) { + Ok(a) => a, + Err(err) => { + let _ = trace_num("UINT256 Parameter Error Code:", err as i64); + return -1; + } + }; + // as hex + let buf = value.as_bytes(); + let _ = trace_data("UINT256 Hex:", buf, DataRepr::AsHex); + + // TODO: replace with require + let expected256: [u8; 32] = [ + 0xD9, 0x55, 0xDA, 0xC2, 0xE7, 0x75, 0x19, 0xF0, 0x5A, 0xD1, 0x51, 0xA5, 0xD3, 0xC9, 0x9F, + 0xC8, 0x12, 0x5F, 0xB3, 0x9D, 0x58, 0xFF, 0x9F, 0x10, 0x6F, 0x1A, 0xCA, 0x44, 0x91, 0x90, + 0x2C, 0x25, + ]; + if *buf != expected256 { + let _ = trace("UINT256 Parameter Error: Invalid Value"); + return -1; + } + + // // VL + // let mut buf = [0x00; 4]; + // let output_len = unsafe { function_param(8, STI_VL.into(), buf.as_mut_ptr(), buf.len()) }; + // let _ = trace_num("VL Value Len:", output_len as i64); + // // as hex + // let _ = trace_data("VL Hex:", &buf[0..4], DataRepr::AsHex); + + // ACCOUNT + let account_id = match get_function_param::(9) { + Ok(a) => a, + Err(err) => { + let _ = trace_num("ACCOUNT Parameter Error Code:", err as i64); + return -1; + } + }; + // trace the value + let _ = trace_data("ACCOUNT Value:", &account_id.0, DataRepr::AsHex); + + // TODO: replace with require + let expectedAccount: [u8; 20] = [ + 0xAE, 0x12, 0x3A, 0x85, 0x56, 0xF3, 0xCF, 0x91, 0x15, 0x47, 0x11, 0x37, 0x6A, 0xFB, 0x0F, + 0x89, 0x4F, 0x83, 0x2B, 0x3D, + ]; + if account_id.0 != expectedAccount { + let _ = trace("ACCOUNT Parameter Error: Invalid Value"); + return -1; + } + + // AMOUNT XRP + let xrp_token = match get_function_param::(10) { + Ok(a) => a, + Err(err) => { + let _ = trace_num("AMOUNT XRP Parameter Error Code:", err as i64); + return -1; + } + }; + match xrp_token { + TokenAmount::XRP { num_drops } => { + let _ = trace_num("AMOUNT Value (XRP):", num_drops); + } + _ => { + let _ = trace_num("AMOUNT Value (XRP):", -1); + } + } + let buf = match xrp_token { + TokenAmount::XRP { num_drops } => num_drops.to_le_bytes(), + _ => [0u8; 8], + }; + let _ = trace_data("AMOUNT Hex:", &buf, DataRepr::AsHex); + + // TODO: replace with require + if let TokenAmount::XRP { num_drops } = xrp_token { + if num_drops != 1000000 { + let _ = trace("AMOUNT.XRP Parameter Error: Invalid Value"); + return -1; + } + } else { + let _ = trace("AMOUNT.XRP Parameter Error: Invalid Type"); + return -1; + } + + // AMOUNT IOU + let iou_token = match get_function_param::(11) { + Ok(a) => a, + Err(err) => { + let _ = trace_num("AMOUNT IOU Parameter Error Code:", err as i64); + return -1; + } + }; + let (iou_amount, iou_issuer, iou_currency) = match &iou_token { + TokenAmount::IOU { + amount, + issuer, + currency_code, + } => { + // trace amount hex + let _ = trace_data("AMOUNT Value (IOU):", &amount.0, DataRepr::AsHex); + let _ = trace_float("AMOUNT Value (IOU) - Original:", &amount.0); + let _ = trace_data("IOU Issuer:", &issuer.0, DataRepr::AsHex); + let _ = trace_data("IOU Currency:", ¤cy_code.0, DataRepr::AsHex); + + // Add FLOAT_ONE to the IOU amount + let mut new_amount: [u8; 8] = [0u8; 8]; + let result = unsafe { + float_add( + amount.0.as_ptr(), + 8, + FLOAT_ONE.as_ptr(), + 8, + new_amount.as_mut_ptr(), + 8, + FLOAT_ROUNDING_MODES_TO_NEAREST, + ) + }; + + if result == 8 { + // trace hex of the new amount + let _ = trace_data( + "AMOUNT Value (IOU) - After adding 1:", + &new_amount, + DataRepr::AsHex, + ); + let _ = trace_float("AMOUNT Value (IOU) - After adding 1:", &new_amount); + + // Create a new TokenAmount with the updated amount + let updated_token = TokenAmount::IOU { + amount: new_amount.into(), + issuer: *issuer, + currency_code: *currency_code, + }; + + // You now have the updated token amount in `updated_token` + // and the raw float bytes in `new_amount` + } else { + let _ = trace_num( + "Error adding FLOAT_ONE to IOU amount, result:", + result as i64, + ); + } + + (Some(*amount), Some(*issuer), Some(*currency_code)) + } + _ => { + let _ = trace_data("AMOUNT Value (IOU):", &[0u8; 8], DataRepr::AsHex); + (None, None, None) + } + }; + // trace new iou_amount as hex + if let Some(amount) = iou_amount { + let _ = trace_data("IOU Amount:", &amount.0, DataRepr::AsHex); + } else { + let _ = trace_data("IOU Amount:", &[0u8; 8], DataRepr::AsHex); + } + + // TODO: replace with require + if iou_amount.is_none() { + let _ = trace("AMOUNT.IOU Parameter Error: Invalid Type"); + return -1; + } + + // let mut buf = [0x00; 12]; + // let output_len = unsafe { function_param(12, STI_NUMBER.into(), buf.as_mut_ptr(), buf.len()) }; + // let _ = trace_num("NUMBER Value Len:", output_len as i64); + + // NUMBER + let number = match get_function_param::(12) { + Ok(a) => a, + Err(err) => { + let _ = trace_num("NUMBER Parameter Error Code:", err as i64); + return -1; + } + }; + // trace the value + let buf = number.as_bytes(); + let _ = trace_data("NUMBER Value:", &buf, DataRepr::AsHex); + + // TODO: replace with require + let expectedNumber: [u8; 12] = [ + 0x00, 0x04, 0x43, 0x64, 0xC5, 0xBB, 0x00, 0x00, 0xFF, 0xFF, 0xFF, 0xF1, + ]; + if buf != expectedNumber { + let _ = trace("NUMBER Parameter Error: Invalid Value"); + return -1; + } + + // // Parse Number to get mantissa and exponent + // let stnum = Number::from(&buf).unwrap(); + let _ = trace_num("NUMBER Mantissa:", number.mantissa); + let _ = trace_num("NUMBER Exponent:", number.exponent as i64); + + let mut opaque_float_buf = [0x00; 8]; + let result = unsafe { + float_set( + number.exponent, + number.mantissa, + opaque_float_buf.as_mut_ptr(), + 8, + FLOAT_ROUNDING_MODES_TO_NEAREST, + ) + }; + + let opaque = OpaqueFloat::from(opaque_float_buf); + let _ = trace_float("NUMBER as OpaqueFloat:", &opaque.0); + let _ = trace_data( + "NUMBER OpaqueFloat Hex:", + &opaque_float_buf, + DataRepr::AsHex, + ); + + // AMOUNT (MPT) + // ISSUE (XRP) + // ISSUE (IOU) + // ISSUE (MPT) + // CURRENCY + + return 0; // Return success code +} + +#[unsafe(no_mangle)] +pub extern "C" fn instance_params() -> i32 { + // UINT8 + let value = match get_instance_param::(1) { + Ok(a) => a, + Err(err) => { + let _ = trace_num("UINT8 Parameter Error Code:", err as i64); + return -1; + } + }; + let _ = trace_num("UINT8 Value:", value as i64); + // as hex + let _ = trace_data("UINT8 Hex:", &[value], DataRepr::AsHex); + + // TODO: replace with require + if value != 255 { + let _ = trace("UINT8 Parameter Error: Invalid Value"); + return -1; + } + + // UINT16 + let value = match get_instance_param::(2) { + Ok(a) => a, + Err(err) => { + let _ = trace_num("UINT16 Parameter Error Code:", err as i64); + return -1; + } + }; + let _ = trace_num("UINT16 Value:", value as i64); + // as hex + let buf = value.to_le_bytes(); + let _ = trace_data("UINT16 Hex:", &buf, DataRepr::AsHex); + + // TODO: replace with require + if value != 65535 { + let _ = trace("UINT16 Parameter Error: Invalid Value"); + return -1; + } + + // UINT32 + let value = match get_instance_param::(3) { + Ok(a) => a, + Err(err) => { + let _ = trace_num("UINT32 Parameter Error Code:", err as i64); + return -1; + } + }; + // as hex + let buf = value.to_le_bytes(); + let _ = trace_data("UINT32 Hex:", &buf, DataRepr::AsHex); + + // TODO: replace with require + if value != 4294967295 { + let _ = trace("UINT32 Parameter Error: Invalid Value"); + return -1; + } + + // UINT64 + let value = match get_instance_param::(4) { + Ok(a) => a, + Err(err) => { + let _ = trace_num("UINT64 Parameter Error Code:", err as i64); + return -1; + } + }; + let _ = trace_num("UINT64 Value:", value as i64); + // as hex + let buf = value.to_le_bytes(); + let _ = trace_data("UINT64 Hex:", &buf, DataRepr::AsHex); + + // TODO: replace with require + if buf != [0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF] { + let _ = trace("UINT64 Parameter Error: Invalid Value"); + return -1; + } + + // UINT128 + let value = match get_instance_param::(5) { + Ok(a) => a, + Err(err) => { + let _ = trace_num("UINT128 Parameter Error Code:", err as i64); + return -1; + } + }; + // as hex + let buf = value.to_le_bytes(); + let _ = trace_data("UINT128 Hex:", &buf, DataRepr::AsHex); + + // TODO: replace with require + if buf + != [ + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x01, + ] + { + let _ = trace("UINT128 Parameter Error: Invalid Value"); + return -1; + } + + // UINT160 + let value = match get_instance_param::(6) { + Ok(a) => a, + Err(err) => { + let _ = trace_num("UINT160 Parameter Error Code:", err as i64); + return -1; + } + }; + // as hex + let buf = value.as_bytes(); + let _ = trace_data("UINT160 Hex:", buf, DataRepr::AsHex); + + // TODO: replace with require + let expected190: [u8; 20] = [ + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x01, + ]; + if *buf != expected190 { + let _ = trace("UINT160 Parameter Error: Invalid Value"); + return -1; + } + + // UINT192 + let value = match get_instance_param::(7) { + Ok(a) => a, + Err(err) => { + let _ = trace_num("UINT192 Parameter Error Code:", err as i64); + return -1; + } + }; + // as hex + let buf = value.as_bytes(); + let _ = trace_data("UINT192 Hex:", buf, DataRepr::AsHex); + + // TODO: replace with require + let expected192: [u8; 24] = [ + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, + ]; + if *buf != expected192 { + let _ = trace("UINT192 Parameter Error: Invalid Value"); + return -1; + } + + // UINT256 + let value = match get_instance_param::(8) { + Ok(a) => a, + Err(err) => { + let _ = trace_num("UINT256 Parameter Error Code:", err as i64); + return -1; + } + }; + // as hex + let buf = value.as_bytes(); + let _ = trace_data("UINT256 Hex:", buf, DataRepr::AsHex); + + // TODO: replace with require + let expected256: [u8; 32] = [ + 0xD9, 0x55, 0xDA, 0xC2, 0xE7, 0x75, 0x19, 0xF0, 0x5A, 0xD1, 0x51, 0xA5, 0xD3, 0xC9, 0x9F, + 0xC8, 0x12, 0x5F, 0xB3, 0x9D, 0x58, 0xFF, 0x9F, 0x10, 0x6F, 0x1A, 0xCA, 0x44, 0x91, 0x90, + 0x2C, 0x25, + ]; + if *buf != expected256 { + let _ = trace("UINT256 Parameter Error: Invalid Value"); + return -1; + } + + // // VL + // let mut buf = [0x00; 4]; + // let output_len = unsafe { instance_param(8, STI_VL.into(), buf.as_mut_ptr(), buf.len()) }; + // let _ = trace_num("VL Value Len:", output_len as i64); + // // as hex + // let _ = trace_data("VL Hex:", &buf[0..4], DataRepr::AsHex); + + // ACCOUNT + let account_id = match get_instance_param::(10) { + Ok(a) => a, + Err(err) => { + let _ = trace_num("ACCOUNT Parameter Error Code:", err as i64); + return -1; + } + }; + // trace the value + let _ = trace_data("ACCOUNT Value:", &account_id.0, DataRepr::AsHex); + + // TODO: replace with require + let expectedAccount: [u8; 20] = [ + 0xAE, 0x12, 0x3A, 0x85, 0x56, 0xF3, 0xCF, 0x91, 0x15, 0x47, 0x11, 0x37, 0x6A, 0xFB, 0x0F, + 0x89, 0x4F, 0x83, 0x2B, 0x3D, + ]; + if account_id.0 != expectedAccount { + let _ = trace("ACCOUNT Parameter Error: Invalid Value"); + return -1; + } + + // AMOUNT XRP + let xrp_token = match get_instance_param::(11) { + Ok(a) => a, + Err(err) => { + let _ = trace_num("AMOUNT XRP Parameter Error Code:", err as i64); + return -1; + } + }; + match xrp_token { + TokenAmount::XRP { num_drops } => { + let _ = trace_num("AMOUNT Value (XRP):", num_drops); + } + _ => { + let _ = trace_num("AMOUNT Value (XRP):", -1); + } + } + let buf = match xrp_token { + TokenAmount::XRP { num_drops } => num_drops.to_le_bytes(), + _ => [0u8; 8], + }; + let _ = trace_data("AMOUNT Hex:", &buf, DataRepr::AsHex); + + // TODO: replace with require + if let TokenAmount::XRP { num_drops } = xrp_token { + if num_drops != 1000000 { + let _ = trace("AMOUNT.XRP Parameter Error: Invalid Value"); + return -1; + } + } else { + let _ = trace("AMOUNT.XRP Parameter Error: Invalid Type"); + return -1; + } + + // AMOUNT IOU + let iou_token = match get_instance_param::(12) { + Ok(a) => a, + Err(err) => { + let _ = trace_num("AMOUNT IOU Parameter Error Code:", err as i64); + return -1; + } + }; + let (iou_amount, iou_issuer, iou_currency) = match &iou_token { + TokenAmount::IOU { + amount, + issuer, + currency_code, + } => { + // trace amount hex + let _ = trace_data("AMOUNT Value (IOU):", &amount.0, DataRepr::AsHex); + let _ = trace_float("AMOUNT Value (IOU) - Original:", &amount.0); + let _ = trace_data("IOU Issuer:", &issuer.0, DataRepr::AsHex); + let _ = trace_data("IOU Currency:", ¤cy_code.0, DataRepr::AsHex); + + // Add FLOAT_ONE to the IOU amount + let mut new_amount: [u8; 8] = [0u8; 8]; + let result = unsafe { + float_add( + amount.0.as_ptr(), + 8, + FLOAT_ONE.as_ptr(), + 8, + new_amount.as_mut_ptr(), + 8, + FLOAT_ROUNDING_MODES_TO_NEAREST, + ) + }; + + if result == 8 { + // trace hex of the new amount + let _ = trace_data( + "AMOUNT Value (IOU) - After adding 1:", + &new_amount, + DataRepr::AsHex, + ); + let _ = trace_float("AMOUNT Value (IOU) - After adding 1:", &new_amount); + + // Create a new TokenAmount with the updated amount + let updated_token = TokenAmount::IOU { + amount: new_amount.into(), + issuer: *issuer, + currency_code: *currency_code, + }; + + // You now have the updated token amount in `updated_token` + // and the raw float bytes in `new_amount` + } else { + let _ = trace_num( + "Error adding FLOAT_ONE to IOU amount, result:", + result as i64, + ); + } + + (Some(*amount), Some(*issuer), Some(*currency_code)) + } + _ => { + let _ = trace_data("AMOUNT Value (IOU):", &[0u8; 8], DataRepr::AsHex); + (None, None, None) + } + }; + // trace new iou_amount as hex + if let Some(amount) = iou_amount { + let _ = trace_data("IOU Amount:", &amount.0, DataRepr::AsHex); + } else { + let _ = trace_data("IOU Amount:", &[0u8; 8], DataRepr::AsHex); + } + + // TODO: replace with require + if iou_amount.is_none() { + let _ = trace("AMOUNT.IOU Parameter Error: Invalid Type"); + return -1; + } + + // let mut buf = [0x00; 12]; + // let output_len = unsafe { instance_param(12, STI_NUMBER.into(), buf.as_mut_ptr(), buf.len()) }; + // let _ = trace_num("NUMBER Value Len:", output_len as i64); + + // NUMBER + let number = match get_instance_param::(13) { + Ok(a) => a, + Err(err) => { + let _ = trace_num("NUMBER Parameter Error Code:", err as i64); + return -1; + } + }; + // trace the value + let buf = number.as_bytes(); + let _ = trace_data("NUMBER Value:", &buf, DataRepr::AsHex); + + // TODO: replace with require + let expectedNumber: [u8; 12] = [ + 0x00, 0x04, 0x43, 0x64, 0xC5, 0xBB, 0x00, 0x00, 0xFF, 0xFF, 0xFF, 0xF1, + ]; + if buf != expectedNumber { + let _ = trace("NUMBER Parameter Error: Invalid Value"); + return -1; + } + + // // Parse Number to get mantissa and exponent + // let stnum = Number::from(&buf).unwrap(); + let _ = trace_num("NUMBER Mantissa:", number.mantissa); + let _ = trace_num("NUMBER Exponent:", number.exponent as i64); + + let mut opaque_float_buf = [0x00; 8]; + let result = unsafe { + float_set( + number.exponent, + number.mantissa, + opaque_float_buf.as_mut_ptr(), + 8, + FLOAT_ROUNDING_MODES_TO_NEAREST, + ) + }; + + let opaque = OpaqueFloat::from(opaque_float_buf); + let _ = trace_float("NUMBER as OpaqueFloat:", &opaque.0); + let _ = trace_data( + "NUMBER OpaqueFloat Hex:", + &opaque_float_buf, + DataRepr::AsHex, + ); + + // AMOUNT (MPT) + // ISSUE (XRP) + // ISSUE (IOU) + // ISSUE (MPT) + // CURRENCY + + return 0; // Return success code +} diff --git a/src/test/consensus/NegativeUNL_test.cpp b/src/test/consensus/NegativeUNL_test.cpp index 0f97704755..4b88bca634 100644 --- a/src/test/consensus/NegativeUNL_test.cpp +++ b/src/test/consensus/NegativeUNL_test.cpp @@ -5,6 +5,7 @@ #include #include +#include #include #include diff --git a/src/test/jtx.h b/src/test/jtx.h index d4b88b0b9e..fa5a0a466c 100644 --- a/src/test/jtx.h +++ b/src/test/jtx.h @@ -14,6 +14,7 @@ #include #include #include +#include #include #include #include diff --git a/src/test/jtx/contract.h b/src/test/jtx/contract.h new file mode 100644 index 0000000000..2cebb3c82d --- /dev/null +++ b/src/test/jtx/contract.h @@ -0,0 +1,164 @@ +#pragma once + +#include +#include +#include +#include +#include +#include + +#include + +#include "test/jtx/SignerUtils.h" + +#include +#include +#include + +namespace xrpl { +namespace test { +namespace jtx { + +/** Contract operations */ +namespace contract { + +Json::Value +create(jtx::Account const& account, std::string const& contractCode); + +Json::Value +create(jtx::Account const& account, uint256 const& contractHash); + +Json::Value +modify( + jtx::Account const& account, + jtx::Account const& contractAccount, + std::string const& contractCode); + +Json::Value +modify( + jtx::Account const& account, + jtx::Account const& contractAccount, + uint256 const& contractHash); + +Json::Value +modify(jtx::Account const& account, jtx::Account const& contractAccount, jtx::Account const& owner); + +Json::Value +del(jtx::Account const& account, jtx::Account const& contractAccount); + +Json::Value +call( + jtx::Account const& account, + jtx::Account const& contractAccount, + std::string const& functionName); + +Json::Value +userDelete(jtx::Account const& account, jtx::Account const& contractAccount); + +/** Add Function on a JTx. */ +class add_function +{ +private: + std::string const name_; + std::vector> call_params_; + +public: + explicit add_function( + std::string const& name, + std::vector> call_params) + : name_{name}, call_params_{std::move(call_params)} + { + } + + void + operator()(Env&, JTx& jtx) const; +}; + +/** Add Instance Parameter on a JTx. */ +template +class add_instance_param +{ +private: + std::uint32_t flags_; + std::string name_; + std::string type_; + T value_; + +public: + explicit add_instance_param( + std::uint32_t flags, + std::string const& name, + std::string const& type, + T value) + : flags_{flags}, name_{name}, type_{type}, value_{value} + { + } + + void + operator()(Env&, JTx& jtx) const + { + if (jtx.jv.isMember(sfContractCode.fieldName)) + { + // Add instance Parameters + if (!jtx.jv.isMember(sfInstanceParameters.fieldName)) + { + jtx.jv[sfInstanceParameters.fieldName] = Json::Value(Json::arrayValue); + } + Json::Value param = Json::Value(Json::objectValue); + param[sfInstanceParameter.fieldName][sfParameterFlag.fieldName] = flags_; + param[sfInstanceParameter.fieldName][sfParameterType.fieldName][jss::type] = type_; + jtx.jv[sfInstanceParameters.fieldName].append(param); + } + + // Add instance Parameter Values + if (!jtx.jv.isMember(sfInstanceParameterValues.fieldName)) + { + jtx.jv[sfInstanceParameterValues.fieldName] = Json::Value(Json::arrayValue); + } + Json::Value param = Json::Value(Json::objectValue); + param[sfInstanceParameterValue.fieldName][sfParameterFlag.fieldName] = flags_; + param[sfInstanceParameterValue.fieldName][sfParameterValue.fieldName][jss::type] = type_; + param[sfInstanceParameterValue.fieldName][sfParameterValue.fieldName][jss::value] = value_; + jtx.jv[sfInstanceParameterValues.fieldName].append(param); + } +}; + +/** Add Parameter Value on a JTx. */ +template +class add_param +{ +private: + std::uint32_t flags_; + std::string name_; + std::string type_; + T value_; + +public: + explicit add_param( + std::uint32_t flags, + std::string const& name, + std::string const& type, + T value) + : flags_(flags), name_(name), type_(type), value_(value) + { + } + + void + operator()(Env&, JTx& jtx) const + { + Json::Value param = Json::Value(Json::objectValue); + param[sfParameter] = Json::Value(Json::objectValue); + param[sfParameter][sfParameterFlag] = flags_; + param[sfParameter][sfParameterValue] = Json::Value(Json::objectValue); + param[sfParameter][sfParameterValue][jss::type] = type_; + param[sfParameter][sfParameterValue][jss::value] = value_; + jtx.jv[sfParameters].append(param); + } +}; + +} // namespace contract + +} // namespace jtx + +} // namespace test +} // namespace xrpl diff --git a/src/test/jtx/impl/contract.cpp b/src/test/jtx/impl/contract.cpp new file mode 100644 index 0000000000..f57b3921b4 --- /dev/null +++ b/src/test/jtx/impl/contract.cpp @@ -0,0 +1,134 @@ +#include +#include + +#include +#include + +namespace xrpl { +namespace test { +namespace jtx { + +namespace contract { + +Json::Value +create(jtx::Account const& account, std::string const& contractCode) +{ + Json::Value jv; + jv[jss::TransactionType] = jss::ContractCreate; + jv[jss::Account] = account.human(); + jv[sfContractCode] = contractCode; + return jv; +} + +Json::Value +create(jtx::Account const& account, uint256 const& contractHash) +{ + Json::Value jv; + jv[jss::TransactionType] = jss::ContractCreate; + jv[jss::Account] = account.human(); + jv[sfContractHash] = to_string(contractHash); + return jv; +} + +Json::Value +modify( + jtx::Account const& account, + jtx::Account const& contractAccount, + std::string const& contractCode) +{ + Json::Value jv; + jv[jss::TransactionType] = jss::ContractModify; + jv[jss::Account] = account.human(); + jv[sfContractAccount] = contractAccount.human(); + jv[sfContractCode] = contractCode; + return jv; +} + +Json::Value +modify( + jtx::Account const& account, + jtx::Account const& contractAccount, + uint256 const& contractHash) +{ + Json::Value jv; + jv[jss::TransactionType] = jss::ContractModify; + jv[jss::Account] = account.human(); + jv[sfContractAccount] = contractAccount.human(); + jv[sfContractHash] = to_string(contractHash); + return jv; +} + +Json::Value +modify(jtx::Account const& account, jtx::Account const& contractAccount, jtx::Account const& owner) +{ + Json::Value jv; + jv[jss::TransactionType] = jss::ContractModify; + jv[jss::Account] = account.human(); + jv[sfContractAccount] = contractAccount.human(); + jv[sfOwner] = owner.human(); + return jv; +} + +Json::Value +del(jtx::Account const& account, jtx::Account const& contractAccount) +{ + Json::Value jv; + jv[jss::TransactionType] = jss::ContractDelete; + jv[jss::Account] = account.human(); + jv[sfContractAccount] = contractAccount.human(); + return jv; +} + +Json::Value +call( + jtx::Account const& account, + jtx::Account const& contractAccount, + std::string const& functionName) +{ + Json::Value jv; + jv[jss::TransactionType] = jss::ContractCall; + jv[jss::Account] = account.human(); + jv[sfContractAccount] = contractAccount.human(); + jv[sfFunctionName] = strHex(functionName); + jv[sfParameters] = Json::Value(Json::arrayValue); + return jv; +} + +Json::Value +userDelete(jtx::Account const& account, jtx::Account const& contractAccount) +{ + Json::Value jv; + jv[jss::TransactionType] = jss::ContractUserDelete; + jv[jss::Account] = account.human(); + jv[sfContractAccount] = contractAccount.human(); + return jv; +} + +Json::Value +addCallParam(std::uint32_t const& flags, std::string const& name, std::string const& typeName) +{ + Json::Value param = Json::Value(Json::objectValue); + param[sfParameter][sfParameterFlag] = flags; + param[sfParameter][sfParameterType][jss::type] = typeName; + return param; +}; + +void +add_function::operator()(Env&, JTx& jt) const +{ + auto const index = jt.jv[sfFunctions].size(); + Json::Value& function = jt.jv[sfFunctions][index]; + + function = Json::Value{}; + function[sfFunction][sfFunctionName] = strHex(name_); + for (auto const& [p_flags, p_name, p_type] : call_params_) + { + function[sfFunction][sfParameters].append(addCallParam(p_flags, p_name, p_type)); + } +} + +} // namespace contract + +} // namespace jtx +} // namespace test +} // namespace xrpl diff --git a/src/test/protocol/Hooks_test.cpp b/src/test/protocol/Hooks_test.cpp deleted file mode 100644 index 53f20a2b5e..0000000000 --- a/src/test/protocol/Hooks_test.cpp +++ /dev/null @@ -1,180 +0,0 @@ -#include - -#include - -#include -#include - -namespace xrpl { - -class Hooks_test : public beast::unit_test::suite -{ - /** - * This unit test was requested here: - * https://github.com/ripple/rippled/pull/4089#issuecomment-1050274539 - * These are tests that exercise facilities that are reserved for when Hooks - * is merged in the future. - **/ - - void - testHookFields() - { - testcase("Test Hooks fields"); - - using namespace test::jtx; - - std::vector> const fields_to_test = { - sfHookResult, - sfHookStateChangeCount, - sfHookEmitCount, - sfHookExecutionIndex, - sfHookApiVersion, - sfHookStateCount, - sfEmitGeneration, - sfHookOn, - sfHookInstructionCount, - sfEmitBurden, - sfHookReturnCode, - sfReferenceCount, - sfEmitParentTxnID, - sfEmitNonce, - sfEmitHookHash, - sfHookStateKey, - sfHookHash, - sfHookNamespace, - sfHookSetTxnID, - sfHookStateData, - sfHookReturnString, - sfHookParameterName, - sfHookParameterValue, - sfEmitCallback, - sfHookAccount, - sfEmittedTxn, - sfHook, - sfHookDefinition, - sfHookParameter, - sfHookGrant, - sfEmitDetails, - sfHookExecutions, - sfHookExecution, - sfHookParameters, - sfHooks, - sfHookGrants}; - - for (auto const& rf : fields_to_test) - { - SField const& f = rf.get(); - - STObject dummy{sfGeneric}; - - BEAST_EXPECT(!dummy.isFieldPresent(f)); - - switch (f.fieldType) - { - case STI_UINT8: { - dummy.setFieldU8(f, 0); - BEAST_EXPECT(dummy.getFieldU8(f) == 0); - - dummy.setFieldU8(f, 255); - BEAST_EXPECT(dummy.getFieldU8(f) == 255); - - BEAST_EXPECT(dummy.isFieldPresent(f)); - break; - } - - case STI_UINT16: { - dummy.setFieldU16(f, 0); - BEAST_EXPECT(dummy.getFieldU16(f) == 0); - - dummy.setFieldU16(f, 0xFFFFU); - BEAST_EXPECT(dummy.getFieldU16(f) == 0xFFFFU); - - BEAST_EXPECT(dummy.isFieldPresent(f)); - break; - } - - case STI_UINT32: { - dummy.setFieldU32(f, 0); - BEAST_EXPECT(dummy.getFieldU32(f) == 0); - - dummy.setFieldU32(f, 0xFFFFFFFFU); - BEAST_EXPECT(dummy.getFieldU32(f) == 0xFFFFFFFFU); - - BEAST_EXPECT(dummy.isFieldPresent(f)); - break; - } - - case STI_UINT64: { - dummy.setFieldU64(f, 0); - BEAST_EXPECT(dummy.getFieldU64(f) == 0); - - dummy.setFieldU64(f, 0xFFFFFFFFFFFFFFFFU); - BEAST_EXPECT(dummy.getFieldU64(f) == 0xFFFFFFFFFFFFFFFFU); - - BEAST_EXPECT(dummy.isFieldPresent(f)); - break; - } - - case STI_UINT256: { - uint256 const u = uint256::fromVoid( - "DEADBEEFDEADBEEFDEADBEEFDEADBEEFDEADBEEFDEADBEEFDEADBE" - "EFDEADBEEF"); - dummy.setFieldH256(f, u); - BEAST_EXPECT(dummy.getFieldH256(f) == u); - BEAST_EXPECT(dummy.isFieldPresent(f)); - break; - } - - case STI_VL: { - std::vector const v{1, 2, 3}; - dummy.setFieldVL(f, v); - BEAST_EXPECT(dummy.getFieldVL(f) == v); - BEAST_EXPECT(dummy.isFieldPresent(f)); - break; - } - - case STI_ACCOUNT: { - // NOLINTNEXTLINE(bugprone-unchecked-optional-access) - AccountID const id = - *parseBase58("rwfSjJNK2YQuN64bSWn7T2eY9FJAyAPYJT"); - dummy.setAccountID(f, id); - BEAST_EXPECT(dummy.getAccountID(f) == id); - BEAST_EXPECT(dummy.isFieldPresent(f)); - break; - } - - case STI_OBJECT: { - dummy.emplace_back(STObject{f}); - BEAST_EXPECT(dummy.getField(f).getFName() == f); - BEAST_EXPECT(dummy.isFieldPresent(f)); - break; - } - - case STI_ARRAY: { - STArray dummy2{f, 2}; - dummy2.push_back(STObject{sfGeneric}); - dummy2.push_back(STObject{sfGeneric}); - dummy.setFieldArray(f, dummy2); - BEAST_EXPECT(dummy.getFieldArray(f) == dummy2); - BEAST_EXPECT(dummy.isFieldPresent(f)); - break; - } - - default: - BEAST_EXPECT(false); - } - } - } - -public: - void - run() override - { - using namespace test::jtx; - testHookFields(); - } -}; - -BEAST_DEFINE_TESTSUITE(Hooks, protocol, xrpl); - -} // namespace xrpl diff --git a/src/test/protocol/STDataType_test.cpp b/src/test/protocol/STDataType_test.cpp new file mode 100644 index 0000000000..f5828a4c82 --- /dev/null +++ b/src/test/protocol/STDataType_test.cpp @@ -0,0 +1,641 @@ +#include +#include +#include +#include +#include + +namespace xrpl { +struct STDataType_test : public beast::unit_test::suite +{ + void + testConstructors() + { + testcase("constructors"); + + auto const& sf = sfParameterType; + + // Test default constructor + { + STDataType dt1(sf); + BEAST_EXPECT(dt1.getInnerSType() == STI_NOTPRESENT); + BEAST_EXPECT(dt1.getSType() == STI_DATATYPE); + BEAST_EXPECT(dt1.getFName() == sf); + } + + // Test constructor with SerializedTypeID + { + STDataType dt2(sf, STI_UINT32); + BEAST_EXPECT(dt2.getInnerSType() == STI_UINT32); + BEAST_EXPECT(!dt2.isDefault()); + } + + // Test deserialization constructor + { + Serializer s; + s.add16(STI_UINT64); + SerialIter sit(s.slice()); + STDataType dt3(sit, sf); + BEAST_EXPECT(dt3.getInnerSType() == STI_UINT64); + } + } + + void + testCopyMove() + { + testcase("copy and move"); + + auto const& sf = sfParameterType; + + // Test copy + { + STDataType original(sf, STI_UINT32); + + // Use aligned storage for placement new + alignas(STDataType) char buffer[sizeof(STDataType)]; + STBase* copied = original.copy(sizeof(buffer), buffer); + + BEAST_EXPECT(copied != nullptr); + auto* dt_copy = dynamic_cast(copied); + BEAST_EXPECT(dt_copy != nullptr); + BEAST_EXPECT(dt_copy->getInnerSType() == STI_UINT32); + BEAST_EXPECT(dt_copy->getFName() == sf); + + // Clean up + dt_copy->~STDataType(); + } + + // Test move + { + STDataType original(sf, STI_UINT64); + + alignas(STDataType) char buffer[sizeof(STDataType)]; + STBase* moved = original.move(sizeof(buffer), buffer); + + BEAST_EXPECT(moved != nullptr); + auto* dt_moved = dynamic_cast(moved); + BEAST_EXPECT(dt_moved != nullptr); + BEAST_EXPECT(dt_moved->getInnerSType() == STI_UINT64); + BEAST_EXPECT(dt_moved->getFName() == sf); + + // Clean up + dt_moved->~STDataType(); + } + } + + void + testSerialization() + { + testcase("serialization"); + + auto const& sf = sfParameterType; + + // Test all type serializations + struct TypeTest + { + SerializedTypeID type; + std::string expectedHex; + }; + + TypeTest tests[] = { + {STI_UINT16, "0001"}, + {STI_UINT32, "0002"}, + {STI_UINT64, "0003"}, + {STI_UINT128, "0004"}, + {STI_UINT256, "0005"}, + {STI_AMOUNT, "0006"}, + {STI_VL, "0007"}, + {STI_ACCOUNT, "0008"}, + {STI_UINT8, "0010"}, + {STI_UINT160, "0011"}, + {STI_PATHSET, "0012"}, + {STI_VECTOR256, "0013"}, + {STI_OBJECT, "000E"}, + {STI_ARRAY, "000F"}, + {STI_ISSUE, "0018"}, + {STI_XCHAIN_BRIDGE, "0019"}, + {STI_CURRENCY, "001A"}, + {STI_UINT192, "0015"}, + {STI_NUMBER, "0009"}}; + + for (auto const& test : tests) + { + Serializer s; + STDataType dt(sf); + dt.setInnerSType(test.type); + BEAST_EXPECT(dt.getInnerSType() == test.type); + dt.add(s); + BEAST_EXPECT(strHex(s) == test.expectedHex); + } + } + + void + testEquivalence() + { + testcase("equivalence"); + + auto const& sf1 = sfParameterType; + + // Test equivalent objects + { + STDataType dt1(sf1, STI_UINT32); + STDataType dt2(sf1, STI_UINT32); + BEAST_EXPECT(dt1.isEquivalent(dt2)); + } + + // Test non-equivalent objects (different inner types) + { + STDataType dt1(sf1, STI_UINT32); + STDataType dt2(sf1, STI_UINT64); + BEAST_EXPECT(!dt1.isEquivalent(dt2)); + } + + // Test non-equivalent objects (different default states) + { + STDataType dt1(sf1); + STDataType dt2(sf1, STI_NOTPRESENT); + // dt1 has default_ = true (implicit from first constructor) + // dt2 has default_ = false (set in second constructor) + BEAST_EXPECT(!dt1.isEquivalent(dt2)); + } + + // Test with non-STDataType object + { + STDataType dt1(sf1, STI_UINT32); + // Create a dummy STBase-derived object for comparison + // Since we can't easily create other STBase types here, + // we'll test that isEquivalent returns false for nullptr cast + struct DummySTBase : public STBase + { + DummySTBase() : STBase(sfInvalid) + { + } + SerializedTypeID + getSType() const override + { + return STI_NOTPRESENT; + } + void + add(Serializer&) const override + { + } + bool + isEquivalent(STBase const&) const override + { + return false; + } + bool + isDefault() const override + { + return true; + } + STBase* + copy(std::size_t, void*) const override + { + return nullptr; + } + STBase* + move(std::size_t, void*) override + { + return nullptr; + } + }; + DummySTBase dummy; + BEAST_EXPECT(!dt1.isEquivalent(dummy)); + } + } + + void + testDefault() + { + testcase("isDefault"); + + auto const& sf = sfParameterType; + + // Test default state + { + STDataType dt1(sf); + // First constructor doesn't set default_ explicitly, + // so it should be true (member initialization) + BEAST_EXPECT(dt1.isDefault()); + } + + { + STDataType dt2(sf, STI_UINT32); + BEAST_EXPECT(!dt2.isDefault()); + } + } + + void + testGetText() + { + testcase("getText"); + + auto const& sf = sfParameterType; + + // Test known types + { + STDataType dt(sf, STI_UINT8); + BEAST_EXPECT(dt.getText() == "STDataType{InnerType: UINT8}"); + } + + { + STDataType dt(sf, STI_UINT16); + BEAST_EXPECT(dt.getText() == "STDataType{InnerType: UINT16}"); + } + + { + STDataType dt(sf, STI_UINT32); + BEAST_EXPECT(dt.getText() == "STDataType{InnerType: UINT32}"); + } + + { + STDataType dt(sf, STI_UINT64); + BEAST_EXPECT(dt.getText() == "STDataType{InnerType: UINT64}"); + } + + { + STDataType dt(sf, STI_UINT128); + BEAST_EXPECT(dt.getText() == "STDataType{InnerType: UINT128}"); + } + + { + STDataType dt(sf, STI_UINT160); + BEAST_EXPECT(dt.getText() == "STDataType{InnerType: UINT160}"); + } + + { + STDataType dt(sf, STI_UINT192); + BEAST_EXPECT(dt.getText() == "STDataType{InnerType: UINT192}"); + } + + { + STDataType dt(sf, STI_UINT256); + BEAST_EXPECT(dt.getText() == "STDataType{InnerType: UINT256}"); + } + + { + STDataType dt(sf, STI_VL); + BEAST_EXPECT(dt.getText() == "STDataType{InnerType: VL}"); + } + + { + STDataType dt(sf, STI_ACCOUNT); + BEAST_EXPECT(dt.getText() == "STDataType{InnerType: ACCOUNT}"); + } + + { + STDataType dt(sf, STI_AMOUNT); + BEAST_EXPECT(dt.getText() == "STDataType{InnerType: AMOUNT}"); + } + + { + STDataType dt(sf, STI_ISSUE); + BEAST_EXPECT(dt.getText() == "STDataType{InnerType: ISSUE}"); + } + + { + STDataType dt(sf, STI_CURRENCY); + BEAST_EXPECT(dt.getText() == "STDataType{InnerType: CURRENCY}"); + } + + { + STDataType dt(sf, STI_NUMBER); + BEAST_EXPECT(dt.getText() == "STDataType{InnerType: NUMBER}"); + } + + // Test unknown type (should return numeric string) + { + STDataType dt(sf, static_cast(999)); + BEAST_EXPECT(dt.getText() == "STDataType{InnerType: 999}"); + } + } + + void + testGetJson() + { + testcase("getJson"); + + auto const& sf = sfParameterType; + + // Test JSON output for various types + { + STDataType dt(sf, STI_UINT32); + Json::Value json = dt.getJson(JsonOptions::none); + BEAST_EXPECT(json.isObject()); + BEAST_EXPECT(json[jss::type].asString() == "UINT32"); + } + + { + STDataType dt(sf, STI_AMOUNT); + Json::Value json = dt.getJson(JsonOptions::none); + BEAST_EXPECT(json.isObject()); + BEAST_EXPECT(json[jss::type].asString() == "AMOUNT"); + } + + { + STDataType dt(sf, STI_ACCOUNT); + Json::Value json = dt.getJson(JsonOptions::none); + BEAST_EXPECT(json.isObject()); + BEAST_EXPECT(json[jss::type].asString() == "ACCOUNT"); + } + + // Test unknown type + { + STDataType dt(sf, static_cast(999)); + Json::Value json = dt.getJson(JsonOptions::none); + BEAST_EXPECT(json.isObject()); + BEAST_EXPECT(json[jss::type].asString() == "999"); + } + } + + void + testDataTypeFromJson() + { + testcase("dataTypeFromJson"); + + auto const& sf = sfParameterType; + + // Test all valid type strings + { + Json::Value v; + v[jss::type] = "UINT8"; + STDataType dt = dataTypeFromJson(sf, v); + BEAST_EXPECT(dt.getInnerSType() == STI_UINT8); + } + + { + Json::Value v; + v[jss::type] = "UINT16"; + STDataType dt = dataTypeFromJson(sf, v); + BEAST_EXPECT(dt.getInnerSType() == STI_UINT16); + } + + { + Json::Value v; + v[jss::type] = "UINT32"; + STDataType dt = dataTypeFromJson(sf, v); + BEAST_EXPECT(dt.getInnerSType() == STI_UINT32); + } + + { + Json::Value v; + v[jss::type] = "UINT64"; + STDataType dt = dataTypeFromJson(sf, v); + BEAST_EXPECT(dt.getInnerSType() == STI_UINT64); + } + + { + Json::Value v; + v[jss::type] = "UINT128"; + STDataType dt = dataTypeFromJson(sf, v); + BEAST_EXPECT(dt.getInnerSType() == STI_UINT128); + } + + { + Json::Value v; + v[jss::type] = "UINT160"; + STDataType dt = dataTypeFromJson(sf, v); + BEAST_EXPECT(dt.getInnerSType() == STI_UINT160); + } + + { + Json::Value v; + v[jss::type] = "UINT192"; + STDataType dt = dataTypeFromJson(sf, v); + BEAST_EXPECT(dt.getInnerSType() == STI_UINT192); + } + + { + Json::Value v; + v[jss::type] = "UINT256"; + STDataType dt = dataTypeFromJson(sf, v); + BEAST_EXPECT(dt.getInnerSType() == STI_UINT256); + } + + { + Json::Value v; + v[jss::type] = "VL"; + STDataType dt = dataTypeFromJson(sf, v); + BEAST_EXPECT(dt.getInnerSType() == STI_VL); + } + + { + Json::Value v; + v[jss::type] = "ACCOUNT"; + STDataType dt = dataTypeFromJson(sf, v); + BEAST_EXPECT(dt.getInnerSType() == STI_ACCOUNT); + } + + { + Json::Value v; + v[jss::type] = "AMOUNT"; + STDataType dt = dataTypeFromJson(sf, v); + BEAST_EXPECT(dt.getInnerSType() == STI_AMOUNT); + } + + { + Json::Value v; + v[jss::type] = "ISSUE"; + STDataType dt = dataTypeFromJson(sf, v); + BEAST_EXPECT(dt.getInnerSType() == STI_ISSUE); + } + + { + Json::Value v; + v[jss::type] = "CURRENCY"; + STDataType dt = dataTypeFromJson(sf, v); + BEAST_EXPECT(dt.getInnerSType() == STI_CURRENCY); + } + + { + Json::Value v; + v[jss::type] = "NUMBER"; + STDataType dt = dataTypeFromJson(sf, v); + BEAST_EXPECT(dt.getInnerSType() == STI_NUMBER); + } + + // Test error cases + + // Non-object JSON should throw + { + Json::Value v = "not an object"; + try + { + STDataType dt = dataTypeFromJson(sf, v); + BEAST_EXPECT(false); // Should not reach here + } + catch (std::runtime_error const& e) + { + BEAST_EXPECT(std::string(e.what()) == "STData: expected object"); + } + } + + // Unknown type string should throw + { + Json::Value v; + v[jss::type] = "UNKNOWN_TYPE"; + try + { + STDataType dt = dataTypeFromJson(sf, v); + BEAST_EXPECT(false); // Should not reach here + } + catch (std::runtime_error const& e) + { + BEAST_EXPECT( + std::string(e.what()) == "STData: unsupported type string: UNKNOWN_TYPE"); + } + } + + // Empty type string should throw + { + Json::Value v; + v[jss::type] = ""; + try + { + STDataType dt = dataTypeFromJson(sf, v); + BEAST_EXPECT(false); // Should not reach here + } + catch (std::runtime_error const& e) + { + BEAST_EXPECT(std::string(e.what()) == "STData: unsupported type string: "); + } + } + } + + void + testRoundTrip() + { + testcase("round trip serialization"); + + auto const& sf = sfParameterType; + + // Test serialization and deserialization round trip + for (auto typeId : + {STI_UINT8, + STI_UINT16, + STI_UINT32, + STI_UINT64, + STI_UINT128, + STI_UINT160, + STI_UINT192, + STI_UINT256, + STI_VL, + STI_ACCOUNT, + STI_AMOUNT, + STI_ISSUE, + STI_CURRENCY, + STI_NUMBER}) + { + // Create original + STDataType original(sf, typeId); + + // Serialize + Serializer s; + original.add(s); + + // Deserialize + SerialIter sit(s.slice()); + STDataType deserialized(sit, sf); + + // Compare + BEAST_EXPECT(deserialized.getInnerSType() == typeId); + BEAST_EXPECT(original.isEquivalent(deserialized)); + } + } + + void + testJsonRoundTrip() + { + testcase("JSON round trip"); + + auto const& sf = sfParameterType; + + std::vector typeStrings = { + "UINT8", + "UINT16", + "UINT32", + "UINT64", + "UINT128", + "UINT160", + "UINT192", + "UINT256", + "VL", + "ACCOUNT", + "AMOUNT", + "ISSUE", + "CURRENCY", + "NUMBER"}; + + for (auto const& typeStr : typeStrings) + { + // Create from JSON + Json::Value input; + input[jss::type] = typeStr; + STDataType dt = dataTypeFromJson(sf, input); + + // Convert back to JSON + Json::Value output = dt.getJson(JsonOptions::none); + + // Verify + BEAST_EXPECT(output[jss::type].asString() == typeStr); + } + } + + void + testGetInnerTypeString() + { + testcase("getInnerTypeString"); + + auto const& sf = sfParameterType; + + struct TypeStringTest + { + SerializedTypeID type; + std::string expected; + }; + + TypeStringTest tests[] = { + {STI_UINT8, "UINT8"}, + {STI_UINT16, "UINT16"}, + {STI_UINT32, "UINT32"}, + {STI_UINT64, "UINT64"}, + {STI_UINT128, "UINT128"}, + {STI_UINT160, "UINT160"}, + {STI_UINT192, "UINT192"}, + {STI_UINT256, "UINT256"}, + {STI_VL, "VL"}, + {STI_ACCOUNT, "ACCOUNT"}, + {STI_AMOUNT, "AMOUNT"}, + {STI_ISSUE, "ISSUE"}, + {STI_CURRENCY, "CURRENCY"}, + {STI_NUMBER, "NUMBER"}, + {static_cast(999), "999"} // Unknown type + }; + + for (auto const& test : tests) + { + STDataType dt(sf, test.type); + BEAST_EXPECT(dt.getInnerTypeString() == test.expected); + } + } + + void + run() override + { + testConstructors(); + testCopyMove(); + testSerialization(); + testEquivalence(); + testDefault(); + testGetText(); + testGetJson(); + testDataTypeFromJson(); + testRoundTrip(); + testJsonRoundTrip(); + testGetInnerTypeString(); + } +}; + +BEAST_DEFINE_TESTSUITE(STDataType, protocol, xrpl); + +} // namespace xrpl diff --git a/src/test/protocol/STData_test.cpp b/src/test/protocol/STData_test.cpp new file mode 100644 index 0000000000..afc929983d --- /dev/null +++ b/src/test/protocol/STData_test.cpp @@ -0,0 +1,1290 @@ +#include +#include +#include +#include +#include + +namespace xrpl { + +struct STData_test : public beast::unit_test::suite +{ + void + testConstructors() + { + testcase("Constructors"); + + auto const& sf = sfParameterValue; + + // Default constructor + { + STData data(sf); + BEAST_EXPECT(data.getSType() == STI_DATA); + BEAST_EXPECT(data.isDefault()); + } + + // Type-specific constructors + { + // UINT8 + STData data_u8(sf, static_cast(8)); + BEAST_EXPECT(data_u8.getFieldU8() == 8); + BEAST_EXPECT(data_u8.getInnerTypeString() == "UINT8"); + BEAST_EXPECT(data_u8.isDefault()); + + // UINT16 + STData data_u16(sf, static_cast(16)); + BEAST_EXPECT(data_u16.getFieldU16() == 16); + BEAST_EXPECT(data_u16.getInnerTypeString() == "UINT16"); + + // UINT32 + STData data_u32(sf, static_cast(32)); + BEAST_EXPECT(data_u32.getFieldU32() == 32); + BEAST_EXPECT(data_u32.getInnerTypeString() == "UINT32"); + + // UINT64 + STData data_u64(sf, static_cast(64)); + BEAST_EXPECT(data_u64.getFieldU64() == 64); + BEAST_EXPECT(data_u64.getInnerTypeString() == "UINT64"); + + // UINT128 + uint128 val128 = uint128(1); + STData data_u128(sf, val128); + BEAST_EXPECT(data_u128.getFieldH128() == val128); + BEAST_EXPECT(data_u128.getInnerTypeString() == "UINT128"); + + // UINT160 + uint160 val160 = uint160(1); + STData data_u160(sf, val160); + BEAST_EXPECT(data_u160.getFieldH160() == val160); + BEAST_EXPECT(data_u160.getInnerTypeString() == "UINT160"); + + // UINT192 + uint192 val192 = uint192(1); + STData data_u192(sf, val192); + BEAST_EXPECT(data_u192.getFieldH192() == val192); + BEAST_EXPECT(data_u192.getInnerTypeString() == "UINT192"); + + // UINT256 + uint256 val256 = uint256(1); + STData data_u256(sf, val256); + BEAST_EXPECT(data_u256.getFieldH256() == val256); + BEAST_EXPECT(data_u256.getInnerTypeString() == "UINT256"); + + // Blob + Blob blob = strUnHex("DEADBEEFCAFEBABE").value(); + STData data_blob(sf, blob); + BEAST_EXPECT(data_blob.getFieldVL() == blob); + BEAST_EXPECT(data_blob.getInnerTypeString() == "VL"); + + // Slice + std::string test_str = "Hello World"; + Slice slice(test_str.data(), test_str.size()); + STData data_slice(sf, slice); + Blob expected_blob(test_str.begin(), test_str.end()); + BEAST_EXPECT(data_slice.getFieldVL() == expected_blob); + + // AccountID + AccountID account(0x123456789ABCDEF0); + STData data_account(sf, account); + BEAST_EXPECT(data_account.getAccountID() == account); + BEAST_EXPECT(data_account.getInnerTypeString() == "ACCOUNT"); + + // STAmount (Native) + STAmount amount_native(1000); + STData data_amount_native(sf, amount_native); + BEAST_EXPECT(data_amount_native.getFieldAmount() == amount_native); + BEAST_EXPECT(data_amount_native.getInnerTypeString() == "AMOUNT"); + + // STAmount (IOU) + IOUAmount iou_amount(5000); + Issue const usd( + Currency(0x5553440000000000), + parseBase58("rG1QQv2nh2gr7RCZ1P8YYcBUKCCN633jCn").value()); + STAmount amount_iou(iou_amount, usd); + STData data_amount_iou(sf, amount_iou); + BEAST_EXPECT(data_amount_iou.getFieldAmount() == amount_iou); + } + } + + void + testSerializationDeserialization() + { + testcase("Serialization/Deserialization"); + + auto const& sf = sfParameterValue; + + // Test each type's serialization and deserialization round-trip + { + // UINT8 + std::uint8_t original_u8 = 8; + STData data_u8(sf, original_u8); + + Serializer s; + data_u8.add(s); + + SerialIter sit(s.slice()); + STData deserialized_u8(sit, sf); + + BEAST_EXPECT(deserialized_u8.getFieldU8() == original_u8); + BEAST_EXPECT(deserialized_u8.getInnerTypeString() == "UINT8"); + } + + { + // UINT16 + std::uint16_t original_u16 = 16; + STData data_u16(sf, original_u16); + + Serializer s; + data_u16.add(s); + + SerialIter sit(s.slice()); + STData deserialized_u16(sit, sf); + + BEAST_EXPECT(deserialized_u16.getFieldU16() == original_u16); + BEAST_EXPECT(deserialized_u16.getInnerTypeString() == "UINT16"); + } + + { + // UINT32 + std::uint32_t original_u32 = 32; + STData data_u32(sf, original_u32); + + Serializer s; + data_u32.add(s); + + SerialIter sit(s.slice()); + STData deserialized_u32(sit, sf); + + BEAST_EXPECT(deserialized_u32.getFieldU32() == original_u32); + BEAST_EXPECT(deserialized_u32.getInnerTypeString() == "UINT32"); + } + + { + // UINT64 + std::uint64_t original_u64 = 64; + STData data_u64(sf, original_u64); + + Serializer s; + data_u64.add(s); + + SerialIter sit(s.slice()); + STData deserialized_u64(sit, sf); + + BEAST_EXPECT(deserialized_u64.getFieldU64() == original_u64); + BEAST_EXPECT(deserialized_u64.getInnerTypeString() == "UINT64"); + } + + { + // UINT128 + uint128 original_u128 = uint128(1); + STData data_u128(sf, original_u128); + + Serializer s; + data_u128.add(s); + + SerialIter sit(s.slice()); + STData deserialized_u128(sit, sf); + + BEAST_EXPECT(deserialized_u128.getFieldH128() == original_u128); + BEAST_EXPECT(deserialized_u128.getInnerTypeString() == "UINT128"); + } + + { + // UINT160 + uint160 original_u160 = uint160(1); + STData data_u160(sf, original_u160); + + Serializer s; + data_u160.add(s); + + SerialIter sit(s.slice()); + STData deserialized_u160(sit, sf); + + BEAST_EXPECT(deserialized_u160.getFieldH160() == original_u160); + BEAST_EXPECT(deserialized_u160.getInnerTypeString() == "UINT160"); + } + + { + // UINT192 + uint192 original_u192 = uint192(1); + STData data_u192(sf, original_u192); + + Serializer s; + data_u192.add(s); + + SerialIter sit(s.slice()); + STData deserialized_u192(sit, sf); + + BEAST_EXPECT(deserialized_u192.getFieldH192() == original_u192); + BEAST_EXPECT(deserialized_u192.getInnerTypeString() == "UINT192"); + } + + { + // UINT256 + uint256 original_u256 = uint256(1); + STData data_u256(sf, original_u256); + + Serializer s; + data_u256.add(s); + + SerialIter sit(s.slice()); + STData deserialized_u256(sit, sf); + + BEAST_EXPECT(deserialized_u256.getFieldH256() == original_u256); + BEAST_EXPECT(deserialized_u256.getInnerTypeString() == "UINT256"); + } + + { + // VL (Variable Length) + Blob original_blob = strUnHex("DEADBEEFCAFEBABE1234567890ABCDEF").value(); + STData data_vl(sf, original_blob); + + Serializer s; + data_vl.add(s); + + SerialIter sit(s.slice()); + STData deserialized_vl(sit, sf); + + BEAST_EXPECT(deserialized_vl.getFieldVL() == original_blob); + BEAST_EXPECT(deserialized_vl.getInnerTypeString() == "VL"); + } + + { + // ACCOUNT + AccountID original_account(0xFEDCBA9876543210); + STData data_account(sf, original_account); + + Serializer s; + data_account.add(s); + + SerialIter sit(s.slice()); + STData deserialized_account(sit, sf); + + BEAST_EXPECT(deserialized_account.getAccountID() == original_account); + BEAST_EXPECT(deserialized_account.getInnerTypeString() == "ACCOUNT"); + } + + { + // AMOUNT (Native) + STAmount original_amount(99999); + STData data_amount(sf, original_amount); + + Serializer s; + data_amount.add(s); + + SerialIter sit(s.slice()); + STData deserialized_amount(sit, sf); + + BEAST_EXPECT(deserialized_amount.getFieldAmount() == original_amount); + BEAST_EXPECT(deserialized_amount.getInnerTypeString() == "AMOUNT"); + } + + { + // CURRENCY + } + + { + // ISSUE + } + + { + // NUMBER + } + } + + void + testSettersAndGetters() + { + testcase("Setters and Getters"); + + auto const& sf = sfParameterValue; + STData data(sf); + + // Test all setter/getter combinations + { + // UINT8 + unsigned char val_u8 = 8; + data.setFieldU8(val_u8); + BEAST_EXPECT(data.getFieldU8() == val_u8); + BEAST_EXPECT(data.getInnerTypeString() == "UINT8"); + } + + { + // UINT16 + std::uint16_t val_u16 = 16; + data.setFieldU16(val_u16); + BEAST_EXPECT(data.getFieldU16() == val_u16); + BEAST_EXPECT(data.getInnerTypeString() == "UINT16"); + } + + { + // UINT32 + std::uint32_t val_u32 = 32; + data.setFieldU32(val_u32); + BEAST_EXPECT(data.getFieldU32() == val_u32); + BEAST_EXPECT(data.getInnerTypeString() == "UINT32"); + } + + { + // UINT64 + std::uint64_t val_u64 = 64; + data.setFieldU64(val_u64); + BEAST_EXPECT(data.getFieldU64() == val_u64); + BEAST_EXPECT(data.getInnerTypeString() == "UINT64"); + } + + { + // UINT128 + uint128 val_u128 = uint128(1); + data.setFieldH128(val_u128); + BEAST_EXPECT(data.getFieldH128() == val_u128); + BEAST_EXPECT(data.getInnerTypeString() == "UINT128"); + } + + { + // UINT160 + uint160 val_u160 = uint160(1); + data.setFieldH160(val_u160); + BEAST_EXPECT(data.getFieldH160() == val_u160); + BEAST_EXPECT(data.getInnerTypeString() == "UINT160"); + } + + { + // UINT192 + uint192 val_u192 = uint192(1); + data.setFieldH192(val_u192); + BEAST_EXPECT(data.getFieldH192() == val_u192); + BEAST_EXPECT(data.getInnerTypeString() == "UINT192"); + } + + { + // UINT256 + uint256 val_u256 = uint256(1); + data.setFieldH256(val_u256); + BEAST_EXPECT(data.getFieldH256() == val_u256); + BEAST_EXPECT(data.getInnerTypeString() == "UINT256"); + } + + { + // VL (Variable Length) - Blob + Blob val_blob = strUnHex("0102030405060708090A0B0C0D0E0F10").value(); + data.setFieldVL(val_blob); + BEAST_EXPECT(data.getFieldVL() == val_blob); + BEAST_EXPECT(data.getInnerTypeString() == "VL"); + } + + { + // VL (Variable Length) - Slice + std::string test_str = "Test String for Slice"; + Slice val_slice(test_str.data(), test_str.size()); + data.setFieldVL(val_slice); + Blob expected_blob(test_str.begin(), test_str.end()); + BEAST_EXPECT(data.getFieldVL() == expected_blob); + BEAST_EXPECT(data.getInnerTypeString() == "VL"); + } + + { + // ACCOUNT + AccountID val_account(0x123456789ABCDEF0); + data.setAccountID(val_account); + BEAST_EXPECT(data.getAccountID() == val_account); + BEAST_EXPECT(data.getInnerTypeString() == "ACCOUNT"); + } + + { + // AMOUNT + STAmount val_amount(777777); + data.setFieldAmount(val_amount); + BEAST_EXPECT(data.getFieldAmount() == val_amount); + BEAST_EXPECT(data.getInnerTypeString() == "AMOUNT"); + } + + { + // CURRENCY + } + + { + // ISSUE + } + + { + // NUMBER + } + } + + void + testJsonConversion() + { + testcase("JSON Conversion"); + + auto const& sf = sfParameterValue; + + // Test JSON serialization for each type + { + // UINT8 + STData data_u8(sf, static_cast(8)); + Json::Value json_u8 = data_u8.getJson(JsonOptions::none); + BEAST_EXPECT(json_u8[jss::type].asString() == "UINT8"); + BEAST_EXPECT(json_u8[jss::value].asUInt() == 8); + } + + { + // UINT16 + STData data_u16(sf, static_cast(16)); + Json::Value json_u16 = data_u16.getJson(JsonOptions::none); + BEAST_EXPECT(json_u16[jss::type].asString() == "UINT16"); + BEAST_EXPECT(json_u16[jss::value].asUInt() == 16); + } + + { + // UINT32 + STData data_u32(sf, static_cast(32)); + Json::Value json_u32 = data_u32.getJson(JsonOptions::none); + BEAST_EXPECT(json_u32[jss::type].asString() == "UINT32"); + BEAST_EXPECT(json_u32[jss::value].asUInt() == 32); + } + + { + // UINT64 + STData data_u64(sf, static_cast(64)); + Json::Value json_u64 = data_u64.getJson(JsonOptions::none); + BEAST_EXPECT(json_u64[jss::type].asString() == "UINT64"); + BEAST_EXPECT(json_u64[jss::value].asString() == "40"); + } + + { + // UINT128 + uint128 val_u128 = uint128(1); + STData data_u128(sf, val_u128); + Json::Value json_u128 = data_u128.getJson(JsonOptions::none); + BEAST_EXPECT(json_u128[jss::type].asString() == "UINT128"); + BEAST_EXPECT(json_u128[jss::value].asString() == "00000000000000000000000000000001"); + } + + { + // UINT160 + uint160 val_u160 = uint160(1); + STData data_u160(sf, val_u160); + Json::Value json_u160 = data_u160.getJson(JsonOptions::none); + BEAST_EXPECT(json_u160[jss::type].asString() == "UINT160"); + BEAST_EXPECT( + json_u160[jss::value].asString() == "0000000000000000000000000000000000000001"); + } + + { + // UINT192 + uint192 val_u192 = uint192(1); + STData data_u192(sf, val_u192); + Json::Value json_u192 = data_u192.getJson(JsonOptions::none); + BEAST_EXPECT(json_u192[jss::type].asString() == "UINT192"); + BEAST_EXPECT( + json_u192[jss::value].asString() == + "000000000000000000000000000000000000000000000001"); + } + + { + // UINT256 + uint256 val_u256 = uint256(1); + STData data_u256(sf, val_u256); + Json::Value json_u256 = data_u256.getJson(JsonOptions::none); + BEAST_EXPECT(json_u256[jss::type].asString() == "UINT256"); + BEAST_EXPECT( + json_u256[jss::value].asString() == + "00000000000000000000000000000000000000000000000000000000000000" + "01"); + } + + { + // VL + Blob blob = strUnHex("DEADBEEF").value(); + STData data_vl(sf, blob); + Json::Value json_vl = data_vl.getJson(JsonOptions::none); + BEAST_EXPECT(json_vl[jss::type].asString() == "VL"); + BEAST_EXPECT(json_vl[jss::value].asString() == "DEADBEEF"); + } + + { + // ACCOUNT + AccountID account(0x123456789ABCDEF0); + STData data_account(sf, account); + Json::Value json_account = data_account.getJson(JsonOptions::none); + BEAST_EXPECT(json_account[jss::type].asString() == "ACCOUNT"); + BEAST_EXPECT(json_account[jss::value].asString() == "rrrrrrrrrrrrrLveWzSkxhcH3hGw6"); + } + + { + // AMOUNT + STAmount amount(1000); + STData data_amount(sf, amount); + Json::Value json_amount = data_amount.getJson(JsonOptions::none); + BEAST_EXPECT(json_amount[jss::type].asString() == "AMOUNT"); + BEAST_EXPECT(json_amount[jss::value].asString() == "1000"); + } + + { + // CURRENCY + } + + { + // ISSUE + } + + { + // NUMBER + } + } + + void + testDataFromJson() + { + testcase("Data From JSON"); + + auto const& sf = sfParameterValue; + + // Test JSON deserialization for each type + { + // UINT8 + Json::Value json_u8(Json::objectValue); + json_u8[jss::type] = "UINT8"; + json_u8[jss::value] = 8; + + STData data_u8 = dataFromJson(sf, json_u8); + BEAST_EXPECT(data_u8.getFieldU8() == 8); + BEAST_EXPECT(data_u8.getInnerTypeString() == "UINT8"); + } + + { + // UINT16 + Json::Value json_u16(Json::objectValue); + json_u16[jss::type] = "UINT16"; + json_u16[jss::value] = 16; + + STData data_u16 = dataFromJson(sf, json_u16); + BEAST_EXPECT(data_u16.getFieldU16() == 16); + BEAST_EXPECT(data_u16.getInnerTypeString() == "UINT16"); + } + + { + // UINT32 + Json::Value json_u32(Json::objectValue); + json_u32[jss::type] = "UINT32"; + json_u32[jss::value] = 32; + + STData data_u32 = dataFromJson(sf, json_u32); + BEAST_EXPECT(data_u32.getFieldU32() == 32); + BEAST_EXPECT(data_u32.getInnerTypeString() == "UINT32"); + } + + { + // UINT64 + Json::Value json_u64(Json::objectValue); + json_u64[jss::type] = "UINT64"; + json_u64[jss::value] = 64; + STData data_u64 = dataFromJson(sf, json_u64); + BEAST_EXPECT(data_u64.getFieldU64() == 64); + BEAST_EXPECT(data_u64.getInnerTypeString() == "UINT64"); + } + + { + // UINT128 + Json::Value json_u128(Json::objectValue); + json_u128[jss::type] = "UINT128"; + json_u128[jss::value] = "00000000000000000000000000000001"; + STData data_u128 = dataFromJson(sf, json_u128); + uint128 expected; + bool ok = expected.parseHex("00000000000000000000000000000001"); + BEAST_EXPECT(ok); + BEAST_EXPECT(data_u128.getFieldH128() == expected); + BEAST_EXPECT(data_u128.getInnerTypeString() == "UINT128"); + } + + { + // UINT160 + Json::Value json_u160(Json::objectValue); + json_u160[jss::type] = "UINT160"; + json_u160[jss::value] = "0000000000000000000000000000000000000001"; + STData data_u160 = dataFromJson(sf, json_u160); + uint160 expected; + bool ok = expected.parseHex("0000000000000000000000000000000000000001"); + BEAST_EXPECT(ok); + BEAST_EXPECT(data_u160.getFieldH160() == expected); + BEAST_EXPECT(data_u160.getInnerTypeString() == "UINT160"); + } + + { + // UINT192 + Json::Value json_u192(Json::objectValue); + json_u192[jss::type] = "UINT192"; + json_u192[jss::value] = "000000000000000000000000000000000000000000000001"; + STData data_u192 = dataFromJson(sf, json_u192); + uint192 expected; + bool ok = expected.parseHex("000000000000000000000000000000000000000000000001"); + BEAST_EXPECT(ok); + BEAST_EXPECT(data_u192.getFieldH192() == expected); + BEAST_EXPECT(data_u192.getInnerTypeString() == "UINT192"); + } + + { + // UINT256 + Json::Value json_u256(Json::objectValue); + json_u256[jss::type] = "UINT256"; + json_u256[jss::value] = + "00000000000000000000000000000000000000000000000000000000000000" + "01"; + STData data_u256 = dataFromJson(sf, json_u256); + uint256 expected; + bool ok = expected.parseHex( + "00000000000000000000000000000000000000000000000000000000000000" + "01"); + BEAST_EXPECT(ok); + BEAST_EXPECT(data_u256.getFieldH256() == expected); + BEAST_EXPECT(data_u256.getInnerTypeString() == "UINT256"); + } + + { + // VL + Json::Value json_vl(Json::objectValue); + json_vl[jss::type] = "VL"; + json_vl[jss::value] = "DEADBEEFCAFEBABE"; + + STData data_vl = dataFromJson(sf, json_vl); + Blob expected_blob = strUnHex("DEADBEEFCAFEBABE").value(); + BEAST_EXPECT(data_vl.getFieldVL() == expected_blob); + BEAST_EXPECT(data_vl.getInnerTypeString() == "VL"); + } + + { + // ACCOUNT + Json::Value json_account(Json::objectValue); + json_account[jss::type] = "ACCOUNT"; + json_account[jss::value] = "rG1QQv2nh2gr7RCZ1P8YYcBUKCCN633jCn"; + + STData data_account = dataFromJson(sf, json_account); + AccountID expected_account = + parseBase58("rG1QQv2nh2gr7RCZ1P8YYcBUKCCN633jCn").value(); + BEAST_EXPECT(data_account.getAccountID() == expected_account); + BEAST_EXPECT(data_account.getInnerTypeString() == "ACCOUNT"); + } + + { + // AMOUNT + Json::Value json_amount(Json::objectValue); + json_amount[jss::type] = "AMOUNT"; + json_amount[jss::value] = "1000"; + + STData data_amount = dataFromJson(sf, json_amount); + STAmount expected_amount(1000); + BEAST_EXPECT(data_amount.getFieldAmount() == expected_amount); + BEAST_EXPECT(data_amount.getInnerTypeString() == "AMOUNT"); + } + + { + // CURRENCY + } + + { + // ISSUE + } + + { + // NUMBER + } + } + + // void + // testErrorCases() + // { + // testcase("Error Cases"); + + // auto const& sf = sfParameterValue; + + // // Test JSON parsing errors + // { + // // Missing type + // Json::Value json_no_type(Json::objectValue); + // json_no_type[jss::value] = 123; + + // try { + // STData data = dataFromJson(sf, json_no_type); + // fail("Expected exception for missing type"); + // } catch (std::runtime_error const& e) { + // pass(); + // } + // } + + // { + // // Missing value + // Json::Value json_no_value(Json::objectValue); + // json_no_value[jss::type] = "UINT8"; + + // try { + // STData data = dataFromJson(sf, json_no_value); + // fail("Expected exception for missing value"); + // } catch (std::runtime_error const& e) { + // pass(); + // } + // } + + // { + // // Invalid type string + // Json::Value json_invalid_type(Json::objectValue); + // json_invalid_type[jss::type] = "INVALID_TYPE"; + // json_invalid_type[jss::value] = 123; + + // try { + // STData data = dataFromJson(sf, json_invalid_type); + // fail("Expected exception for invalid type"); + // } catch (std::runtime_error const& e) { + // pass(); + // } + // } + + // { + // // Invalid UINT256 hex + // Json::Value json_invalid_hex(Json::objectValue); + // json_invalid_hex[jss::type] = "UINT256"; + // json_invalid_hex[jss::value] = "INVALID_HEX_STRING"; + + // try { + // STData data = dataFromJson(sf, json_invalid_hex); + // fail("Expected exception for invalid hex"); + // } catch (std::runtime_error const& e) { + // pass(); + // } + // } + + // { + // // Invalid VL hex + // Json::Value json_invalid_vl(Json::objectValue); + // json_invalid_vl[jss::type] = "VL"; + // json_invalid_vl[jss::value] = "INVALID_HEX"; + + // try { + // STData data = dataFromJson(sf, json_invalid_vl); + // fail("Expected exception for invalid VL data"); + // } catch (std::invalid_argument const& e) { + // pass(); + // } + // } + + // { + // // Invalid account + // Json::Value json_invalid_account(Json::objectValue); + // json_invalid_account[jss::type] = "ACCOUNT"; + // json_invalid_account[jss::value] = "INVALID_ACCOUNT_STRING"; + + // try { + // STData data = dataFromJson(sf, json_invalid_account); + // fail("Expected exception for invalid account"); + // } catch (std::runtime_error const& e) { + // pass(); + // } + // } + + // { + // // Non-object JSON + // Json::Value json_not_object = "not an object"; + + // try { + // STData data = dataFromJson(sf, json_not_object); + // fail("Expected exception for non-object JSON"); + // } catch (std::runtime_error const& e) { + // pass(); + // } + // } + // } + + // void + // testEquivalence() + // { + // testcase("Equivalence"); + + // auto const& sf = sfParameterValue; + + // // Test equivalence for same types and values + // { + // STData data1(sf, static_cast(42)); + // STData data2(sf, static_cast(42)); + // BEAST_EXPECT(data1.isEquivalent(data2)); + // } + + // // Test non-equivalence for different values + // { + // STData data1(sf, static_cast(42)); + // STData data2(sf, static_cast(43)); + // BEAST_EXPECT(!data1.isEquivalent(data2)); + // } + + // // Test non-equivalence for different types + // { + // STData data1(sf, static_cast(42)); + // STData data2(sf, static_cast(42)); + // BEAST_EXPECT(!data1.isEquivalent(data2)); + // } + + // // Test equivalence with complex types + // { + // uint256 val; + // val.parseHex("123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0"); + // STData data1(sf, val); + // STData data2(sf, val); + // BEAST_EXPECT(data1.isEquivalent(data2)); + // } + // } + + // void + // testSize() + // { + // testcase("Size Calculation"); + + // auto const& sf = sfParameterValue; + + // // Test size calculation for each type + // { + // STData data_u8(sf, static_cast(42)); + // BEAST_EXPECT(data_u8.size() == sizeof(uint8_t)); + // } + + // { + // STData data_u16(sf, static_cast(1234)); + // BEAST_EXPECT(data_u16.size() == sizeof(uint16_t)); + // } + + // { + // STData data_u32(sf, static_cast(123456)); + // BEAST_EXPECT(data_u32.size() == sizeof(uint32_t)); + // } + + // { + // STData data_u64(sf, static_cast(123456789)); + // BEAST_EXPECT(data_u64.size() == sizeof(uint64_t)); + // } + + // { + // uint128 val_u128(0x12345678, 0x9ABCDEF0); + // STData data_u128(sf, val_u128); + // BEAST_EXPECT(data_u128.size() == uint128::size()); + // } + + // { + // uint256 val_u256; + // val_u256.parseHex("123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0"); + // STData data_u256(sf, val_u256); + // BEAST_EXPECT(data_u256.size() == uint256::size()); + // } + + // { + // Blob blob = strUnHex("DEADBEEFCAFEBABE").value(); + // STData data_vl(sf, blob); + // BEAST_EXPECT(data_vl.size() == blob.size()); + // } + + // { + // AccountID account(0x123456789ABCDEF0); + // STData data_account(sf, account); + // BEAST_EXPECT(data_account.size() == uint160::size()); + // } + + // { + // // Native amount + // STAmount amount_native(1000); + // STData data_amount_native(sf, amount_native); + // BEAST_EXPECT(data_amount_native.size() == 8); // Native amounts + // are 8 bytes + // } + + // { + // // IOU amount + // IOUAmount iou_amount(5000); + // Issue const usd( + // Currency(0x5553440000000000), + // parseBase58("rG1QQv2nh2gr7RCZ1P8YYcBUKCCN633jCn").value()); + // STAmount amount_iou(iou_amount, usd); + // STData data_amount_iou(sf, amount_iou); + // BEAST_EXPECT(data_amount_iou.size() == 48); // IOU amounts are 48 + // bytes + // } + // } + + // void + // testTextRepresentation() + // { + // testcase("Text Representation"); + + // auto const& sf = sfParameterValue; + + // // Test getText() for various types + // { + // STData data_u8(sf, static_cast(42)); + // std::string text = data_u8.getText(); + // BEAST_EXPECT(text.find("STData") != std::string::npos); + // BEAST_EXPECT(text.find("UINT8") != std::string::npos); + // } + + // { + // uint256 val; + // val.parseHex("123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0"); + // STData data_u256(sf, val); + // std::string text = data_u256.getText(); + // BEAST_EXPECT(text.find("STData") != std::string::npos); + // BEAST_EXPECT(text.find("UINT256") != std::string::npos); + // } + + // { + // Blob blob = strUnHex("DEADBEEF").value(); + // STData data_vl(sf, blob); + // std::string text = data_vl.getText(); + // BEAST_EXPECT(text.find("STData") != std::string::npos); + // BEAST_EXPECT(text.find("VL") != std::string::npos); + // } + // } + + // void + // testCopyAndMove() + // { + // testcase("Copy and Move Operations"); + + // auto const& sf = sfParameterValue; + + // // Test copy functionality + // { + // uint256 val; + // val.parseHex("123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0"); + // STData original(sf, val); + + // // Test copy + // char buffer[1024]; + // STBase* copied = original.copy(sizeof(buffer), buffer); + // BEAST_EXPECT(copied != nullptr); + + // STData* copied_data = dynamic_cast(copied); + // BEAST_EXPECT(copied_data != nullptr); + // BEAST_EXPECT(copied_data->getFieldH256() == val); + // BEAST_EXPECT(copied_data->getInnerTypeString() == "UINT256"); + // } + + // // Test move functionality + // { + // Blob blob = strUnHex("DEADBEEFCAFEBABE").value(); + // STData original(sf, blob); + + // char buffer[1024]; + // STBase* moved = original.move(sizeof(buffer), buffer); + // BEAST_EXPECT(moved != nullptr); + + // STData* moved_data = dynamic_cast(moved); + // BEAST_EXPECT(moved_data != nullptr); + // BEAST_EXPECT(moved_data->getFieldVL() == blob); + // BEAST_EXPECT(moved_data->getInnerTypeString() == "VL"); + // } + // } + + // void + // testBoundaryValues() + // { + // testcase("Boundary Values"); + + // auto const& sf = sfParameterValue; + + // // Test minimum and maximum values for each numeric type + // { + // // UINT8 boundaries + // STData data_u8_min(sf, static_cast(0)); + // BEAST_EXPECT(data_u8_min.getFieldU8() == 0); + + // STData data_u8_max(sf, static_cast(255)); + // BEAST_EXPECT(data_u8_max.getFieldU8() == 255); + // } + + // { + // // UINT16 boundaries + // STData data_u16_min(sf, static_cast(0)); + // BEAST_EXPECT(data_u16_min.getFieldU16() == 0); + + // STData data_u16_max(sf, static_cast(65535)); + // BEAST_EXPECT(data_u16_max.getFieldU16() == 65535); + // } + + // { + // // UINT32 boundaries + // STData data_u32_min(sf, static_cast(0)); + // BEAST_EXPECT(data_u32_min.getFieldU32() == 0); + + // STData data_u32_max(sf, static_cast(0xFFFFFFFF)); + // BEAST_EXPECT(data_u32_max.getFieldU32() == 0xFFFFFFFF); + // } + + // { + // // UINT64 boundaries + // STData data_u64_min(sf, static_cast(0)); + // BEAST_EXPECT(data_u64_min.getFieldU64() == 0); + + // STData data_u64_max(sf, + // static_cast(0xFFFFFFFFFFFFFFFF)); + // BEAST_EXPECT(data_u64_max.getFieldU64() == 0xFFFFFFFFFFFFFFFF); + // } + + // { + // // Empty blob + // Blob empty_blob; + // STData data_empty_vl(sf, empty_blob); + // BEAST_EXPECT(data_empty_vl.getFieldVL() == empty_blob); + // BEAST_EXPECT(data_empty_vl.size() == 0); + // } + + // { + // // Large blob (test with reasonably sized data) + // Blob large_blob(1000, 0xAB); // 1000 bytes of 0xAB + // STData data_large_vl(sf, large_blob); + // BEAST_EXPECT(data_large_vl.getFieldVL() == large_blob); + // BEAST_EXPECT(data_large_vl.size() == 1000); + // } + // } + + // void + // testSpecialSerializationCases() + // { + // testcase("Special Serialization Cases"); + + // auto const& sf = sfParameterValue; + + // // Test serialization format compliance + // { + // // Verify type prefix is included in serialization + // STData data_u8(sf, static_cast(0x42)); + // Serializer s; + // data_u8.add(s); + + // // Should start with type identifier (STI_UINT8 = 0x0010) + // auto slice = s.slice(); + // BEAST_EXPECT(slice.size() >= 2); + // std::uint16_t type_id = (static_cast(slice[0]) << + // 8) | slice[1]; BEAST_EXPECT(type_id == STI_UINT8); + // } + + // { + // // Test VL serialization includes length + // Blob test_blob = strUnHex("DEADBEEF").value(); + // STData data_vl(sf, test_blob); + // Serializer s; + // data_vl.add(s); + + // auto slice = s.slice(); + // BEAST_EXPECT(slice.size() >= 2); + // std::uint16_t type_id = (static_cast(slice[0]) << + // 8) | slice[1]; BEAST_EXPECT(type_id == STI_VL); + // } + // } + + // void + // testTypeConsistency() + // { + // testcase("Type Consistency"); + + // auto const& sf = sfParameterValue; + + // // Verify that changing types properly updates internal state + // { + // STData data(sf); + + // // Start with UINT8 + // data.setFieldU8(42); + // BEAST_EXPECT(data.getInnerTypeString() == "UINT8"); + // BEAST_EXPECT(data.getFieldU8() == 42); + + // // Change to UINT16 + // data.setFieldU16(1234); + // BEAST_EXPECT(data.getInnerTypeString() == "UINT16"); + // BEAST_EXPECT(data.getFieldU16() == 1234); + + // // Change to VL + // Blob blob = strUnHex("DEADBEEF").value(); + // data.setFieldVL(blob); + // BEAST_EXPECT(data.getInnerTypeString() == "VL"); + // BEAST_EXPECT(data.getFieldVL() == blob); + + // // Change to UINT256 + // uint256 val; + // val.parseHex("123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0"); + // data.setFieldH256(val); + // BEAST_EXPECT(data.getInnerTypeString() == "UINT256"); + // BEAST_EXPECT(data.getFieldH256() == val); + // } + // } + + // void + // testComplexAmountTypes() + // { + // testcase("Complex Amount Types"); + + // auto const& sf = sfParameterValue; + + // // Test various STAmount configurations + // { + // // Zero native amount + // STAmount zero_native(0); + // STData data_zero(sf, zero_native); + // BEAST_EXPECT(data_zero.getFieldAmount() == zero_native); + // BEAST_EXPECT(data_zero.size() == 8); // Native amounts are 8 + // bytes + // } + + // { + // // Maximum native amount + // STAmount max_native(100000000000000000ULL); // Max XRP in drops + // STData data_max(sf, max_native); + // BEAST_EXPECT(data_max.getFieldAmount() == max_native); + // } + + // { + // // IOU with zero value + // IOUAmount zero_iou(0); + // Issue const eur( + // Currency(0x4555520000000000), + // parseBase58("rG1QQv2nh2gr7RCZ1P8YYcBUKCCN633jCn").value()); + // STAmount zero_iou_amount(zero_iou, eur); + // STData data_zero_iou(sf, zero_iou_amount); + // BEAST_EXPECT(data_zero_iou.getFieldAmount() == zero_iou_amount); + // BEAST_EXPECT(data_zero_iou.size() == 48); // IOU amounts are 48 + // bytes + // } + // } + + // void + // testJsonRoundTrip() + // { + // testcase("JSON Round Trip"); + + // auto const& sf = sfParameterValue; + + // // Test complete round trip: STData -> JSON -> STData + // { + // // UINT8 + // STData original_u8(sf, static_cast(123)); + // Json::Value json_u8 = original_u8.getJson(JsonOptions::none); + // STData restored_u8 = dataFromJson(sf, json_u8); + // BEAST_EXPECT(original_u8.isEquivalent(restored_u8)); + // } + + // { + // // UINT256 + // uint256 val; + // val.parseHex("FEDCBA9876543210FEDCBA9876543210FEDCBA9876543210FEDCBA9876543210"); + // STData original_u256(sf, val); + // Json::Value json_u256 = original_u256.getJson(JsonOptions::none); + // STData restored_u256 = dataFromJson(sf, json_u256); + // BEAST_EXPECT(original_u256.isEquivalent(restored_u256)); + // } + + // { + // // VL + // Blob blob = strUnHex("0123456789ABCDEF").value(); + // STData original_vl(sf, blob); + // Json::Value json_vl = original_vl.getJson(JsonOptions::none); + // STData restored_vl = dataFromJson(sf, json_vl); + // BEAST_EXPECT(original_vl.isEquivalent(restored_vl)); + // } + + // { + // // ACCOUNT + // AccountID account_id = + // parseBase58("rG1QQv2nh2gr7RCZ1P8YYcBUKCCN633jCn").value(); + // STData original_account(sf, account_id); + // Json::Value json_account = + // original_account.getJson(JsonOptions::none); STData + // restored_account = dataFromJson(sf, json_account); + // BEAST_EXPECT(original_account.isEquivalent(restored_account)); + // } + // } + + // void + // testSerializationRoundTrip() + // { + // testcase("Serialization Round Trip"); + + // auto const& sf = sfParameterValue; + + // // Test complete serialization round trip for all types + // std::vector test_data; + + // // Populate test data with various types + // test_data.emplace_back(sf, static_cast(0xFF)); + // test_data.emplace_back(sf, static_cast(0xFFFF)); + // test_data.emplace_back(sf, static_cast(0xFFFFFFFF)); + // test_data.emplace_back(sf, + // static_cast(0xFFFFFFFFFFFFFFFF)); + + // uint128 val128(0xFFFFFFFF, 0xFFFFFFFF); + // test_data.emplace_back(sf, val128); + + // uint256 val256; + // val256.parseHex("FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF"); + // test_data.emplace_back(sf, val256); + + // Blob blob = strUnHex("DEADBEEFCAFEBABE1234567890ABCDEF").value(); + // test_data.emplace_back(sf, blob); + + // AccountID account(0xFFFFFFFFFFFFFFFF); + // test_data.emplace_back(sf, account); + + // STAmount amount(999999); + // test_data.emplace_back(sf, amount); + + // // Test round trip for each + // for (auto const& original : test_data) + // { + // Serializer s; + // original.add(s); + + // SerialIter sit(s.slice()); + // STData deserialized(sit, sf); + + // BEAST_EXPECT(original.isEquivalent(deserialized)); + // BEAST_EXPECT(original.getInnerTypeString() == + // deserialized.getInnerTypeString()); + // } + // } + + // void + // testMakeFieldPresent() + // { + // testcase("Make Field Present"); + + // auto const& sf = sfParameterValue; + + // // Test makeFieldPresent functionality + // { + // STData data(sf); + // STBase* field = data.makeFieldPresent(); + // BEAST_EXPECT(field != nullptr); + + // // Field should now be present (not STI_NOTPRESENT) + // BEAST_EXPECT(field->getSType() != STI_NOTPRESENT); + // } + // } + + void + run() override + { + testConstructors(); + testSerializationDeserialization(); + testSettersAndGetters(); + testJsonConversion(); + testDataFromJson(); + // testErrorCases(); + // testEquivalence(); + // testSize(); + // testTextRepresentation(); + // testCopyAndMove(); + // testBoundaryValues(); + // testSpecialSerializationCases(); + // testTypeConsistency(); + // testComplexAmountTypes(); + // testJsonRoundTrip(); + // testSerializationRoundTrip(); + // testMakeFieldPresent(); + } +}; + +BEAST_DEFINE_TESTSUITE(STData, protocol, xrpl); + +} // namespace xrpl diff --git a/src/test/protocol/STJson_test.cpp b/src/test/protocol/STJson_test.cpp new file mode 100644 index 0000000000..8fd19feb8b --- /dev/null +++ b/src/test/protocol/STJson_test.cpp @@ -0,0 +1,793 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +namespace xrpl { + +struct STJson_test : public beast::unit_test::suite +{ + void + testDefaultConstructor() + { + testcase("Default constructor"); + STJson json; + BEAST_EXPECT(json.isObject()); + BEAST_EXPECT(!json.isArray()); + BEAST_EXPECT(json.getMap().empty()); + } + + void + testSetAndGet() + { + testcase("setObjectField() and getObjectField()"); + STJson json; + auto value = std::make_shared(sfLedgerIndex, 12345); + json.setObjectField("foo", value); + + auto retrieved = json.getObjectField("foo"); + BEAST_EXPECT(retrieved.has_value()); + BEAST_EXPECT((*retrieved)->getSType() == STI_UINT32); + BEAST_EXPECT(std::dynamic_pointer_cast(*retrieved)->value() == 12345); + + // Test non-existent key + auto missing = json.getObjectField("bar"); + BEAST_EXPECT(!missing.has_value()); + } + + void + testMoveConstructor() + { + testcase("Move constructor (Object)"); + STJson::Map map; + map["bar"] = std::make_shared(sfTransactionType, 42); + STJson json(std::move(map)); + BEAST_EXPECT(json.isObject()); + BEAST_EXPECT(json.getMap().size() == 1); + BEAST_EXPECT(std::dynamic_pointer_cast(json.getMap().at("bar"))->value() == 42); + } + + void + testArrayConstruction() + { + testcase("Array constructor"); + STJson::Array arr; + arr.push_back(std::make_shared(sfNetworkID, 100)); + arr.push_back(std::make_shared(sfNetworkID, 200)); + + STJson json(std::move(arr)); + BEAST_EXPECT(json.isArray()); + BEAST_EXPECT(!json.isObject()); + BEAST_EXPECT(json.arraySize() == 2); + + auto elem0 = json.getArrayElement(0); + BEAST_EXPECT(elem0.has_value()); + BEAST_EXPECT(std::dynamic_pointer_cast(*elem0)->value() == 100); + } + + void + testTypeChecking() + { + testcase("Type checking methods"); + STJson objJson; + BEAST_EXPECT(objJson.isObject()); + BEAST_EXPECT(!objJson.isArray()); + BEAST_EXPECT(objJson.getType() == STJson::JsonType::Object); + + STJson arrJson(STJson::Array{}); + BEAST_EXPECT(arrJson.isArray()); + BEAST_EXPECT(!arrJson.isObject()); + BEAST_EXPECT(arrJson.getType() == STJson::JsonType::Array); + } + + void + testArrayOperations() + { + testcase("Array operations"); + STJson json(STJson::Array{}); + + // Test push + json.pushArrayElement(std::make_shared(sfCloseResolution, 10)); + json.pushArrayElement(std::make_shared(sfCloseResolution, 20)); + json.pushArrayElement(std::make_shared(sfCloseResolution, 30)); + + BEAST_EXPECT(json.arraySize() == 3); + + // Test get + auto elem1 = json.getArrayElement(1); + BEAST_EXPECT(elem1.has_value()); + BEAST_EXPECT(std::dynamic_pointer_cast(*elem1)->value() == 20); + + // Test set (replace) + json.setArrayElement(1, std::make_shared(sfCloseResolution, 25)); + auto elem1Updated = json.getArrayElement(1); + BEAST_EXPECT(std::dynamic_pointer_cast(*elem1Updated)->value() == 25); + + // Test out of bounds + auto missing = json.getArrayElement(10); + BEAST_EXPECT(!missing.has_value()); + } + + void + testArrayAutoResize() + { + testcase("Array auto-resize"); + STJson json(STJson::Array{}); + + // Set element at index 5 (should auto-resize with nulls) + json.setArrayElement(5, std::make_shared(sfNetworkID, 999)); + + BEAST_EXPECT(json.arraySize() == 6); + + // Check nulls were added + for (size_t i = 0; i < 5; ++i) + { + auto elem = json.getArrayElement(i); + BEAST_EXPECT(elem.has_value()); + BEAST_EXPECT(*elem == nullptr); + } + + // Check value at index 5 + auto elem5 = json.getArrayElement(5); + BEAST_EXPECT(elem5.has_value()); + BEAST_EXPECT(std::dynamic_pointer_cast(*elem5)->value() == 999); + } + + void + testArrayElementFields() + { + testcase("Array element field operations"); + STJson json(STJson::Array{}); + + // Set field in array element (auto-creates object) + json.setArrayElementField(0, "name", std::make_shared(sfNetworkID, 42)); + json.setArrayElementField(0, "value", std::make_shared(sfNetworkID, 100)); + + // Get fields + auto name = json.getArrayElementField(0, "name"); + BEAST_EXPECT(name.has_value()); + BEAST_EXPECT(std::dynamic_pointer_cast(*name)->value() == 42); + + auto value = json.getArrayElementField(0, "value"); + BEAST_EXPECT(value.has_value()); + BEAST_EXPECT(std::dynamic_pointer_cast(*value)->value() == 100); + + // Set field at higher index (auto-resize) + json.setArrayElementField(3, "test", std::make_shared(sfCloseResolution, 99)); + BEAST_EXPECT(json.arraySize() == 4); + + auto test = json.getArrayElementField(3, "test"); + BEAST_EXPECT(test.has_value()); + BEAST_EXPECT(std::dynamic_pointer_cast(*test)->value() == 99); + } + + void + testNestedObjectField() + { + testcase("Nested object field operations"); + STJson json; + + json.setNestedObjectField("user", "id", std::make_shared(sfNetworkID, 123)); + json.setNestedObjectField("user", "name", std::make_shared(sfNetworkID, 456)); + + auto id = json.getNestedObjectField("user", "id"); + BEAST_EXPECT(id.has_value()); + BEAST_EXPECT(std::dynamic_pointer_cast(*id)->value() == 123); + + auto name = json.getNestedObjectField("user", "name"); + BEAST_EXPECT(name.has_value()); + BEAST_EXPECT(std::dynamic_pointer_cast(*name)->value() == 456); + + // Test non-existent nested key + auto missing = json.getNestedObjectField("user", "age"); + BEAST_EXPECT(!missing.has_value()); + } + + void + testNestedArrayOperations() + { + testcase("Nested array operations"); + STJson json; + + // Set entire elements in nested array + json.setNestedArrayElement("items", 0, std::make_shared(sfNetworkID, 10)); + json.setNestedArrayElement("items", 1, std::make_shared(sfNetworkID, 20)); + json.setNestedArrayElement("items", 2, std::make_shared(sfNetworkID, 30)); + + // Get elements + auto item0 = json.getNestedArrayElement("items", 0); + BEAST_EXPECT(item0.has_value()); + BEAST_EXPECT(std::dynamic_pointer_cast(*item0)->value() == 10); + + auto item2 = json.getNestedArrayElement("items", 2); + BEAST_EXPECT(item2.has_value()); + BEAST_EXPECT(std::dynamic_pointer_cast(*item2)->value() == 30); + + // Auto-resize test + json.setNestedArrayElement("items", 5, std::make_shared(sfNetworkID, 60)); + auto item5 = json.getNestedArrayElement("items", 5); + BEAST_EXPECT(item5.has_value()); + BEAST_EXPECT(std::dynamic_pointer_cast(*item5)->value() == 60); + } + + void + testNestedArrayElementFields() + { + testcase("Nested array element field operations"); + STJson json; + + // Set fields in nested array elements + json.setNestedArrayElementField( + "users", 0, "id", std::make_shared(sfNetworkID, 100)); + json.setNestedArrayElementField( + "users", 0, "name", std::make_shared(sfNetworkID, 200)); + json.setNestedArrayElementField( + "users", 1, "id", std::make_shared(sfNetworkID, 101)); + json.setNestedArrayElementField( + "users", 1, "name", std::make_shared(sfNetworkID, 201)); + + // Get fields + auto user0id = json.getNestedArrayElementField("users", 0, "id"); + BEAST_EXPECT(user0id.has_value()); + BEAST_EXPECT(std::dynamic_pointer_cast(*user0id)->value() == 100); + + auto user1name = json.getNestedArrayElementField("users", 1, "name"); + BEAST_EXPECT(user1name.has_value()); + BEAST_EXPECT(std::dynamic_pointer_cast(*user1name)->value() == 201); + + // Test missing field + auto missing = json.getNestedArrayElementField("users", 0, "age"); + BEAST_EXPECT(!missing.has_value()); + } + + void + testDepthValidation() + { + testcase("Depth validation (max 1 level)"); + + // Valid: Object with nested object (depth 1) + { + STJson json; + auto nested = std::make_shared(); + nested->setObjectField("x", std::make_shared(sfNetworkID, 42)); + + try + { + json.setObjectField("nested", nested); + pass(); + } + catch (...) + { + fail("Should allow depth 1 nesting"); + } + } + + // Invalid: Object with nested object containing nested object (depth 2) + { + STJson json; + auto nested1 = std::make_shared(); + auto nested2 = std::make_shared(); + nested2->setObjectField("x", std::make_shared(sfNetworkID, 42)); + nested1->setObjectField("nested", nested2); + + try + { + json.setObjectField("nested", nested1); + fail("Should reject depth 2 nesting"); + } + catch (std::runtime_error const& e) + { + pass(); + } + } + + // Valid: Array with object elements (depth 1) + { + STJson json(STJson::Array{}); + auto elem = std::make_shared(); + elem->setObjectField("x", std::make_shared(sfNetworkID, 42)); + + try + { + json.pushArrayElement(elem); + pass(); + } + catch (...) + { + fail("Should allow depth 1 in array"); + } + } + + // Invalid: Array with nested arrays (depth 2) + { + STJson json(STJson::Array{}); + auto innerArray = std::make_shared(STJson::Array{}); + innerArray->pushArrayElement(std::make_shared(sfNetworkID, 42)); + + try + { + json.pushArrayElement(innerArray); + fail("Should reject array of arrays"); + } + catch (std::runtime_error const& e) + { + pass(); + } + } + + // Valid: Object with nested array (depth 1) + { + STJson json; + auto arr = std::make_shared(STJson::Array{}); + arr->pushArrayElement(std::make_shared(sfNetworkID, 42)); + + try + { + json.setObjectField("arr", arr); + pass(); + } + catch (...) + { + fail("Should allow object with array"); + } + } + + // Test depth validation in setNestedObjectField + { + STJson json; + auto nested = std::make_shared(); + nested->setObjectField("x", std::make_shared(sfNetworkID, 42)); + + try + { + json.setNestedObjectField("outer", "inner", nested); + fail("Should reject depth 2 via setNestedObjectField"); + } + catch (std::runtime_error const& e) + { + pass(); + } + } + } + + void + testAddAndFromBlob() + { + testcase("add() and fromBlob() for objects"); + STJson json; + json.setObjectField("a", std::make_shared(sfCloseResolution, 7)); + json.setObjectField("b", std::make_shared(sfNetworkID, 123456)); + + Serializer s; + json.add(s); + + auto blob = s.peekData(); + auto parsed = STJson::fromBlob(blob.data(), blob.size()); + BEAST_EXPECT(parsed->isObject()); + BEAST_EXPECT(parsed->getMap().size() == 2); + + auto a = parsed->getObjectField("a"); + BEAST_EXPECT(a.has_value()); + BEAST_EXPECT(std::dynamic_pointer_cast(*a)->value() == 7); + + auto b = parsed->getObjectField("b"); + BEAST_EXPECT(b.has_value()); + BEAST_EXPECT(std::dynamic_pointer_cast(*b)->value() == 123456); + } + + void + testArraySerialization() + { + testcase("Array serialization and deserialization"); + STJson json(STJson::Array{}); + json.pushArrayElement(std::make_shared(sfCloseResolution, 10)); + json.pushArrayElement(std::make_shared(sfNetworkID, 20)); + json.pushArrayElement(std::make_shared(sfIndexNext, 30)); + + Serializer s; + json.add(s); + + auto blob = s.peekData(); + auto parsed = STJson::fromBlob(blob.data(), blob.size()); + + BEAST_EXPECT(parsed->isArray()); + BEAST_EXPECT(parsed->arraySize() == 3); + + auto elem0 = parsed->getArrayElement(0); + BEAST_EXPECT(std::dynamic_pointer_cast(*elem0)->value() == 10); + + auto elem1 = parsed->getArrayElement(1); + BEAST_EXPECT(std::dynamic_pointer_cast(*elem1)->value() == 20); + + auto elem2 = parsed->getArrayElement(2); + BEAST_EXPECT(std::dynamic_pointer_cast(*elem2)->value() == 30); + } + + void + testFromSerialIter() + { + testcase("fromSerialIter()"); + STJson json; + json.setObjectField("x", std::make_shared(sfCloseResolution, 99)); + Serializer s; + json.add(s); + + SerialIter sit(s.peekData().data(), s.peekData().size()); + auto parsed = STJson::fromSerialIter(sit); + BEAST_EXPECT(parsed->isObject()); + BEAST_EXPECT(parsed->getMap().size() == 1); + + auto x = parsed->getObjectField("x"); + BEAST_EXPECT(x.has_value()); + BEAST_EXPECT(std::dynamic_pointer_cast(*x)->value() == 99); + } + + void + testFromSField() + { + testcase("Constructor from SField"); + STJson json; + json.setObjectField("x", std::make_shared(sfCloseResolution, 99)); + Serializer s; + json.add(s); + + SerialIter sit(s.peekData().data(), s.peekData().size()); + auto parsed = STJson{sit, sfContractCode}; + BEAST_EXPECT(parsed.isObject()); + BEAST_EXPECT(parsed.getMap().size() == 1); + + auto x = parsed.getObjectField("x"); + BEAST_EXPECT(x.has_value()); + BEAST_EXPECT(std::dynamic_pointer_cast(*x)->value() == 99); + } + + void + testGetJson() + { + testcase("getJson() for objects"); + STJson json; + json.setObjectField("foo", std::make_shared(sfTransactionType, 65535)); + json.setObjectField("bar", nullptr); // test null value + + Json::Value jv = json.getJson(JsonOptions::none); + BEAST_EXPECT(jv.isObject()); + BEAST_EXPECT(jv["foo"].asUInt() == 65535); + BEAST_EXPECT(jv["bar"].isNull()); + } + + void + testGetJsonArray() + { + testcase("getJson() for arrays"); + STJson json(STJson::Array{}); + json.pushArrayElement(std::make_shared(sfNetworkID, 100)); + json.pushArrayElement(std::make_shared(sfNetworkID, 200)); + json.pushArrayElement(nullptr); // null element + + Json::Value jv = json.getJson(JsonOptions::none); + BEAST_EXPECT(jv.isArray()); + BEAST_EXPECT(jv.size() == 3); + BEAST_EXPECT(jv[Json::UInt(0)].asUInt() == 100); + BEAST_EXPECT(jv[Json::UInt(1)].asUInt() == 200); + BEAST_EXPECT(jv[Json::UInt(2)].isNull()); + } + + void + testMakeValueFromVLWithType() + { + testcase("makeValueFromVLWithType()"); + Serializer s; + s.add8(STI_UINT32); + s.add32(0xDEADBEEF); + SerialIter sit(s.peekData().data(), s.peekData().size()); + auto value = STJson::makeValueFromVLWithType(sit); + BEAST_EXPECT(value->getSType() == STI_UINT32); + BEAST_EXPECT(std::dynamic_pointer_cast(value)->value() == 0xDEADBEEF); + } + + void + testMixedStructures() + { + testcase("Mixed structures (objects with arrays)"); + STJson json; + + // Add simple fields + json.setObjectField("id", std::make_shared(sfNetworkID, 1)); + + // Add nested object + json.setNestedObjectField( + "metadata", "version", std::make_shared(sfNetworkID, 2)); + + // Add nested array with objects + json.setNestedArrayElementField( + "users", 0, "name", std::make_shared(sfNetworkID, 100)); + json.setNestedArrayElementField( + "users", 0, "age", std::make_shared(sfNetworkID, 25)); + json.setNestedArrayElementField( + "users", 1, "name", std::make_shared(sfNetworkID, 101)); + + // Serialize and deserialize + Serializer s; + json.add(s); + auto parsed = STJson::fromBlob(s.peekData().data(), s.peekData().size()); + + // Verify structure + BEAST_EXPECT(parsed->isObject()); + + auto id = parsed->getObjectField("id"); + BEAST_EXPECT(std::dynamic_pointer_cast(*id)->value() == 1); + + auto version = parsed->getNestedObjectField("metadata", "version"); + BEAST_EXPECT(std::dynamic_pointer_cast(*version)->value() == 2); + + auto user0name = parsed->getNestedArrayElementField("users", 0, "name"); + BEAST_EXPECT(std::dynamic_pointer_cast(*user0name)->value() == 100); + + auto user1name = parsed->getNestedArrayElementField("users", 1, "name"); + BEAST_EXPECT(std::dynamic_pointer_cast(*user1name)->value() == 101); + } + + void + testSTTypes() + { + testcase("All STypes roundtrip"); + + // STI_UINT8 + { + STJson json; + json.setObjectField("u8", std::make_shared(sfCloseResolution, 200)); + Serializer s; + json.add(s); + auto parsed = STJson::fromBlob(s.peekData().data(), s.peekData().size()); + auto u8 = parsed->getObjectField("u8"); + BEAST_EXPECT(std::dynamic_pointer_cast(*u8)->value() == 200); + } + + // STI_UINT16 + { + STJson json; + json.setObjectField("u16", std::make_shared(sfSignerWeight, 4242)); + Serializer s; + json.add(s); + auto parsed = STJson::fromBlob(s.peekData().data(), s.peekData().size()); + auto u16 = parsed->getObjectField("u16"); + BEAST_EXPECT(std::dynamic_pointer_cast(*u16)->value() == 4242); + } + + // STI_UINT32 + { + STJson json; + json.setObjectField("u32", std::make_shared(sfNetworkID, 0xABCDEF01)); + Serializer s; + json.add(s); + auto parsed = STJson::fromBlob(s.peekData().data(), s.peekData().size()); + auto u32 = parsed->getObjectField("u32"); + BEAST_EXPECT(std::dynamic_pointer_cast(*u32)->value() == 0xABCDEF01); + } + + // STI_UINT64 + { + STJson json; + json.setObjectField( + "u64", std::make_shared(sfIndexNext, 0x123456789ABCDEF0ull)); + Serializer s; + json.add(s); + auto parsed = STJson::fromBlob(s.peekData().data(), s.peekData().size()); + auto u64 = parsed->getObjectField("u64"); + BEAST_EXPECT( + std::dynamic_pointer_cast(*u64)->value() == 0x123456789ABCDEF0ull); + } + + // STI_UINT160 + { + STJson json; + uint160 val; + val.data()[0] = 0x01; + val.data()[19] = 0xFF; + json.setObjectField("u160", std::make_shared(sfTakerPaysCurrency, val)); + Serializer s; + json.add(s); + auto parsed = STJson::fromBlob(s.peekData().data(), s.peekData().size()); + auto u160 = parsed->getObjectField("u160"); + BEAST_EXPECT(std::dynamic_pointer_cast(*u160)->value() == val); + } + + // STI_UINT256 + { + STJson json; + uint256 val; + val.data()[0] = 0xAA; + val.data()[31] = 0xBB; + json.setObjectField("u256", std::make_shared(sfLedgerHash, val)); + Serializer s; + json.add(s); + auto parsed = STJson::fromBlob(s.peekData().data(), s.peekData().size()); + auto u256 = parsed->getObjectField("u256"); + BEAST_EXPECT(std::dynamic_pointer_cast(*u256)->value() == val); + } + + // STI_AMOUNT + { + STJson json; + // XRP amount + STAmount xrp(sfAmount, static_cast(123456789u)); + json.setObjectField("amount", std::make_shared(xrp)); + Serializer s; + json.add(s); + auto parsed = STJson::fromBlob(s.peekData().data(), s.peekData().size()); + auto amount = parsed->getObjectField("amount"); + auto parsedAmt = std::dynamic_pointer_cast(*amount); + BEAST_EXPECT(parsedAmt->mantissa() == 123456789u); + BEAST_EXPECT(parsedAmt->issue() == xrp.issue()); + } + + // STI_VL (STBlob) + { + STJson json; + std::vector blobData = {0xDE, 0xAD, 0xBE, 0xEF}; + json.setObjectField( + "blob", std::make_shared(sfPublicKey, blobData.data(), blobData.size())); + Serializer s; + json.add(s); + auto parsed = STJson::fromBlob(s.peekData().data(), s.peekData().size()); + auto blob = parsed->getObjectField("blob"); + auto parsedBlob = std::dynamic_pointer_cast(*blob); + BEAST_EXPECT(parsedBlob->size() == blobData.size()); + BEAST_EXPECT(std::memcmp(parsedBlob->data(), blobData.data(), blobData.size()) == 0); + } + + // STI_ACCOUNT + { + STJson json; + // Use a known AccountID (20 bytes) + AccountID acct = AccountID{}; + json.setObjectField("acct", std::make_shared(sfAccount, acct)); + Serializer s; + json.add(s); + auto parsed = STJson::fromBlob(s.peekData().data(), s.peekData().size()); + auto account = parsed->getObjectField("acct"); + auto parsedAcct = std::dynamic_pointer_cast(*account); + BEAST_EXPECT(parsedAcct->value() == acct); + } + + // STI_CURRENCY (STCurrency) + { + STJson json; + Currency cur; + cur.data()[0] = 0xAA; + cur.data()[19] = 0xBB; + json.setObjectField("currency", std::make_shared(sfGeneric, cur)); + Serializer s; + json.add(s); + auto parsed = STJson::fromBlob(s.peekData().data(), s.peekData().size()); + auto currency = parsed->getObjectField("currency"); + auto parsedCur = std::dynamic_pointer_cast(*currency); + BEAST_EXPECT(parsedCur->value() == cur); + } + + // STI_JSON (STJson) Nested JSON + { + STJson innerJson; + // XRP amount + STAmount xrp(sfAmount, static_cast(123456789u)); + innerJson.setObjectField("amount", std::make_shared(xrp)); + + STJson json; + json.setObjectField("nested", std::make_shared(innerJson)); + Serializer s; + json.add(s); + + auto parsed = STJson::fromBlob(s.peekData().data(), s.peekData().size()); + auto nested = parsed->getObjectField("nested"); + auto parsedNested = std::dynamic_pointer_cast(*nested); + auto amount = parsedNested->getObjectField("amount"); + BEAST_EXPECT(std::dynamic_pointer_cast(*amount)->mantissa() == 123456789u); + } + } + + void + testEdgeCases() + { + testcase("Edge cases"); + + // Empty object + { + STJson json; + Serializer s; + json.add(s); + auto parsed = STJson::fromBlob(s.peekData().data(), s.peekData().size()); + BEAST_EXPECT(parsed->isObject()); + BEAST_EXPECT(parsed->getMap().empty()); + } + + // Empty array + { + STJson json(STJson::Array{}); + Serializer s; + json.add(s); + auto parsed = STJson::fromBlob(s.peekData().data(), s.peekData().size()); + BEAST_EXPECT(parsed->isArray()); + BEAST_EXPECT(parsed->arraySize() == 0); + } + + // Array with null elements + { + STJson json(STJson::Array{}); + json.pushArrayElement(nullptr); + json.pushArrayElement(std::make_shared(sfNetworkID, 42)); + json.pushArrayElement(nullptr); + + BEAST_EXPECT(json.arraySize() == 3); + + auto elem0 = json.getArrayElement(0); + BEAST_EXPECT(elem0.has_value()); + BEAST_EXPECT(*elem0 == nullptr); + + auto elem1 = json.getArrayElement(1); + BEAST_EXPECT(elem1.has_value()); + BEAST_EXPECT(*elem1 != nullptr); + } + + // Object with null value + { + STJson json; + json.setObjectField("null_field", nullptr); + + auto val = json.getObjectField("null_field"); + BEAST_EXPECT(val.has_value()); + BEAST_EXPECT(*val == nullptr); + } + } + + void + run() override + { + testDefaultConstructor(); + testSetAndGet(); + testMoveConstructor(); + testArrayConstruction(); + testTypeChecking(); + testArrayOperations(); + testArrayAutoResize(); + testArrayElementFields(); + testNestedObjectField(); + testNestedArrayOperations(); + testNestedArrayElementFields(); + testDepthValidation(); + testAddAndFromBlob(); + testArraySerialization(); + testFromSerialIter(); + testFromSField(); + testGetJson(); + testGetJsonArray(); + testMakeValueFromVLWithType(); + testMixedStructures(); + testSTTypes(); + testEdgeCases(); + } +}; + +BEAST_DEFINE_TESTSUITE(STJson, protocol, xrpl); + +} // namespace xrpl diff --git a/src/tests/libxrpl/protocol_autogen/TestHelpers.h b/src/tests/libxrpl/protocol_autogen/TestHelpers.h index cf2d7ffe6a..5bcf06cadc 100644 --- a/src/tests/libxrpl/protocol_autogen/TestHelpers.h +++ b/src/tests/libxrpl/protocol_autogen/TestHelpers.h @@ -9,6 +9,7 @@ #include #include #include +#include #include #include #include @@ -148,6 +149,13 @@ canonical_XCHAIN_BRIDGE() return XChainBridgeValue{xrpAccount(), xrpIssue(), xrpAccount(), xrpIssue()}; } +using JsonValue = std::decay_t; +inline JsonValue +canonical_JSON() +{ + return JsonValue{}; +} + // Untyped field canonical values inline STArray diff --git a/src/tests/libxrpl/protocol_autogen/ledger_entries/AccountRootTests.cpp b/src/tests/libxrpl/protocol_autogen/ledger_entries/AccountRootTests.cpp index e967b614b7..3191f29da4 100644 --- a/src/tests/libxrpl/protocol_autogen/ledger_entries/AccountRootTests.cpp +++ b/src/tests/libxrpl/protocol_autogen/ledger_entries/AccountRootTests.cpp @@ -43,6 +43,7 @@ TEST(AccountRootTests, BuilderSettersRoundTrip) auto const aMMIDValue = canonical_UINT256(); auto const vaultIDValue = canonical_UINT256(); auto const loanBrokerIDValue = canonical_UINT256(); + auto const contractIDValue = canonical_UINT256(); AccountRootBuilder builder{ accountValue, @@ -70,6 +71,7 @@ TEST(AccountRootTests, BuilderSettersRoundTrip) builder.setAMMID(aMMIDValue); builder.setVaultID(vaultIDValue); builder.setLoanBrokerID(loanBrokerIDValue); + builder.setContractID(contractIDValue); builder.setLedgerIndex(index); builder.setFlags(0x1u); @@ -252,6 +254,14 @@ TEST(AccountRootTests, BuilderSettersRoundTrip) EXPECT_TRUE(entry.hasLoanBrokerID()); } + { + auto const& expected = contractIDValue; + auto const actualOpt = entry.getContractID(); + ASSERT_TRUE(actualOpt.has_value()); + expectEqualField(expected, *actualOpt, "sfContractID"); + EXPECT_TRUE(entry.hasContractID()); + } + EXPECT_TRUE(entry.hasLedgerIndex()); auto const ledgerIndex = entry.getLedgerIndex(); ASSERT_TRUE(ledgerIndex.has_value()); @@ -288,6 +298,7 @@ TEST(AccountRootTests, BuilderFromSleRoundTrip) auto const aMMIDValue = canonical_UINT256(); auto const vaultIDValue = canonical_UINT256(); auto const loanBrokerIDValue = canonical_UINT256(); + auto const contractIDValue = canonical_UINT256(); auto sle = std::make_shared(AccountRoot::entryType, index); @@ -314,6 +325,7 @@ TEST(AccountRootTests, BuilderFromSleRoundTrip) sle->at(sfAMMID) = aMMIDValue; sle->at(sfVaultID) = vaultIDValue; sle->at(sfLoanBrokerID) = loanBrokerIDValue; + sle->at(sfContractID) = contractIDValue; AccountRootBuilder builderFromSle{sle}; EXPECT_TRUE(builderFromSle.validate()); @@ -605,6 +617,19 @@ TEST(AccountRootTests, BuilderFromSleRoundTrip) expectEqualField(expected, *fromBuilderOpt, "sfLoanBrokerID"); } + { + auto const& expected = contractIDValue; + + auto const fromSleOpt = entryFromSle.getContractID(); + auto const fromBuilderOpt = entryFromBuilder.getContractID(); + + ASSERT_TRUE(fromSleOpt.has_value()); + ASSERT_TRUE(fromBuilderOpt.has_value()); + + expectEqualField(expected, *fromSleOpt, "sfContractID"); + expectEqualField(expected, *fromBuilderOpt, "sfContractID"); + } + EXPECT_EQ(entryFromSle.getKey(), index); EXPECT_EQ(entryFromBuilder.getKey(), index); } @@ -703,5 +728,7 @@ TEST(AccountRootTests, OptionalFieldsReturnNullopt) EXPECT_FALSE(entry.getVaultID().has_value()); EXPECT_FALSE(entry.hasLoanBrokerID()); EXPECT_FALSE(entry.getLoanBrokerID().has_value()); + EXPECT_FALSE(entry.hasContractID()); + EXPECT_FALSE(entry.getContractID().has_value()); } } diff --git a/src/tests/libxrpl/protocol_autogen/ledger_entries/ContractDataTests.cpp b/src/tests/libxrpl/protocol_autogen/ledger_entries/ContractDataTests.cpp new file mode 100644 index 0000000000..eda0003568 --- /dev/null +++ b/src/tests/libxrpl/protocol_autogen/ledger_entries/ContractDataTests.cpp @@ -0,0 +1,223 @@ +// Auto-generated unit tests for ledger entry ContractData + + +#include + +#include + +#include +#include +#include + +#include + +namespace xrpl::ledger_entries { + +// 1 & 4) Set fields via builder setters, build, then read them back via +// wrapper getters. After build(), validate() should succeed for both the +// builder's STObject and the wrapper's SLE. +TEST(ContractDataTests, BuilderSettersRoundTrip) +{ + uint256 const index{1u}; + + auto const previousTxnIDValue = canonical_UINT256(); + auto const previousTxnLgrSeqValue = canonical_UINT32(); + auto const ownerNodeValue = canonical_UINT64(); + auto const ownerValue = canonical_ACCOUNT(); + auto const contractAccountValue = canonical_ACCOUNT(); + auto const contractJsonValue = canonical_JSON(); + + ContractDataBuilder builder{ + previousTxnIDValue, + previousTxnLgrSeqValue, + ownerNodeValue, + ownerValue, + contractAccountValue, + contractJsonValue + }; + + + builder.setLedgerIndex(index); + builder.setFlags(0x1u); + + EXPECT_TRUE(builder.validate()); + + auto const entry = builder.build(index); + + EXPECT_TRUE(entry.validate()); + + { + auto const& expected = previousTxnIDValue; + auto const actual = entry.getPreviousTxnID(); + expectEqualField(expected, actual, "sfPreviousTxnID"); + } + + { + auto const& expected = previousTxnLgrSeqValue; + auto const actual = entry.getPreviousTxnLgrSeq(); + expectEqualField(expected, actual, "sfPreviousTxnLgrSeq"); + } + + { + auto const& expected = ownerNodeValue; + auto const actual = entry.getOwnerNode(); + expectEqualField(expected, actual, "sfOwnerNode"); + } + + { + auto const& expected = ownerValue; + auto const actual = entry.getOwner(); + expectEqualField(expected, actual, "sfOwner"); + } + + { + auto const& expected = contractAccountValue; + auto const actual = entry.getContractAccount(); + expectEqualField(expected, actual, "sfContractAccount"); + } + + { + auto const& expected = contractJsonValue; + auto const actual = entry.getContractJson(); + expectEqualField(expected, actual, "sfContractJson"); + } + + EXPECT_TRUE(entry.hasLedgerIndex()); + auto const ledgerIndex = entry.getLedgerIndex(); + ASSERT_TRUE(ledgerIndex.has_value()); + EXPECT_EQ(*ledgerIndex, index); + EXPECT_EQ(entry.getKey(), index); +} + +// 2 & 4) Start from an SLE, set fields directly on it, construct a builder +// from that SLE, build a new wrapper, and verify all fields (and validate()). +TEST(ContractDataTests, BuilderFromSleRoundTrip) +{ + uint256 const index{2u}; + + auto const previousTxnIDValue = canonical_UINT256(); + auto const previousTxnLgrSeqValue = canonical_UINT32(); + auto const ownerNodeValue = canonical_UINT64(); + auto const ownerValue = canonical_ACCOUNT(); + auto const contractAccountValue = canonical_ACCOUNT(); + auto const contractJsonValue = canonical_JSON(); + + auto sle = std::make_shared(ContractData::entryType, index); + + sle->at(sfPreviousTxnID) = previousTxnIDValue; + sle->at(sfPreviousTxnLgrSeq) = previousTxnLgrSeqValue; + sle->at(sfOwnerNode) = ownerNodeValue; + sle->at(sfOwner) = ownerValue; + sle->at(sfContractAccount) = contractAccountValue; + sle->at(sfContractJson) = contractJsonValue; + + ContractDataBuilder builderFromSle{sle}; + EXPECT_TRUE(builderFromSle.validate()); + + auto const entryFromBuilder = builderFromSle.build(index); + + ContractData entryFromSle{sle}; + EXPECT_TRUE(entryFromBuilder.validate()); + EXPECT_TRUE(entryFromSle.validate()); + + { + auto const& expected = previousTxnIDValue; + + auto const fromSle = entryFromSle.getPreviousTxnID(); + auto const fromBuilder = entryFromBuilder.getPreviousTxnID(); + + expectEqualField(expected, fromSle, "sfPreviousTxnID"); + expectEqualField(expected, fromBuilder, "sfPreviousTxnID"); + } + + { + auto const& expected = previousTxnLgrSeqValue; + + auto const fromSle = entryFromSle.getPreviousTxnLgrSeq(); + auto const fromBuilder = entryFromBuilder.getPreviousTxnLgrSeq(); + + expectEqualField(expected, fromSle, "sfPreviousTxnLgrSeq"); + expectEqualField(expected, fromBuilder, "sfPreviousTxnLgrSeq"); + } + + { + auto const& expected = ownerNodeValue; + + auto const fromSle = entryFromSle.getOwnerNode(); + auto const fromBuilder = entryFromBuilder.getOwnerNode(); + + expectEqualField(expected, fromSle, "sfOwnerNode"); + expectEqualField(expected, fromBuilder, "sfOwnerNode"); + } + + { + auto const& expected = ownerValue; + + auto const fromSle = entryFromSle.getOwner(); + auto const fromBuilder = entryFromBuilder.getOwner(); + + expectEqualField(expected, fromSle, "sfOwner"); + expectEqualField(expected, fromBuilder, "sfOwner"); + } + + { + auto const& expected = contractAccountValue; + + auto const fromSle = entryFromSle.getContractAccount(); + auto const fromBuilder = entryFromBuilder.getContractAccount(); + + expectEqualField(expected, fromSle, "sfContractAccount"); + expectEqualField(expected, fromBuilder, "sfContractAccount"); + } + + { + auto const& expected = contractJsonValue; + + auto const fromSle = entryFromSle.getContractJson(); + auto const fromBuilder = entryFromBuilder.getContractJson(); + + expectEqualField(expected, fromSle, "sfContractJson"); + expectEqualField(expected, fromBuilder, "sfContractJson"); + } + + EXPECT_EQ(entryFromSle.getKey(), index); + EXPECT_EQ(entryFromBuilder.getKey(), index); +} + +// 3) Verify wrapper throws when constructed from wrong ledger entry type. +TEST(ContractDataTests, WrapperThrowsOnWrongEntryType) +{ + uint256 const index{3u}; + + // Build a valid ledger entry of a different type + // Ticket requires: Account, OwnerNode, TicketSequence, PreviousTxnID, PreviousTxnLgrSeq + // Check requires: Account, Destination, SendMax, Sequence, OwnerNode, DestinationNode, PreviousTxnID, PreviousTxnLgrSeq + TicketBuilder wrongBuilder{ + canonical_ACCOUNT(), + canonical_UINT64(), + canonical_UINT32(), + canonical_UINT256(), + canonical_UINT32()}; + auto wrongEntry = wrongBuilder.build(index); + + EXPECT_THROW(ContractData{wrongEntry.getSle()}, std::runtime_error); +} + +// 4) Verify builder throws when constructed from wrong ledger entry type. +TEST(ContractDataTests, BuilderThrowsOnWrongEntryType) +{ + uint256 const index{4u}; + + // Build a valid ledger entry of a different type + TicketBuilder wrongBuilder{ + canonical_ACCOUNT(), + canonical_UINT64(), + canonical_UINT32(), + canonical_UINT256(), + canonical_UINT32()}; + auto wrongEntry = wrongBuilder.build(index); + + EXPECT_THROW(ContractDataBuilder{wrongEntry.getSle()}, std::runtime_error); +} + +} diff --git a/src/tests/libxrpl/protocol_autogen/ledger_entries/ContractSourceTests.cpp b/src/tests/libxrpl/protocol_autogen/ledger_entries/ContractSourceTests.cpp new file mode 100644 index 0000000000..52e83a7fa5 --- /dev/null +++ b/src/tests/libxrpl/protocol_autogen/ledger_entries/ContractSourceTests.cpp @@ -0,0 +1,275 @@ +// Auto-generated unit tests for ledger entry ContractSource + + +#include + +#include + +#include +#include +#include + +#include + +namespace xrpl::ledger_entries { + +// 1 & 4) Set fields via builder setters, build, then read them back via +// wrapper getters. After build(), validate() should succeed for both the +// builder's STObject and the wrapper's SLE. +TEST(ContractSourceTests, BuilderSettersRoundTrip) +{ + uint256 const index{1u}; + + auto const previousTxnIDValue = canonical_UINT256(); + auto const previousTxnLgrSeqValue = canonical_UINT32(); + auto const contractHashValue = canonical_UINT256(); + auto const contractCodeValue = canonical_VL(); + auto const functionsValue = canonical_ARRAY(); + auto const instanceParametersValue = canonical_ARRAY(); + auto const referenceCountValue = canonical_UINT64(); + + ContractSourceBuilder builder{ + previousTxnIDValue, + previousTxnLgrSeqValue, + contractHashValue, + contractCodeValue, + functionsValue, + referenceCountValue + }; + + builder.setInstanceParameters(instanceParametersValue); + + builder.setLedgerIndex(index); + builder.setFlags(0x1u); + + EXPECT_TRUE(builder.validate()); + + auto const entry = builder.build(index); + + EXPECT_TRUE(entry.validate()); + + { + auto const& expected = previousTxnIDValue; + auto const actual = entry.getPreviousTxnID(); + expectEqualField(expected, actual, "sfPreviousTxnID"); + } + + { + auto const& expected = previousTxnLgrSeqValue; + auto const actual = entry.getPreviousTxnLgrSeq(); + expectEqualField(expected, actual, "sfPreviousTxnLgrSeq"); + } + + { + auto const& expected = contractHashValue; + auto const actual = entry.getContractHash(); + expectEqualField(expected, actual, "sfContractHash"); + } + + { + auto const& expected = contractCodeValue; + auto const actual = entry.getContractCode(); + expectEqualField(expected, actual, "sfContractCode"); + } + + { + auto const& expected = functionsValue; + auto const actual = entry.getFunctions(); + expectEqualField(expected, actual, "sfFunctions"); + } + + { + auto const& expected = referenceCountValue; + auto const actual = entry.getReferenceCount(); + expectEqualField(expected, actual, "sfReferenceCount"); + } + + { + auto const& expected = instanceParametersValue; + auto const actualOpt = entry.getInstanceParameters(); + ASSERT_TRUE(actualOpt.has_value()); + expectEqualField(expected, *actualOpt, "sfInstanceParameters"); + EXPECT_TRUE(entry.hasInstanceParameters()); + } + + EXPECT_TRUE(entry.hasLedgerIndex()); + auto const ledgerIndex = entry.getLedgerIndex(); + ASSERT_TRUE(ledgerIndex.has_value()); + EXPECT_EQ(*ledgerIndex, index); + EXPECT_EQ(entry.getKey(), index); +} + +// 2 & 4) Start from an SLE, set fields directly on it, construct a builder +// from that SLE, build a new wrapper, and verify all fields (and validate()). +TEST(ContractSourceTests, BuilderFromSleRoundTrip) +{ + uint256 const index{2u}; + + auto const previousTxnIDValue = canonical_UINT256(); + auto const previousTxnLgrSeqValue = canonical_UINT32(); + auto const contractHashValue = canonical_UINT256(); + auto const contractCodeValue = canonical_VL(); + auto const functionsValue = canonical_ARRAY(); + auto const instanceParametersValue = canonical_ARRAY(); + auto const referenceCountValue = canonical_UINT64(); + + auto sle = std::make_shared(ContractSource::entryType, index); + + sle->at(sfPreviousTxnID) = previousTxnIDValue; + sle->at(sfPreviousTxnLgrSeq) = previousTxnLgrSeqValue; + sle->at(sfContractHash) = contractHashValue; + sle->at(sfContractCode) = contractCodeValue; + sle->setFieldArray(sfFunctions, functionsValue); + sle->setFieldArray(sfInstanceParameters, instanceParametersValue); + sle->at(sfReferenceCount) = referenceCountValue; + + ContractSourceBuilder builderFromSle{sle}; + EXPECT_TRUE(builderFromSle.validate()); + + auto const entryFromBuilder = builderFromSle.build(index); + + ContractSource entryFromSle{sle}; + EXPECT_TRUE(entryFromBuilder.validate()); + EXPECT_TRUE(entryFromSle.validate()); + + { + auto const& expected = previousTxnIDValue; + + auto const fromSle = entryFromSle.getPreviousTxnID(); + auto const fromBuilder = entryFromBuilder.getPreviousTxnID(); + + expectEqualField(expected, fromSle, "sfPreviousTxnID"); + expectEqualField(expected, fromBuilder, "sfPreviousTxnID"); + } + + { + auto const& expected = previousTxnLgrSeqValue; + + auto const fromSle = entryFromSle.getPreviousTxnLgrSeq(); + auto const fromBuilder = entryFromBuilder.getPreviousTxnLgrSeq(); + + expectEqualField(expected, fromSle, "sfPreviousTxnLgrSeq"); + expectEqualField(expected, fromBuilder, "sfPreviousTxnLgrSeq"); + } + + { + auto const& expected = contractHashValue; + + auto const fromSle = entryFromSle.getContractHash(); + auto const fromBuilder = entryFromBuilder.getContractHash(); + + expectEqualField(expected, fromSle, "sfContractHash"); + expectEqualField(expected, fromBuilder, "sfContractHash"); + } + + { + auto const& expected = contractCodeValue; + + auto const fromSle = entryFromSle.getContractCode(); + auto const fromBuilder = entryFromBuilder.getContractCode(); + + expectEqualField(expected, fromSle, "sfContractCode"); + expectEqualField(expected, fromBuilder, "sfContractCode"); + } + + { + auto const& expected = functionsValue; + + auto const fromSle = entryFromSle.getFunctions(); + auto const fromBuilder = entryFromBuilder.getFunctions(); + + expectEqualField(expected, fromSle, "sfFunctions"); + expectEqualField(expected, fromBuilder, "sfFunctions"); + } + + { + auto const& expected = referenceCountValue; + + auto const fromSle = entryFromSle.getReferenceCount(); + auto const fromBuilder = entryFromBuilder.getReferenceCount(); + + expectEqualField(expected, fromSle, "sfReferenceCount"); + expectEqualField(expected, fromBuilder, "sfReferenceCount"); + } + + { + auto const& expected = instanceParametersValue; + + auto const fromSleOpt = entryFromSle.getInstanceParameters(); + auto const fromBuilderOpt = entryFromBuilder.getInstanceParameters(); + + ASSERT_TRUE(fromSleOpt.has_value()); + ASSERT_TRUE(fromBuilderOpt.has_value()); + + expectEqualField(expected, *fromSleOpt, "sfInstanceParameters"); + expectEqualField(expected, *fromBuilderOpt, "sfInstanceParameters"); + } + + EXPECT_EQ(entryFromSle.getKey(), index); + EXPECT_EQ(entryFromBuilder.getKey(), index); +} + +// 3) Verify wrapper throws when constructed from wrong ledger entry type. +TEST(ContractSourceTests, WrapperThrowsOnWrongEntryType) +{ + uint256 const index{3u}; + + // Build a valid ledger entry of a different type + // Ticket requires: Account, OwnerNode, TicketSequence, PreviousTxnID, PreviousTxnLgrSeq + // Check requires: Account, Destination, SendMax, Sequence, OwnerNode, DestinationNode, PreviousTxnID, PreviousTxnLgrSeq + TicketBuilder wrongBuilder{ + canonical_ACCOUNT(), + canonical_UINT64(), + canonical_UINT32(), + canonical_UINT256(), + canonical_UINT32()}; + auto wrongEntry = wrongBuilder.build(index); + + EXPECT_THROW(ContractSource{wrongEntry.getSle()}, std::runtime_error); +} + +// 4) Verify builder throws when constructed from wrong ledger entry type. +TEST(ContractSourceTests, BuilderThrowsOnWrongEntryType) +{ + uint256 const index{4u}; + + // Build a valid ledger entry of a different type + TicketBuilder wrongBuilder{ + canonical_ACCOUNT(), + canonical_UINT64(), + canonical_UINT32(), + canonical_UINT256(), + canonical_UINT32()}; + auto wrongEntry = wrongBuilder.build(index); + + EXPECT_THROW(ContractSourceBuilder{wrongEntry.getSle()}, std::runtime_error); +} + +// 5) Build with only required fields and verify optional fields return nullopt. +TEST(ContractSourceTests, OptionalFieldsReturnNullopt) +{ + uint256 const index{3u}; + + auto const previousTxnIDValue = canonical_UINT256(); + auto const previousTxnLgrSeqValue = canonical_UINT32(); + auto const contractHashValue = canonical_UINT256(); + auto const contractCodeValue = canonical_VL(); + auto const functionsValue = canonical_ARRAY(); + auto const referenceCountValue = canonical_UINT64(); + + ContractSourceBuilder builder{ + previousTxnIDValue, + previousTxnLgrSeqValue, + contractHashValue, + contractCodeValue, + functionsValue, + referenceCountValue + }; + + auto const entry = builder.build(index); + + // Verify optional fields are not present + EXPECT_FALSE(entry.hasInstanceParameters()); + EXPECT_FALSE(entry.getInstanceParameters().has_value()); +} +} diff --git a/src/tests/libxrpl/protocol_autogen/ledger_entries/ContractTests.cpp b/src/tests/libxrpl/protocol_autogen/ledger_entries/ContractTests.cpp new file mode 100644 index 0000000000..a25cb55825 --- /dev/null +++ b/src/tests/libxrpl/protocol_autogen/ledger_entries/ContractTests.cpp @@ -0,0 +1,324 @@ +// Auto-generated unit tests for ledger entry Contract + + +#include + +#include + +#include +#include +#include + +#include + +namespace xrpl::ledger_entries { + +// 1 & 4) Set fields via builder setters, build, then read them back via +// wrapper getters. After build(), validate() should succeed for both the +// builder's STObject and the wrapper's SLE. +TEST(ContractTests, BuilderSettersRoundTrip) +{ + uint256 const index{1u}; + + auto const previousTxnIDValue = canonical_UINT256(); + auto const previousTxnLgrSeqValue = canonical_UINT32(); + auto const sequenceValue = canonical_UINT32(); + auto const ownerNodeValue = canonical_UINT64(); + auto const ownerValue = canonical_ACCOUNT(); + auto const contractAccountValue = canonical_ACCOUNT(); + auto const contractHashValue = canonical_UINT256(); + auto const instanceParameterValuesValue = canonical_ARRAY(); + auto const uRIValue = canonical_VL(); + + ContractBuilder builder{ + previousTxnIDValue, + previousTxnLgrSeqValue, + sequenceValue, + ownerNodeValue, + ownerValue, + contractAccountValue, + contractHashValue + }; + + builder.setInstanceParameterValues(instanceParameterValuesValue); + builder.setURI(uRIValue); + + builder.setLedgerIndex(index); + builder.setFlags(0x1u); + + EXPECT_TRUE(builder.validate()); + + auto const entry = builder.build(index); + + EXPECT_TRUE(entry.validate()); + + { + auto const& expected = previousTxnIDValue; + auto const actual = entry.getPreviousTxnID(); + expectEqualField(expected, actual, "sfPreviousTxnID"); + } + + { + auto const& expected = previousTxnLgrSeqValue; + auto const actual = entry.getPreviousTxnLgrSeq(); + expectEqualField(expected, actual, "sfPreviousTxnLgrSeq"); + } + + { + auto const& expected = sequenceValue; + auto const actual = entry.getSequence(); + expectEqualField(expected, actual, "sfSequence"); + } + + { + auto const& expected = ownerNodeValue; + auto const actual = entry.getOwnerNode(); + expectEqualField(expected, actual, "sfOwnerNode"); + } + + { + auto const& expected = ownerValue; + auto const actual = entry.getOwner(); + expectEqualField(expected, actual, "sfOwner"); + } + + { + auto const& expected = contractAccountValue; + auto const actual = entry.getContractAccount(); + expectEqualField(expected, actual, "sfContractAccount"); + } + + { + auto const& expected = contractHashValue; + auto const actual = entry.getContractHash(); + expectEqualField(expected, actual, "sfContractHash"); + } + + { + auto const& expected = instanceParameterValuesValue; + auto const actualOpt = entry.getInstanceParameterValues(); + ASSERT_TRUE(actualOpt.has_value()); + expectEqualField(expected, *actualOpt, "sfInstanceParameterValues"); + EXPECT_TRUE(entry.hasInstanceParameterValues()); + } + + { + auto const& expected = uRIValue; + auto const actualOpt = entry.getURI(); + ASSERT_TRUE(actualOpt.has_value()); + expectEqualField(expected, *actualOpt, "sfURI"); + EXPECT_TRUE(entry.hasURI()); + } + + EXPECT_TRUE(entry.hasLedgerIndex()); + auto const ledgerIndex = entry.getLedgerIndex(); + ASSERT_TRUE(ledgerIndex.has_value()); + EXPECT_EQ(*ledgerIndex, index); + EXPECT_EQ(entry.getKey(), index); +} + +// 2 & 4) Start from an SLE, set fields directly on it, construct a builder +// from that SLE, build a new wrapper, and verify all fields (and validate()). +TEST(ContractTests, BuilderFromSleRoundTrip) +{ + uint256 const index{2u}; + + auto const previousTxnIDValue = canonical_UINT256(); + auto const previousTxnLgrSeqValue = canonical_UINT32(); + auto const sequenceValue = canonical_UINT32(); + auto const ownerNodeValue = canonical_UINT64(); + auto const ownerValue = canonical_ACCOUNT(); + auto const contractAccountValue = canonical_ACCOUNT(); + auto const contractHashValue = canonical_UINT256(); + auto const instanceParameterValuesValue = canonical_ARRAY(); + auto const uRIValue = canonical_VL(); + + auto sle = std::make_shared(Contract::entryType, index); + + sle->at(sfPreviousTxnID) = previousTxnIDValue; + sle->at(sfPreviousTxnLgrSeq) = previousTxnLgrSeqValue; + sle->at(sfSequence) = sequenceValue; + sle->at(sfOwnerNode) = ownerNodeValue; + sle->at(sfOwner) = ownerValue; + sle->at(sfContractAccount) = contractAccountValue; + sle->at(sfContractHash) = contractHashValue; + sle->setFieldArray(sfInstanceParameterValues, instanceParameterValuesValue); + sle->at(sfURI) = uRIValue; + + ContractBuilder builderFromSle{sle}; + EXPECT_TRUE(builderFromSle.validate()); + + auto const entryFromBuilder = builderFromSle.build(index); + + Contract entryFromSle{sle}; + EXPECT_TRUE(entryFromBuilder.validate()); + EXPECT_TRUE(entryFromSle.validate()); + + { + auto const& expected = previousTxnIDValue; + + auto const fromSle = entryFromSle.getPreviousTxnID(); + auto const fromBuilder = entryFromBuilder.getPreviousTxnID(); + + expectEqualField(expected, fromSle, "sfPreviousTxnID"); + expectEqualField(expected, fromBuilder, "sfPreviousTxnID"); + } + + { + auto const& expected = previousTxnLgrSeqValue; + + auto const fromSle = entryFromSle.getPreviousTxnLgrSeq(); + auto const fromBuilder = entryFromBuilder.getPreviousTxnLgrSeq(); + + expectEqualField(expected, fromSle, "sfPreviousTxnLgrSeq"); + expectEqualField(expected, fromBuilder, "sfPreviousTxnLgrSeq"); + } + + { + auto const& expected = sequenceValue; + + auto const fromSle = entryFromSle.getSequence(); + auto const fromBuilder = entryFromBuilder.getSequence(); + + expectEqualField(expected, fromSle, "sfSequence"); + expectEqualField(expected, fromBuilder, "sfSequence"); + } + + { + auto const& expected = ownerNodeValue; + + auto const fromSle = entryFromSle.getOwnerNode(); + auto const fromBuilder = entryFromBuilder.getOwnerNode(); + + expectEqualField(expected, fromSle, "sfOwnerNode"); + expectEqualField(expected, fromBuilder, "sfOwnerNode"); + } + + { + auto const& expected = ownerValue; + + auto const fromSle = entryFromSle.getOwner(); + auto const fromBuilder = entryFromBuilder.getOwner(); + + expectEqualField(expected, fromSle, "sfOwner"); + expectEqualField(expected, fromBuilder, "sfOwner"); + } + + { + auto const& expected = contractAccountValue; + + auto const fromSle = entryFromSle.getContractAccount(); + auto const fromBuilder = entryFromBuilder.getContractAccount(); + + expectEqualField(expected, fromSle, "sfContractAccount"); + expectEqualField(expected, fromBuilder, "sfContractAccount"); + } + + { + auto const& expected = contractHashValue; + + auto const fromSle = entryFromSle.getContractHash(); + auto const fromBuilder = entryFromBuilder.getContractHash(); + + expectEqualField(expected, fromSle, "sfContractHash"); + expectEqualField(expected, fromBuilder, "sfContractHash"); + } + + { + auto const& expected = instanceParameterValuesValue; + + auto const fromSleOpt = entryFromSle.getInstanceParameterValues(); + auto const fromBuilderOpt = entryFromBuilder.getInstanceParameterValues(); + + ASSERT_TRUE(fromSleOpt.has_value()); + ASSERT_TRUE(fromBuilderOpt.has_value()); + + expectEqualField(expected, *fromSleOpt, "sfInstanceParameterValues"); + expectEqualField(expected, *fromBuilderOpt, "sfInstanceParameterValues"); + } + + { + auto const& expected = uRIValue; + + auto const fromSleOpt = entryFromSle.getURI(); + auto const fromBuilderOpt = entryFromBuilder.getURI(); + + ASSERT_TRUE(fromSleOpt.has_value()); + ASSERT_TRUE(fromBuilderOpt.has_value()); + + expectEqualField(expected, *fromSleOpt, "sfURI"); + expectEqualField(expected, *fromBuilderOpt, "sfURI"); + } + + EXPECT_EQ(entryFromSle.getKey(), index); + EXPECT_EQ(entryFromBuilder.getKey(), index); +} + +// 3) Verify wrapper throws when constructed from wrong ledger entry type. +TEST(ContractTests, WrapperThrowsOnWrongEntryType) +{ + uint256 const index{3u}; + + // Build a valid ledger entry of a different type + // Ticket requires: Account, OwnerNode, TicketSequence, PreviousTxnID, PreviousTxnLgrSeq + // Check requires: Account, Destination, SendMax, Sequence, OwnerNode, DestinationNode, PreviousTxnID, PreviousTxnLgrSeq + TicketBuilder wrongBuilder{ + canonical_ACCOUNT(), + canonical_UINT64(), + canonical_UINT32(), + canonical_UINT256(), + canonical_UINT32()}; + auto wrongEntry = wrongBuilder.build(index); + + EXPECT_THROW(Contract{wrongEntry.getSle()}, std::runtime_error); +} + +// 4) Verify builder throws when constructed from wrong ledger entry type. +TEST(ContractTests, BuilderThrowsOnWrongEntryType) +{ + uint256 const index{4u}; + + // Build a valid ledger entry of a different type + TicketBuilder wrongBuilder{ + canonical_ACCOUNT(), + canonical_UINT64(), + canonical_UINT32(), + canonical_UINT256(), + canonical_UINT32()}; + auto wrongEntry = wrongBuilder.build(index); + + EXPECT_THROW(ContractBuilder{wrongEntry.getSle()}, std::runtime_error); +} + +// 5) Build with only required fields and verify optional fields return nullopt. +TEST(ContractTests, OptionalFieldsReturnNullopt) +{ + uint256 const index{3u}; + + auto const previousTxnIDValue = canonical_UINT256(); + auto const previousTxnLgrSeqValue = canonical_UINT32(); + auto const sequenceValue = canonical_UINT32(); + auto const ownerNodeValue = canonical_UINT64(); + auto const ownerValue = canonical_ACCOUNT(); + auto const contractAccountValue = canonical_ACCOUNT(); + auto const contractHashValue = canonical_UINT256(); + + ContractBuilder builder{ + previousTxnIDValue, + previousTxnLgrSeqValue, + sequenceValue, + ownerNodeValue, + ownerValue, + contractAccountValue, + contractHashValue + }; + + auto const entry = builder.build(index); + + // Verify optional fields are not present + EXPECT_FALSE(entry.hasInstanceParameterValues()); + EXPECT_FALSE(entry.getInstanceParameterValues().has_value()); + EXPECT_FALSE(entry.hasURI()); + EXPECT_FALSE(entry.getURI().has_value()); +} +} diff --git a/src/tests/libxrpl/protocol_autogen/transactions/ContractCallTests.cpp b/src/tests/libxrpl/protocol_autogen/transactions/ContractCallTests.cpp new file mode 100644 index 0000000000..f467bc5eba --- /dev/null +++ b/src/tests/libxrpl/protocol_autogen/transactions/ContractCallTests.cpp @@ -0,0 +1,231 @@ +// Auto-generated unit tests for transaction ContractCall + + +#include + +#include + +#include +#include +#include +#include +#include + +#include + +namespace xrpl::transactions { + +// 1 & 4) Set fields via builder setters, build, then read them back via +// wrapper getters. After build(), validate() should succeed. +TEST(TransactionsContractCallTests, BuilderSettersRoundTrip) +{ + // Generate a deterministic keypair for signing + auto const [publicKey, secretKey] = + generateKeyPair(KeyType::secp256k1, generateSeed("testContractCall")); + + // Common transaction fields + auto const accountValue = calcAccountID(publicKey); + std::uint32_t const sequenceValue = 1; + auto const feeValue = canonical_AMOUNT(); + + // Transaction-specific field values + auto const contractAccountValue = canonical_ACCOUNT(); + auto const functionNameValue = canonical_VL(); + auto const parametersValue = canonical_ARRAY(); + auto const computationAllowanceValue = canonical_UINT32(); + + ContractCallBuilder builder{ + accountValue, + contractAccountValue, + functionNameValue, + computationAllowanceValue, + sequenceValue, + feeValue + }; + + // Set optional fields + builder.setParameters(parametersValue); + + auto tx = builder.build(publicKey, secretKey); + + std::string reason; + EXPECT_TRUE(tx.validate(reason)) << reason; + + // Verify signing was applied + EXPECT_FALSE(tx.getSigningPubKey().empty()); + EXPECT_TRUE(tx.hasTxnSignature()); + + // Verify common fields + EXPECT_EQ(tx.getAccount(), accountValue); + EXPECT_EQ(tx.getSequence(), sequenceValue); + EXPECT_EQ(tx.getFee(), feeValue); + + // Verify required fields + { + auto const& expected = contractAccountValue; + auto const actual = tx.getContractAccount(); + expectEqualField(expected, actual, "sfContractAccount"); + } + + { + auto const& expected = functionNameValue; + auto const actual = tx.getFunctionName(); + expectEqualField(expected, actual, "sfFunctionName"); + } + + { + auto const& expected = computationAllowanceValue; + auto const actual = tx.getComputationAllowance(); + expectEqualField(expected, actual, "sfComputationAllowance"); + } + + // Verify optional fields + { + auto const& expected = parametersValue; + auto const actualOpt = tx.getParameters(); + ASSERT_TRUE(actualOpt.has_value()) << "Optional field sfParameters should be present"; + expectEqualField(expected, *actualOpt, "sfParameters"); + EXPECT_TRUE(tx.hasParameters()); + } + +} + +// 2 & 4) Start from an STTx, construct a builder from it, build a new wrapper, +// and verify all fields match. +TEST(TransactionsContractCallTests, BuilderFromStTxRoundTrip) +{ + // Generate a deterministic keypair for signing + auto const [publicKey, secretKey] = + generateKeyPair(KeyType::secp256k1, generateSeed("testContractCallFromTx")); + + // Common transaction fields + auto const accountValue = calcAccountID(publicKey); + std::uint32_t const sequenceValue = 2; + auto const feeValue = canonical_AMOUNT(); + + // Transaction-specific field values + auto const contractAccountValue = canonical_ACCOUNT(); + auto const functionNameValue = canonical_VL(); + auto const parametersValue = canonical_ARRAY(); + auto const computationAllowanceValue = canonical_UINT32(); + + // Build an initial transaction + ContractCallBuilder initialBuilder{ + accountValue, + contractAccountValue, + functionNameValue, + computationAllowanceValue, + sequenceValue, + feeValue + }; + + initialBuilder.setParameters(parametersValue); + + auto initialTx = initialBuilder.build(publicKey, secretKey); + + // Create builder from existing STTx + ContractCallBuilder builderFromTx{initialTx.getSTTx()}; + + auto rebuiltTx = builderFromTx.build(publicKey, secretKey); + + std::string reason; + EXPECT_TRUE(rebuiltTx.validate(reason)) << reason; + + // Verify common fields + EXPECT_EQ(rebuiltTx.getAccount(), accountValue); + EXPECT_EQ(rebuiltTx.getSequence(), sequenceValue); + EXPECT_EQ(rebuiltTx.getFee(), feeValue); + + // Verify required fields + { + auto const& expected = contractAccountValue; + auto const actual = rebuiltTx.getContractAccount(); + expectEqualField(expected, actual, "sfContractAccount"); + } + + { + auto const& expected = functionNameValue; + auto const actual = rebuiltTx.getFunctionName(); + expectEqualField(expected, actual, "sfFunctionName"); + } + + { + auto const& expected = computationAllowanceValue; + auto const actual = rebuiltTx.getComputationAllowance(); + expectEqualField(expected, actual, "sfComputationAllowance"); + } + + // Verify optional fields + { + auto const& expected = parametersValue; + auto const actualOpt = rebuiltTx.getParameters(); + ASSERT_TRUE(actualOpt.has_value()) << "Optional field sfParameters should be present"; + expectEqualField(expected, *actualOpt, "sfParameters"); + } + +} + +// 3) Verify wrapper throws when constructed from wrong transaction type. +TEST(TransactionsContractCallTests, WrapperThrowsOnWrongTxType) +{ + // Build a valid transaction of a different type + auto const [pk, sk] = + generateKeyPair(KeyType::secp256k1, generateSeed("testWrongType")); + auto const account = calcAccountID(pk); + + AccountSetBuilder wrongBuilder{account, 1, canonical_AMOUNT()}; + auto wrongTx = wrongBuilder.build(pk, sk); + + EXPECT_THROW(ContractCall{wrongTx.getSTTx()}, std::runtime_error); +} + +// 4) Verify builder throws when constructed from wrong transaction type. +TEST(TransactionsContractCallTests, BuilderThrowsOnWrongTxType) +{ + // Build a valid transaction of a different type + auto const [pk, sk] = + generateKeyPair(KeyType::secp256k1, generateSeed("testWrongTypeBuilder")); + auto const account = calcAccountID(pk); + + AccountSetBuilder wrongBuilder{account, 1, canonical_AMOUNT()}; + auto wrongTx = wrongBuilder.build(pk, sk); + + EXPECT_THROW(ContractCallBuilder{wrongTx.getSTTx()}, std::runtime_error); +} + +// 5) Build with only required fields and verify optional fields return nullopt. +TEST(TransactionsContractCallTests, OptionalFieldsReturnNullopt) +{ + // Generate a deterministic keypair for signing + auto const [publicKey, secretKey] = + generateKeyPair(KeyType::secp256k1, generateSeed("testContractCallNullopt")); + + // Common transaction fields + auto const accountValue = calcAccountID(publicKey); + std::uint32_t const sequenceValue = 3; + auto const feeValue = canonical_AMOUNT(); + + // Transaction-specific required field values + auto const contractAccountValue = canonical_ACCOUNT(); + auto const functionNameValue = canonical_VL(); + auto const computationAllowanceValue = canonical_UINT32(); + + ContractCallBuilder builder{ + accountValue, + contractAccountValue, + functionNameValue, + computationAllowanceValue, + sequenceValue, + feeValue + }; + + // Do NOT set optional fields + + auto tx = builder.build(publicKey, secretKey); + + // Verify optional fields are not present + EXPECT_FALSE(tx.hasParameters()); + EXPECT_FALSE(tx.getParameters().has_value()); +} + +} diff --git a/src/tests/libxrpl/protocol_autogen/transactions/ContractClawbackTests.cpp b/src/tests/libxrpl/protocol_autogen/transactions/ContractClawbackTests.cpp new file mode 100644 index 0000000000..0200657391 --- /dev/null +++ b/src/tests/libxrpl/protocol_autogen/transactions/ContractClawbackTests.cpp @@ -0,0 +1,195 @@ +// Auto-generated unit tests for transaction ContractClawback + + +#include + +#include + +#include +#include +#include +#include +#include + +#include + +namespace xrpl::transactions { + +// 1 & 4) Set fields via builder setters, build, then read them back via +// wrapper getters. After build(), validate() should succeed. +TEST(TransactionsContractClawbackTests, BuilderSettersRoundTrip) +{ + // Generate a deterministic keypair for signing + auto const [publicKey, secretKey] = + generateKeyPair(KeyType::secp256k1, generateSeed("testContractClawback")); + + // Common transaction fields + auto const accountValue = calcAccountID(publicKey); + std::uint32_t const sequenceValue = 1; + auto const feeValue = canonical_AMOUNT(); + + // Transaction-specific field values + auto const contractAccountValue = canonical_ACCOUNT(); + auto const amountValue = canonical_AMOUNT(); + + ContractClawbackBuilder builder{ + accountValue, + amountValue, + sequenceValue, + feeValue + }; + + // Set optional fields + builder.setContractAccount(contractAccountValue); + + auto tx = builder.build(publicKey, secretKey); + + std::string reason; + EXPECT_TRUE(tx.validate(reason)) << reason; + + // Verify signing was applied + EXPECT_FALSE(tx.getSigningPubKey().empty()); + EXPECT_TRUE(tx.hasTxnSignature()); + + // Verify common fields + EXPECT_EQ(tx.getAccount(), accountValue); + EXPECT_EQ(tx.getSequence(), sequenceValue); + EXPECT_EQ(tx.getFee(), feeValue); + + // Verify required fields + { + auto const& expected = amountValue; + auto const actual = tx.getAmount(); + expectEqualField(expected, actual, "sfAmount"); + } + + // Verify optional fields + { + auto const& expected = contractAccountValue; + auto const actualOpt = tx.getContractAccount(); + ASSERT_TRUE(actualOpt.has_value()) << "Optional field sfContractAccount should be present"; + expectEqualField(expected, *actualOpt, "sfContractAccount"); + EXPECT_TRUE(tx.hasContractAccount()); + } + +} + +// 2 & 4) Start from an STTx, construct a builder from it, build a new wrapper, +// and verify all fields match. +TEST(TransactionsContractClawbackTests, BuilderFromStTxRoundTrip) +{ + // Generate a deterministic keypair for signing + auto const [publicKey, secretKey] = + generateKeyPair(KeyType::secp256k1, generateSeed("testContractClawbackFromTx")); + + // Common transaction fields + auto const accountValue = calcAccountID(publicKey); + std::uint32_t const sequenceValue = 2; + auto const feeValue = canonical_AMOUNT(); + + // Transaction-specific field values + auto const contractAccountValue = canonical_ACCOUNT(); + auto const amountValue = canonical_AMOUNT(); + + // Build an initial transaction + ContractClawbackBuilder initialBuilder{ + accountValue, + amountValue, + sequenceValue, + feeValue + }; + + initialBuilder.setContractAccount(contractAccountValue); + + auto initialTx = initialBuilder.build(publicKey, secretKey); + + // Create builder from existing STTx + ContractClawbackBuilder builderFromTx{initialTx.getSTTx()}; + + auto rebuiltTx = builderFromTx.build(publicKey, secretKey); + + std::string reason; + EXPECT_TRUE(rebuiltTx.validate(reason)) << reason; + + // Verify common fields + EXPECT_EQ(rebuiltTx.getAccount(), accountValue); + EXPECT_EQ(rebuiltTx.getSequence(), sequenceValue); + EXPECT_EQ(rebuiltTx.getFee(), feeValue); + + // Verify required fields + { + auto const& expected = amountValue; + auto const actual = rebuiltTx.getAmount(); + expectEqualField(expected, actual, "sfAmount"); + } + + // Verify optional fields + { + auto const& expected = contractAccountValue; + auto const actualOpt = rebuiltTx.getContractAccount(); + ASSERT_TRUE(actualOpt.has_value()) << "Optional field sfContractAccount should be present"; + expectEqualField(expected, *actualOpt, "sfContractAccount"); + } + +} + +// 3) Verify wrapper throws when constructed from wrong transaction type. +TEST(TransactionsContractClawbackTests, WrapperThrowsOnWrongTxType) +{ + // Build a valid transaction of a different type + auto const [pk, sk] = + generateKeyPair(KeyType::secp256k1, generateSeed("testWrongType")); + auto const account = calcAccountID(pk); + + AccountSetBuilder wrongBuilder{account, 1, canonical_AMOUNT()}; + auto wrongTx = wrongBuilder.build(pk, sk); + + EXPECT_THROW(ContractClawback{wrongTx.getSTTx()}, std::runtime_error); +} + +// 4) Verify builder throws when constructed from wrong transaction type. +TEST(TransactionsContractClawbackTests, BuilderThrowsOnWrongTxType) +{ + // Build a valid transaction of a different type + auto const [pk, sk] = + generateKeyPair(KeyType::secp256k1, generateSeed("testWrongTypeBuilder")); + auto const account = calcAccountID(pk); + + AccountSetBuilder wrongBuilder{account, 1, canonical_AMOUNT()}; + auto wrongTx = wrongBuilder.build(pk, sk); + + EXPECT_THROW(ContractClawbackBuilder{wrongTx.getSTTx()}, std::runtime_error); +} + +// 5) Build with only required fields and verify optional fields return nullopt. +TEST(TransactionsContractClawbackTests, OptionalFieldsReturnNullopt) +{ + // Generate a deterministic keypair for signing + auto const [publicKey, secretKey] = + generateKeyPair(KeyType::secp256k1, generateSeed("testContractClawbackNullopt")); + + // Common transaction fields + auto const accountValue = calcAccountID(publicKey); + std::uint32_t const sequenceValue = 3; + auto const feeValue = canonical_AMOUNT(); + + // Transaction-specific required field values + auto const amountValue = canonical_AMOUNT(); + + ContractClawbackBuilder builder{ + accountValue, + amountValue, + sequenceValue, + feeValue + }; + + // Do NOT set optional fields + + auto tx = builder.build(publicKey, secretKey); + + // Verify optional fields are not present + EXPECT_FALSE(tx.hasContractAccount()); + EXPECT_FALSE(tx.getContractAccount().has_value()); +} + +} diff --git a/src/tests/libxrpl/protocol_autogen/transactions/ContractCreateTests.cpp b/src/tests/libxrpl/protocol_autogen/transactions/ContractCreateTests.cpp new file mode 100644 index 0000000000..29dd15c6b7 --- /dev/null +++ b/src/tests/libxrpl/protocol_autogen/transactions/ContractCreateTests.cpp @@ -0,0 +1,282 @@ +// Auto-generated unit tests for transaction ContractCreate + + +#include + +#include + +#include +#include +#include +#include +#include + +#include + +namespace xrpl::transactions { + +// 1 & 4) Set fields via builder setters, build, then read them back via +// wrapper getters. After build(), validate() should succeed. +TEST(TransactionsContractCreateTests, BuilderSettersRoundTrip) +{ + // Generate a deterministic keypair for signing + auto const [publicKey, secretKey] = + generateKeyPair(KeyType::secp256k1, generateSeed("testContractCreate")); + + // Common transaction fields + auto const accountValue = calcAccountID(publicKey); + std::uint32_t const sequenceValue = 1; + auto const feeValue = canonical_AMOUNT(); + + // Transaction-specific field values + auto const contractCodeValue = canonical_VL(); + auto const contractHashValue = canonical_UINT256(); + auto const functionsValue = canonical_ARRAY(); + auto const instanceParametersValue = canonical_ARRAY(); + auto const instanceParameterValuesValue = canonical_ARRAY(); + auto const uRIValue = canonical_VL(); + + ContractCreateBuilder builder{ + accountValue, + sequenceValue, + feeValue + }; + + // Set optional fields + builder.setContractCode(contractCodeValue); + builder.setContractHash(contractHashValue); + builder.setFunctions(functionsValue); + builder.setInstanceParameters(instanceParametersValue); + builder.setInstanceParameterValues(instanceParameterValuesValue); + builder.setURI(uRIValue); + + auto tx = builder.build(publicKey, secretKey); + + std::string reason; + EXPECT_TRUE(tx.validate(reason)) << reason; + + // Verify signing was applied + EXPECT_FALSE(tx.getSigningPubKey().empty()); + EXPECT_TRUE(tx.hasTxnSignature()); + + // Verify common fields + EXPECT_EQ(tx.getAccount(), accountValue); + EXPECT_EQ(tx.getSequence(), sequenceValue); + EXPECT_EQ(tx.getFee(), feeValue); + + // Verify required fields + // Verify optional fields + { + auto const& expected = contractCodeValue; + auto const actualOpt = tx.getContractCode(); + ASSERT_TRUE(actualOpt.has_value()) << "Optional field sfContractCode should be present"; + expectEqualField(expected, *actualOpt, "sfContractCode"); + EXPECT_TRUE(tx.hasContractCode()); + } + + { + auto const& expected = contractHashValue; + auto const actualOpt = tx.getContractHash(); + ASSERT_TRUE(actualOpt.has_value()) << "Optional field sfContractHash should be present"; + expectEqualField(expected, *actualOpt, "sfContractHash"); + EXPECT_TRUE(tx.hasContractHash()); + } + + { + auto const& expected = functionsValue; + auto const actualOpt = tx.getFunctions(); + ASSERT_TRUE(actualOpt.has_value()) << "Optional field sfFunctions should be present"; + expectEqualField(expected, *actualOpt, "sfFunctions"); + EXPECT_TRUE(tx.hasFunctions()); + } + + { + auto const& expected = instanceParametersValue; + auto const actualOpt = tx.getInstanceParameters(); + ASSERT_TRUE(actualOpt.has_value()) << "Optional field sfInstanceParameters should be present"; + expectEqualField(expected, *actualOpt, "sfInstanceParameters"); + EXPECT_TRUE(tx.hasInstanceParameters()); + } + + { + auto const& expected = instanceParameterValuesValue; + auto const actualOpt = tx.getInstanceParameterValues(); + ASSERT_TRUE(actualOpt.has_value()) << "Optional field sfInstanceParameterValues should be present"; + expectEqualField(expected, *actualOpt, "sfInstanceParameterValues"); + EXPECT_TRUE(tx.hasInstanceParameterValues()); + } + + { + auto const& expected = uRIValue; + auto const actualOpt = tx.getURI(); + ASSERT_TRUE(actualOpt.has_value()) << "Optional field sfURI should be present"; + expectEqualField(expected, *actualOpt, "sfURI"); + EXPECT_TRUE(tx.hasURI()); + } + +} + +// 2 & 4) Start from an STTx, construct a builder from it, build a new wrapper, +// and verify all fields match. +TEST(TransactionsContractCreateTests, BuilderFromStTxRoundTrip) +{ + // Generate a deterministic keypair for signing + auto const [publicKey, secretKey] = + generateKeyPair(KeyType::secp256k1, generateSeed("testContractCreateFromTx")); + + // Common transaction fields + auto const accountValue = calcAccountID(publicKey); + std::uint32_t const sequenceValue = 2; + auto const feeValue = canonical_AMOUNT(); + + // Transaction-specific field values + auto const contractCodeValue = canonical_VL(); + auto const contractHashValue = canonical_UINT256(); + auto const functionsValue = canonical_ARRAY(); + auto const instanceParametersValue = canonical_ARRAY(); + auto const instanceParameterValuesValue = canonical_ARRAY(); + auto const uRIValue = canonical_VL(); + + // Build an initial transaction + ContractCreateBuilder initialBuilder{ + accountValue, + sequenceValue, + feeValue + }; + + initialBuilder.setContractCode(contractCodeValue); + initialBuilder.setContractHash(contractHashValue); + initialBuilder.setFunctions(functionsValue); + initialBuilder.setInstanceParameters(instanceParametersValue); + initialBuilder.setInstanceParameterValues(instanceParameterValuesValue); + initialBuilder.setURI(uRIValue); + + auto initialTx = initialBuilder.build(publicKey, secretKey); + + // Create builder from existing STTx + ContractCreateBuilder builderFromTx{initialTx.getSTTx()}; + + auto rebuiltTx = builderFromTx.build(publicKey, secretKey); + + std::string reason; + EXPECT_TRUE(rebuiltTx.validate(reason)) << reason; + + // Verify common fields + EXPECT_EQ(rebuiltTx.getAccount(), accountValue); + EXPECT_EQ(rebuiltTx.getSequence(), sequenceValue); + EXPECT_EQ(rebuiltTx.getFee(), feeValue); + + // Verify required fields + // Verify optional fields + { + auto const& expected = contractCodeValue; + auto const actualOpt = rebuiltTx.getContractCode(); + ASSERT_TRUE(actualOpt.has_value()) << "Optional field sfContractCode should be present"; + expectEqualField(expected, *actualOpt, "sfContractCode"); + } + + { + auto const& expected = contractHashValue; + auto const actualOpt = rebuiltTx.getContractHash(); + ASSERT_TRUE(actualOpt.has_value()) << "Optional field sfContractHash should be present"; + expectEqualField(expected, *actualOpt, "sfContractHash"); + } + + { + auto const& expected = functionsValue; + auto const actualOpt = rebuiltTx.getFunctions(); + ASSERT_TRUE(actualOpt.has_value()) << "Optional field sfFunctions should be present"; + expectEqualField(expected, *actualOpt, "sfFunctions"); + } + + { + auto const& expected = instanceParametersValue; + auto const actualOpt = rebuiltTx.getInstanceParameters(); + ASSERT_TRUE(actualOpt.has_value()) << "Optional field sfInstanceParameters should be present"; + expectEqualField(expected, *actualOpt, "sfInstanceParameters"); + } + + { + auto const& expected = instanceParameterValuesValue; + auto const actualOpt = rebuiltTx.getInstanceParameterValues(); + ASSERT_TRUE(actualOpt.has_value()) << "Optional field sfInstanceParameterValues should be present"; + expectEqualField(expected, *actualOpt, "sfInstanceParameterValues"); + } + + { + auto const& expected = uRIValue; + auto const actualOpt = rebuiltTx.getURI(); + ASSERT_TRUE(actualOpt.has_value()) << "Optional field sfURI should be present"; + expectEqualField(expected, *actualOpt, "sfURI"); + } + +} + +// 3) Verify wrapper throws when constructed from wrong transaction type. +TEST(TransactionsContractCreateTests, WrapperThrowsOnWrongTxType) +{ + // Build a valid transaction of a different type + auto const [pk, sk] = + generateKeyPair(KeyType::secp256k1, generateSeed("testWrongType")); + auto const account = calcAccountID(pk); + + AccountSetBuilder wrongBuilder{account, 1, canonical_AMOUNT()}; + auto wrongTx = wrongBuilder.build(pk, sk); + + EXPECT_THROW(ContractCreate{wrongTx.getSTTx()}, std::runtime_error); +} + +// 4) Verify builder throws when constructed from wrong transaction type. +TEST(TransactionsContractCreateTests, BuilderThrowsOnWrongTxType) +{ + // Build a valid transaction of a different type + auto const [pk, sk] = + generateKeyPair(KeyType::secp256k1, generateSeed("testWrongTypeBuilder")); + auto const account = calcAccountID(pk); + + AccountSetBuilder wrongBuilder{account, 1, canonical_AMOUNT()}; + auto wrongTx = wrongBuilder.build(pk, sk); + + EXPECT_THROW(ContractCreateBuilder{wrongTx.getSTTx()}, std::runtime_error); +} + +// 5) Build with only required fields and verify optional fields return nullopt. +TEST(TransactionsContractCreateTests, OptionalFieldsReturnNullopt) +{ + // Generate a deterministic keypair for signing + auto const [publicKey, secretKey] = + generateKeyPair(KeyType::secp256k1, generateSeed("testContractCreateNullopt")); + + // Common transaction fields + auto const accountValue = calcAccountID(publicKey); + std::uint32_t const sequenceValue = 3; + auto const feeValue = canonical_AMOUNT(); + + // Transaction-specific required field values + + ContractCreateBuilder builder{ + accountValue, + sequenceValue, + feeValue + }; + + // Do NOT set optional fields + + auto tx = builder.build(publicKey, secretKey); + + // Verify optional fields are not present + EXPECT_FALSE(tx.hasContractCode()); + EXPECT_FALSE(tx.getContractCode().has_value()); + EXPECT_FALSE(tx.hasContractHash()); + EXPECT_FALSE(tx.getContractHash().has_value()); + EXPECT_FALSE(tx.hasFunctions()); + EXPECT_FALSE(tx.getFunctions().has_value()); + EXPECT_FALSE(tx.hasInstanceParameters()); + EXPECT_FALSE(tx.getInstanceParameters().has_value()); + EXPECT_FALSE(tx.hasInstanceParameterValues()); + EXPECT_FALSE(tx.getInstanceParameterValues().has_value()); + EXPECT_FALSE(tx.hasURI()); + EXPECT_FALSE(tx.getURI().has_value()); +} + +} diff --git a/src/tests/libxrpl/protocol_autogen/transactions/ContractDeleteTests.cpp b/src/tests/libxrpl/protocol_autogen/transactions/ContractDeleteTests.cpp new file mode 100644 index 0000000000..6140cf0ec8 --- /dev/null +++ b/src/tests/libxrpl/protocol_autogen/transactions/ContractDeleteTests.cpp @@ -0,0 +1,146 @@ +// Auto-generated unit tests for transaction ContractDelete + + +#include + +#include + +#include +#include +#include +#include +#include + +#include + +namespace xrpl::transactions { + +// 1 & 4) Set fields via builder setters, build, then read them back via +// wrapper getters. After build(), validate() should succeed. +TEST(TransactionsContractDeleteTests, BuilderSettersRoundTrip) +{ + // Generate a deterministic keypair for signing + auto const [publicKey, secretKey] = + generateKeyPair(KeyType::secp256k1, generateSeed("testContractDelete")); + + // Common transaction fields + auto const accountValue = calcAccountID(publicKey); + std::uint32_t const sequenceValue = 1; + auto const feeValue = canonical_AMOUNT(); + + // Transaction-specific field values + auto const contractAccountValue = canonical_ACCOUNT(); + + ContractDeleteBuilder builder{ + accountValue, + contractAccountValue, + sequenceValue, + feeValue + }; + + // Set optional fields + + auto tx = builder.build(publicKey, secretKey); + + std::string reason; + EXPECT_TRUE(tx.validate(reason)) << reason; + + // Verify signing was applied + EXPECT_FALSE(tx.getSigningPubKey().empty()); + EXPECT_TRUE(tx.hasTxnSignature()); + + // Verify common fields + EXPECT_EQ(tx.getAccount(), accountValue); + EXPECT_EQ(tx.getSequence(), sequenceValue); + EXPECT_EQ(tx.getFee(), feeValue); + + // Verify required fields + { + auto const& expected = contractAccountValue; + auto const actual = tx.getContractAccount(); + expectEqualField(expected, actual, "sfContractAccount"); + } + + // Verify optional fields +} + +// 2 & 4) Start from an STTx, construct a builder from it, build a new wrapper, +// and verify all fields match. +TEST(TransactionsContractDeleteTests, BuilderFromStTxRoundTrip) +{ + // Generate a deterministic keypair for signing + auto const [publicKey, secretKey] = + generateKeyPair(KeyType::secp256k1, generateSeed("testContractDeleteFromTx")); + + // Common transaction fields + auto const accountValue = calcAccountID(publicKey); + std::uint32_t const sequenceValue = 2; + auto const feeValue = canonical_AMOUNT(); + + // Transaction-specific field values + auto const contractAccountValue = canonical_ACCOUNT(); + + // Build an initial transaction + ContractDeleteBuilder initialBuilder{ + accountValue, + contractAccountValue, + sequenceValue, + feeValue + }; + + + auto initialTx = initialBuilder.build(publicKey, secretKey); + + // Create builder from existing STTx + ContractDeleteBuilder builderFromTx{initialTx.getSTTx()}; + + auto rebuiltTx = builderFromTx.build(publicKey, secretKey); + + std::string reason; + EXPECT_TRUE(rebuiltTx.validate(reason)) << reason; + + // Verify common fields + EXPECT_EQ(rebuiltTx.getAccount(), accountValue); + EXPECT_EQ(rebuiltTx.getSequence(), sequenceValue); + EXPECT_EQ(rebuiltTx.getFee(), feeValue); + + // Verify required fields + { + auto const& expected = contractAccountValue; + auto const actual = rebuiltTx.getContractAccount(); + expectEqualField(expected, actual, "sfContractAccount"); + } + + // Verify optional fields +} + +// 3) Verify wrapper throws when constructed from wrong transaction type. +TEST(TransactionsContractDeleteTests, WrapperThrowsOnWrongTxType) +{ + // Build a valid transaction of a different type + auto const [pk, sk] = + generateKeyPair(KeyType::secp256k1, generateSeed("testWrongType")); + auto const account = calcAccountID(pk); + + AccountSetBuilder wrongBuilder{account, 1, canonical_AMOUNT()}; + auto wrongTx = wrongBuilder.build(pk, sk); + + EXPECT_THROW(ContractDelete{wrongTx.getSTTx()}, std::runtime_error); +} + +// 4) Verify builder throws when constructed from wrong transaction type. +TEST(TransactionsContractDeleteTests, BuilderThrowsOnWrongTxType) +{ + // Build a valid transaction of a different type + auto const [pk, sk] = + generateKeyPair(KeyType::secp256k1, generateSeed("testWrongTypeBuilder")); + auto const account = calcAccountID(pk); + + AccountSetBuilder wrongBuilder{account, 1, canonical_AMOUNT()}; + auto wrongTx = wrongBuilder.build(pk, sk); + + EXPECT_THROW(ContractDeleteBuilder{wrongTx.getSTTx()}, std::runtime_error); +} + + +} diff --git a/src/tests/libxrpl/protocol_autogen/transactions/ContractModifyTests.cpp b/src/tests/libxrpl/protocol_autogen/transactions/ContractModifyTests.cpp new file mode 100644 index 0000000000..f527363e7e --- /dev/null +++ b/src/tests/libxrpl/protocol_autogen/transactions/ContractModifyTests.cpp @@ -0,0 +1,324 @@ +// Auto-generated unit tests for transaction ContractModify + + +#include + +#include + +#include +#include +#include +#include +#include + +#include + +namespace xrpl::transactions { + +// 1 & 4) Set fields via builder setters, build, then read them back via +// wrapper getters. After build(), validate() should succeed. +TEST(TransactionsContractModifyTests, BuilderSettersRoundTrip) +{ + // Generate a deterministic keypair for signing + auto const [publicKey, secretKey] = + generateKeyPair(KeyType::secp256k1, generateSeed("testContractModify")); + + // Common transaction fields + auto const accountValue = calcAccountID(publicKey); + std::uint32_t const sequenceValue = 1; + auto const feeValue = canonical_AMOUNT(); + + // Transaction-specific field values + auto const contractAccountValue = canonical_ACCOUNT(); + auto const ownerValue = canonical_ACCOUNT(); + auto const contractCodeValue = canonical_VL(); + auto const contractHashValue = canonical_UINT256(); + auto const functionsValue = canonical_ARRAY(); + auto const instanceParametersValue = canonical_ARRAY(); + auto const instanceParameterValuesValue = canonical_ARRAY(); + auto const uRIValue = canonical_VL(); + + ContractModifyBuilder builder{ + accountValue, + sequenceValue, + feeValue + }; + + // Set optional fields + builder.setContractAccount(contractAccountValue); + builder.setOwner(ownerValue); + builder.setContractCode(contractCodeValue); + builder.setContractHash(contractHashValue); + builder.setFunctions(functionsValue); + builder.setInstanceParameters(instanceParametersValue); + builder.setInstanceParameterValues(instanceParameterValuesValue); + builder.setURI(uRIValue); + + auto tx = builder.build(publicKey, secretKey); + + std::string reason; + EXPECT_TRUE(tx.validate(reason)) << reason; + + // Verify signing was applied + EXPECT_FALSE(tx.getSigningPubKey().empty()); + EXPECT_TRUE(tx.hasTxnSignature()); + + // Verify common fields + EXPECT_EQ(tx.getAccount(), accountValue); + EXPECT_EQ(tx.getSequence(), sequenceValue); + EXPECT_EQ(tx.getFee(), feeValue); + + // Verify required fields + // Verify optional fields + { + auto const& expected = contractAccountValue; + auto const actualOpt = tx.getContractAccount(); + ASSERT_TRUE(actualOpt.has_value()) << "Optional field sfContractAccount should be present"; + expectEqualField(expected, *actualOpt, "sfContractAccount"); + EXPECT_TRUE(tx.hasContractAccount()); + } + + { + auto const& expected = ownerValue; + auto const actualOpt = tx.getOwner(); + ASSERT_TRUE(actualOpt.has_value()) << "Optional field sfOwner should be present"; + expectEqualField(expected, *actualOpt, "sfOwner"); + EXPECT_TRUE(tx.hasOwner()); + } + + { + auto const& expected = contractCodeValue; + auto const actualOpt = tx.getContractCode(); + ASSERT_TRUE(actualOpt.has_value()) << "Optional field sfContractCode should be present"; + expectEqualField(expected, *actualOpt, "sfContractCode"); + EXPECT_TRUE(tx.hasContractCode()); + } + + { + auto const& expected = contractHashValue; + auto const actualOpt = tx.getContractHash(); + ASSERT_TRUE(actualOpt.has_value()) << "Optional field sfContractHash should be present"; + expectEqualField(expected, *actualOpt, "sfContractHash"); + EXPECT_TRUE(tx.hasContractHash()); + } + + { + auto const& expected = functionsValue; + auto const actualOpt = tx.getFunctions(); + ASSERT_TRUE(actualOpt.has_value()) << "Optional field sfFunctions should be present"; + expectEqualField(expected, *actualOpt, "sfFunctions"); + EXPECT_TRUE(tx.hasFunctions()); + } + + { + auto const& expected = instanceParametersValue; + auto const actualOpt = tx.getInstanceParameters(); + ASSERT_TRUE(actualOpt.has_value()) << "Optional field sfInstanceParameters should be present"; + expectEqualField(expected, *actualOpt, "sfInstanceParameters"); + EXPECT_TRUE(tx.hasInstanceParameters()); + } + + { + auto const& expected = instanceParameterValuesValue; + auto const actualOpt = tx.getInstanceParameterValues(); + ASSERT_TRUE(actualOpt.has_value()) << "Optional field sfInstanceParameterValues should be present"; + expectEqualField(expected, *actualOpt, "sfInstanceParameterValues"); + EXPECT_TRUE(tx.hasInstanceParameterValues()); + } + + { + auto const& expected = uRIValue; + auto const actualOpt = tx.getURI(); + ASSERT_TRUE(actualOpt.has_value()) << "Optional field sfURI should be present"; + expectEqualField(expected, *actualOpt, "sfURI"); + EXPECT_TRUE(tx.hasURI()); + } + +} + +// 2 & 4) Start from an STTx, construct a builder from it, build a new wrapper, +// and verify all fields match. +TEST(TransactionsContractModifyTests, BuilderFromStTxRoundTrip) +{ + // Generate a deterministic keypair for signing + auto const [publicKey, secretKey] = + generateKeyPair(KeyType::secp256k1, generateSeed("testContractModifyFromTx")); + + // Common transaction fields + auto const accountValue = calcAccountID(publicKey); + std::uint32_t const sequenceValue = 2; + auto const feeValue = canonical_AMOUNT(); + + // Transaction-specific field values + auto const contractAccountValue = canonical_ACCOUNT(); + auto const ownerValue = canonical_ACCOUNT(); + auto const contractCodeValue = canonical_VL(); + auto const contractHashValue = canonical_UINT256(); + auto const functionsValue = canonical_ARRAY(); + auto const instanceParametersValue = canonical_ARRAY(); + auto const instanceParameterValuesValue = canonical_ARRAY(); + auto const uRIValue = canonical_VL(); + + // Build an initial transaction + ContractModifyBuilder initialBuilder{ + accountValue, + sequenceValue, + feeValue + }; + + initialBuilder.setContractAccount(contractAccountValue); + initialBuilder.setOwner(ownerValue); + initialBuilder.setContractCode(contractCodeValue); + initialBuilder.setContractHash(contractHashValue); + initialBuilder.setFunctions(functionsValue); + initialBuilder.setInstanceParameters(instanceParametersValue); + initialBuilder.setInstanceParameterValues(instanceParameterValuesValue); + initialBuilder.setURI(uRIValue); + + auto initialTx = initialBuilder.build(publicKey, secretKey); + + // Create builder from existing STTx + ContractModifyBuilder builderFromTx{initialTx.getSTTx()}; + + auto rebuiltTx = builderFromTx.build(publicKey, secretKey); + + std::string reason; + EXPECT_TRUE(rebuiltTx.validate(reason)) << reason; + + // Verify common fields + EXPECT_EQ(rebuiltTx.getAccount(), accountValue); + EXPECT_EQ(rebuiltTx.getSequence(), sequenceValue); + EXPECT_EQ(rebuiltTx.getFee(), feeValue); + + // Verify required fields + // Verify optional fields + { + auto const& expected = contractAccountValue; + auto const actualOpt = rebuiltTx.getContractAccount(); + ASSERT_TRUE(actualOpt.has_value()) << "Optional field sfContractAccount should be present"; + expectEqualField(expected, *actualOpt, "sfContractAccount"); + } + + { + auto const& expected = ownerValue; + auto const actualOpt = rebuiltTx.getOwner(); + ASSERT_TRUE(actualOpt.has_value()) << "Optional field sfOwner should be present"; + expectEqualField(expected, *actualOpt, "sfOwner"); + } + + { + auto const& expected = contractCodeValue; + auto const actualOpt = rebuiltTx.getContractCode(); + ASSERT_TRUE(actualOpt.has_value()) << "Optional field sfContractCode should be present"; + expectEqualField(expected, *actualOpt, "sfContractCode"); + } + + { + auto const& expected = contractHashValue; + auto const actualOpt = rebuiltTx.getContractHash(); + ASSERT_TRUE(actualOpt.has_value()) << "Optional field sfContractHash should be present"; + expectEqualField(expected, *actualOpt, "sfContractHash"); + } + + { + auto const& expected = functionsValue; + auto const actualOpt = rebuiltTx.getFunctions(); + ASSERT_TRUE(actualOpt.has_value()) << "Optional field sfFunctions should be present"; + expectEqualField(expected, *actualOpt, "sfFunctions"); + } + + { + auto const& expected = instanceParametersValue; + auto const actualOpt = rebuiltTx.getInstanceParameters(); + ASSERT_TRUE(actualOpt.has_value()) << "Optional field sfInstanceParameters should be present"; + expectEqualField(expected, *actualOpt, "sfInstanceParameters"); + } + + { + auto const& expected = instanceParameterValuesValue; + auto const actualOpt = rebuiltTx.getInstanceParameterValues(); + ASSERT_TRUE(actualOpt.has_value()) << "Optional field sfInstanceParameterValues should be present"; + expectEqualField(expected, *actualOpt, "sfInstanceParameterValues"); + } + + { + auto const& expected = uRIValue; + auto const actualOpt = rebuiltTx.getURI(); + ASSERT_TRUE(actualOpt.has_value()) << "Optional field sfURI should be present"; + expectEqualField(expected, *actualOpt, "sfURI"); + } + +} + +// 3) Verify wrapper throws when constructed from wrong transaction type. +TEST(TransactionsContractModifyTests, WrapperThrowsOnWrongTxType) +{ + // Build a valid transaction of a different type + auto const [pk, sk] = + generateKeyPair(KeyType::secp256k1, generateSeed("testWrongType")); + auto const account = calcAccountID(pk); + + AccountSetBuilder wrongBuilder{account, 1, canonical_AMOUNT()}; + auto wrongTx = wrongBuilder.build(pk, sk); + + EXPECT_THROW(ContractModify{wrongTx.getSTTx()}, std::runtime_error); +} + +// 4) Verify builder throws when constructed from wrong transaction type. +TEST(TransactionsContractModifyTests, BuilderThrowsOnWrongTxType) +{ + // Build a valid transaction of a different type + auto const [pk, sk] = + generateKeyPair(KeyType::secp256k1, generateSeed("testWrongTypeBuilder")); + auto const account = calcAccountID(pk); + + AccountSetBuilder wrongBuilder{account, 1, canonical_AMOUNT()}; + auto wrongTx = wrongBuilder.build(pk, sk); + + EXPECT_THROW(ContractModifyBuilder{wrongTx.getSTTx()}, std::runtime_error); +} + +// 5) Build with only required fields and verify optional fields return nullopt. +TEST(TransactionsContractModifyTests, OptionalFieldsReturnNullopt) +{ + // Generate a deterministic keypair for signing + auto const [publicKey, secretKey] = + generateKeyPair(KeyType::secp256k1, generateSeed("testContractModifyNullopt")); + + // Common transaction fields + auto const accountValue = calcAccountID(publicKey); + std::uint32_t const sequenceValue = 3; + auto const feeValue = canonical_AMOUNT(); + + // Transaction-specific required field values + + ContractModifyBuilder builder{ + accountValue, + sequenceValue, + feeValue + }; + + // Do NOT set optional fields + + auto tx = builder.build(publicKey, secretKey); + + // Verify optional fields are not present + EXPECT_FALSE(tx.hasContractAccount()); + EXPECT_FALSE(tx.getContractAccount().has_value()); + EXPECT_FALSE(tx.hasOwner()); + EXPECT_FALSE(tx.getOwner().has_value()); + EXPECT_FALSE(tx.hasContractCode()); + EXPECT_FALSE(tx.getContractCode().has_value()); + EXPECT_FALSE(tx.hasContractHash()); + EXPECT_FALSE(tx.getContractHash().has_value()); + EXPECT_FALSE(tx.hasFunctions()); + EXPECT_FALSE(tx.getFunctions().has_value()); + EXPECT_FALSE(tx.hasInstanceParameters()); + EXPECT_FALSE(tx.getInstanceParameters().has_value()); + EXPECT_FALSE(tx.hasInstanceParameterValues()); + EXPECT_FALSE(tx.getInstanceParameterValues().has_value()); + EXPECT_FALSE(tx.hasURI()); + EXPECT_FALSE(tx.getURI().has_value()); +} + +} diff --git a/src/tests/libxrpl/protocol_autogen/transactions/ContractUserDeleteTests.cpp b/src/tests/libxrpl/protocol_autogen/transactions/ContractUserDeleteTests.cpp new file mode 100644 index 0000000000..896f0157a2 --- /dev/null +++ b/src/tests/libxrpl/protocol_autogen/transactions/ContractUserDeleteTests.cpp @@ -0,0 +1,231 @@ +// Auto-generated unit tests for transaction ContractUserDelete + + +#include + +#include + +#include +#include +#include +#include +#include + +#include + +namespace xrpl::transactions { + +// 1 & 4) Set fields via builder setters, build, then read them back via +// wrapper getters. After build(), validate() should succeed. +TEST(TransactionsContractUserDeleteTests, BuilderSettersRoundTrip) +{ + // Generate a deterministic keypair for signing + auto const [publicKey, secretKey] = + generateKeyPair(KeyType::secp256k1, generateSeed("testContractUserDelete")); + + // Common transaction fields + auto const accountValue = calcAccountID(publicKey); + std::uint32_t const sequenceValue = 1; + auto const feeValue = canonical_AMOUNT(); + + // Transaction-specific field values + auto const contractAccountValue = canonical_ACCOUNT(); + auto const functionNameValue = canonical_VL(); + auto const parametersValue = canonical_ARRAY(); + auto const computationAllowanceValue = canonical_UINT32(); + + ContractUserDeleteBuilder builder{ + accountValue, + contractAccountValue, + functionNameValue, + computationAllowanceValue, + sequenceValue, + feeValue + }; + + // Set optional fields + builder.setParameters(parametersValue); + + auto tx = builder.build(publicKey, secretKey); + + std::string reason; + EXPECT_TRUE(tx.validate(reason)) << reason; + + // Verify signing was applied + EXPECT_FALSE(tx.getSigningPubKey().empty()); + EXPECT_TRUE(tx.hasTxnSignature()); + + // Verify common fields + EXPECT_EQ(tx.getAccount(), accountValue); + EXPECT_EQ(tx.getSequence(), sequenceValue); + EXPECT_EQ(tx.getFee(), feeValue); + + // Verify required fields + { + auto const& expected = contractAccountValue; + auto const actual = tx.getContractAccount(); + expectEqualField(expected, actual, "sfContractAccount"); + } + + { + auto const& expected = functionNameValue; + auto const actual = tx.getFunctionName(); + expectEqualField(expected, actual, "sfFunctionName"); + } + + { + auto const& expected = computationAllowanceValue; + auto const actual = tx.getComputationAllowance(); + expectEqualField(expected, actual, "sfComputationAllowance"); + } + + // Verify optional fields + { + auto const& expected = parametersValue; + auto const actualOpt = tx.getParameters(); + ASSERT_TRUE(actualOpt.has_value()) << "Optional field sfParameters should be present"; + expectEqualField(expected, *actualOpt, "sfParameters"); + EXPECT_TRUE(tx.hasParameters()); + } + +} + +// 2 & 4) Start from an STTx, construct a builder from it, build a new wrapper, +// and verify all fields match. +TEST(TransactionsContractUserDeleteTests, BuilderFromStTxRoundTrip) +{ + // Generate a deterministic keypair for signing + auto const [publicKey, secretKey] = + generateKeyPair(KeyType::secp256k1, generateSeed("testContractUserDeleteFromTx")); + + // Common transaction fields + auto const accountValue = calcAccountID(publicKey); + std::uint32_t const sequenceValue = 2; + auto const feeValue = canonical_AMOUNT(); + + // Transaction-specific field values + auto const contractAccountValue = canonical_ACCOUNT(); + auto const functionNameValue = canonical_VL(); + auto const parametersValue = canonical_ARRAY(); + auto const computationAllowanceValue = canonical_UINT32(); + + // Build an initial transaction + ContractUserDeleteBuilder initialBuilder{ + accountValue, + contractAccountValue, + functionNameValue, + computationAllowanceValue, + sequenceValue, + feeValue + }; + + initialBuilder.setParameters(parametersValue); + + auto initialTx = initialBuilder.build(publicKey, secretKey); + + // Create builder from existing STTx + ContractUserDeleteBuilder builderFromTx{initialTx.getSTTx()}; + + auto rebuiltTx = builderFromTx.build(publicKey, secretKey); + + std::string reason; + EXPECT_TRUE(rebuiltTx.validate(reason)) << reason; + + // Verify common fields + EXPECT_EQ(rebuiltTx.getAccount(), accountValue); + EXPECT_EQ(rebuiltTx.getSequence(), sequenceValue); + EXPECT_EQ(rebuiltTx.getFee(), feeValue); + + // Verify required fields + { + auto const& expected = contractAccountValue; + auto const actual = rebuiltTx.getContractAccount(); + expectEqualField(expected, actual, "sfContractAccount"); + } + + { + auto const& expected = functionNameValue; + auto const actual = rebuiltTx.getFunctionName(); + expectEqualField(expected, actual, "sfFunctionName"); + } + + { + auto const& expected = computationAllowanceValue; + auto const actual = rebuiltTx.getComputationAllowance(); + expectEqualField(expected, actual, "sfComputationAllowance"); + } + + // Verify optional fields + { + auto const& expected = parametersValue; + auto const actualOpt = rebuiltTx.getParameters(); + ASSERT_TRUE(actualOpt.has_value()) << "Optional field sfParameters should be present"; + expectEqualField(expected, *actualOpt, "sfParameters"); + } + +} + +// 3) Verify wrapper throws when constructed from wrong transaction type. +TEST(TransactionsContractUserDeleteTests, WrapperThrowsOnWrongTxType) +{ + // Build a valid transaction of a different type + auto const [pk, sk] = + generateKeyPair(KeyType::secp256k1, generateSeed("testWrongType")); + auto const account = calcAccountID(pk); + + AccountSetBuilder wrongBuilder{account, 1, canonical_AMOUNT()}; + auto wrongTx = wrongBuilder.build(pk, sk); + + EXPECT_THROW(ContractUserDelete{wrongTx.getSTTx()}, std::runtime_error); +} + +// 4) Verify builder throws when constructed from wrong transaction type. +TEST(TransactionsContractUserDeleteTests, BuilderThrowsOnWrongTxType) +{ + // Build a valid transaction of a different type + auto const [pk, sk] = + generateKeyPair(KeyType::secp256k1, generateSeed("testWrongTypeBuilder")); + auto const account = calcAccountID(pk); + + AccountSetBuilder wrongBuilder{account, 1, canonical_AMOUNT()}; + auto wrongTx = wrongBuilder.build(pk, sk); + + EXPECT_THROW(ContractUserDeleteBuilder{wrongTx.getSTTx()}, std::runtime_error); +} + +// 5) Build with only required fields and verify optional fields return nullopt. +TEST(TransactionsContractUserDeleteTests, OptionalFieldsReturnNullopt) +{ + // Generate a deterministic keypair for signing + auto const [publicKey, secretKey] = + generateKeyPair(KeyType::secp256k1, generateSeed("testContractUserDeleteNullopt")); + + // Common transaction fields + auto const accountValue = calcAccountID(publicKey); + std::uint32_t const sequenceValue = 3; + auto const feeValue = canonical_AMOUNT(); + + // Transaction-specific required field values + auto const contractAccountValue = canonical_ACCOUNT(); + auto const functionNameValue = canonical_VL(); + auto const computationAllowanceValue = canonical_UINT32(); + + ContractUserDeleteBuilder builder{ + accountValue, + contractAccountValue, + functionNameValue, + computationAllowanceValue, + sequenceValue, + feeValue + }; + + // Do NOT set optional fields + + auto tx = builder.build(publicKey, secretKey); + + // Verify optional fields are not present + EXPECT_FALSE(tx.hasParameters()); + EXPECT_FALSE(tx.getParameters().has_value()); +} + +} diff --git a/src/xrpld/app/misc/DeleteUtils.cpp b/src/xrpld/app/misc/DeleteUtils.cpp new file mode 100644 index 0000000000..503a5b72f1 --- /dev/null +++ b/src/xrpld/app/misc/DeleteUtils.cpp @@ -0,0 +1,383 @@ +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +namespace xrpl { + +// Local function definitions that provides signature compatibility. +TER +offerDelete( + ServiceRegistry& registry, + ApplyView& view, + AccountID const& account, + uint256 const& delIndex, + std::shared_ptr const& sleDel, + beast::Journal j) +{ + return offerDelete(view, sleDel, j); +} + +TER +removeSignersFromLedger( + ServiceRegistry& registry, + ApplyView& view, + AccountID const& account, + uint256 const& delIndex, + std::shared_ptr const& sleDel, + beast::Journal j) +{ + return SignerListSet::removeFromLedger(registry, view, account, j); +} + +TER +removeTicketFromLedger( + ServiceRegistry&, + ApplyView& view, + AccountID const& account, + uint256 const& delIndex, + std::shared_ptr const&, + beast::Journal j) +{ + return Transactor::ticketDelete(view, account, delIndex, j); +} + +TER +removeDepositPreauthFromLedger( + ServiceRegistry&, + ApplyView& view, + AccountID const&, + uint256 const& delIndex, + std::shared_ptr const&, + beast::Journal j) +{ + return DepositPreauth::removeFromLedger(view, delIndex, j); +} + +TER +removeNFTokenOfferFromLedger( + ServiceRegistry& registry, + ApplyView& view, + AccountID const& account, + uint256 const& delIndex, + std::shared_ptr const& sleDel, + beast::Journal) +{ + if (!nft::deleteTokenOffer(view, sleDel)) + return tefBAD_LEDGER; + + return tesSUCCESS; +} + +TER +removeDIDFromLedger( + ServiceRegistry& registry, + ApplyView& view, + AccountID const& account, + uint256 const& delIndex, + std::shared_ptr const& sleDel, + beast::Journal j) +{ + return DIDDelete::deleteSLE(view, sleDel, account, j); +} + +TER +removeOracleFromLedger( + ServiceRegistry&, + ApplyView& view, + AccountID const& account, + uint256 const&, + std::shared_ptr const& sleDel, + beast::Journal j) +{ + return OracleDelete::deleteOracle(view, sleDel, account, j); +} + +TER +removeCredentialFromLedger( + ServiceRegistry&, + ApplyView& view, + AccountID const&, + uint256 const&, + std::shared_ptr const& sleDel, + beast::Journal j) +{ + return credentials::deleteSLE(view, sleDel, j); +} + +TER +removeDelegateFromLedger( + ServiceRegistry& registry, + ApplyView& view, + AccountID const& account, + uint256 const& delIndex, + std::shared_ptr const& sleDel, + beast::Journal j) +{ + return DelegateSet::deleteDelegate(view, sleDel, account, j); +} + +TER +removeContractFromLedger( + ServiceRegistry& registry, + ApplyView& view, + AccountID const& account, + uint256 const& delIndex, + std::shared_ptr const& sleDel, + beast::Journal j) +{ + return ContractDelete::deleteContract(view, sleDel, account, j); +} + +// Return nullptr if the LedgerEntryType represents an obligation that can't +// be deleted. Otherwise return the pointer to the function that can delete +// the non-obligation +DeleterFuncPtr +nonObligationDeleter(LedgerEntryType t) +{ + switch (t) + { + case ltOFFER: + return offerDelete; + case ltSIGNER_LIST: + return removeSignersFromLedger; + case ltTICKET: + return removeTicketFromLedger; + case ltDEPOSIT_PREAUTH: + return removeDepositPreauthFromLedger; + case ltNFTOKEN_OFFER: + return removeNFTokenOfferFromLedger; + case ltDID: + return removeDIDFromLedger; + case ltORACLE: + return removeOracleFromLedger; + case ltCREDENTIAL: + return removeCredentialFromLedger; + case ltDELEGATE: + return removeDelegateFromLedger; + case ltCONTRACT: + return removeContractFromLedger; + default: + return nullptr; + } +} + +TER +deletePreclaim( + PreclaimContext const& ctx, + std::uint32_t seqDelta, + AccountID const account, + AccountID const dest, + bool isPseudoAccount) +{ + auto destSle = ctx.view.read(keylet::account(dest)); + + if (!destSle) + return tecNO_DST; + + if ((*destSle)[sfFlags] & lsfRequireDestTag && !ctx.tx[~sfDestinationTag]) + return tecDST_TAG_NEEDED; + + // If credentials are provided - check them anyway + if (auto const err = credentials::valid(ctx.tx, ctx.view, account, ctx.j); !isTesSuccess(err)) + return err; + + // if credentials then postpone auth check to doApply, to check for expired + // credentials + if (!ctx.tx.isFieldPresent(sfCredentialIDs)) + { + // Check whether the destination account requires deposit authorization. + if (destSle->getFlags() & lsfDepositAuth) + { + if (!ctx.view.exists(keylet::depositPreauth(dest, account)) && !isPseudoAccount) + return tecNO_PERMISSION; + } + } + + auto srcSle = ctx.view.read(keylet::account(account)); + XRPL_ASSERT(srcSle, "xrpl::DeleteAccount::preclaim : non-null account"); + if (!srcSle) + return terNO_ACCOUNT; + + { + // If an issuer has any issued NFTs resident in the ledger then it + // cannot be deleted. + if ((*srcSle)[~sfMintedNFTokens] != (*srcSle)[~sfBurnedNFTokens]) + return tecHAS_OBLIGATIONS; + + // If the account owns any NFTs it cannot be deleted. + Keylet const first = keylet::nftpage_min(account); + Keylet const last = keylet::nftpage_max(account); + + auto const cp = ctx.view.read( + Keylet(ltNFTOKEN_PAGE, ctx.view.succ(first.key, last.key.next()).value_or(last.key))); + if (cp) + return tecHAS_OBLIGATIONS; + } + + // We don't allow an account to be deleted if its sequence number + // is within 256 of the current ledger. This prevents replay of old + // transactions if this account is resurrected after it is deleted. + // + // We look at the account's Sequence rather than the transaction's + // Sequence in preparation for Tickets. + if ((*srcSle)[sfSequence] + seqDelta > ctx.view.seq()) + return tecTOO_SOON; + + // When fixNFTokenRemint is enabled, we don't allow an account to be + // deleted if is within 256 of the + // current ledger. This is to prevent having duplicate NFTokenIDs after + // account re-creation. + // + // Without this restriction, duplicate NFTokenIDs can be reproduced when + // authorized minting is involved. Because when the minter mints a NFToken, + // the issuer's sequence does not change. So when the issuer re-creates + // their account and mints a NFToken, it is possible that the + // NFTokenSequence of this NFToken is the same as the one that the + // authorized minter minted in a previous ledger. + if ((*srcSle)[~sfFirstNFTokenSequence].value_or(0) + (*srcSle)[~sfMintedNFTokens].value_or(0) + + seqDelta > + ctx.view.seq()) + return tecTOO_SOON; + + // Verify that the account does not own any objects that would prevent + // the account from being deleted. + Keylet const ownerDirKeylet{keylet::ownerDir(account)}; + if (dirIsEmpty(ctx.view, ownerDirKeylet)) + return tesSUCCESS; + + std::shared_ptr sleDirNode{}; + unsigned int uDirEntry{0}; + uint256 dirEntry{beast::zero}; + + // Account has no directory at all. This _should_ have been caught + // by the dirIsEmpty() check earlier, but it's okay to catch it here. + if (!cdirFirst(ctx.view, ownerDirKeylet.key, sleDirNode, uDirEntry, dirEntry)) + return tesSUCCESS; + + std::int32_t deletableDirEntryCount{0}; + do + { + // Make sure any directory node types that we find are the kind + // we can delete. + auto sleItem = ctx.view.read(keylet::child(dirEntry)); + if (!sleItem) + { + // Directory node has an invalid index. Bail out. + JLOG(ctx.j.fatal()) << "DeleteAccount: directory node in ledger " << ctx.view.seq() + << " has index to object that is missing: " << to_string(dirEntry); + return tefBAD_LEDGER; + } + + LedgerEntryType const nodeType{safe_cast((*sleItem)[sfLedgerEntryType])}; + + if (!nonObligationDeleter(nodeType)) + return tecHAS_OBLIGATIONS; + + // We found a deletable directory entry. Count it. If we find too + // many deletable directory entries then bail out. + if (++deletableDirEntryCount > maxDeletableDirEntries) + return tefTOO_BIG; + + } while (cdirNext(ctx.view, ownerDirKeylet.key, sleDirNode, uDirEntry, dirEntry)); + + return tesSUCCESS; +} + +TER +deleteDoApply( + ApplyContext& applyCtx, + STAmount const& accountBalance, + AccountID const& account, + AccountID const& dest) +{ + auto& view = applyCtx.view(); + STTx const tx = applyCtx.tx; + beast::Journal j = applyCtx.journal; + + auto srcSle = view.peek(keylet::account(account)); + XRPL_ASSERT(srcSle, "xrpl::DeleteAccount::doApply : non-null source account"); + + if (!srcSle) + return tefBAD_LEDGER; + + auto destSle = view.peek(keylet::account(dest)); + XRPL_ASSERT(destSle, "xrpl::DeleteAccount::doApply : non-null destination account"); + + if (!destSle) + return tefBAD_LEDGER; + + if (tx.isFieldPresent(sfCredentialIDs)) + { + if (auto err = verifyDepositPreauth(tx, view, account, dest, destSle, j); + !isTesSuccess(err)) + return err; + } + + Keylet const ownerDirKeylet{keylet::ownerDir(account)}; + auto const ter = cleanupOnAccountDelete( + view, + ownerDirKeylet, + [&](LedgerEntryType nodeType, + uint256 const& dirEntry, + std::shared_ptr& sleItem) -> std::pair { + if (auto deleter = nonObligationDeleter(nodeType)) + { + TER const result{deleter(applyCtx.registry, view, account, dirEntry, sleItem, j)}; + + return {result, SkipEntry::No}; + } + + UNREACHABLE( + "xrpl::DeleteAccount::doApply : undeletable item not found " + "in preclaim"); + JLOG(j.error()) << "DeleteAccount undeletable item not " + "found in preclaim."; + return {tecHAS_OBLIGATIONS, SkipEntry::No}; + }, + j); + if (ter != tesSUCCESS) + return ter; + + // Transfer any XRP remaining after the fee is paid to the destination: + (*destSle)[sfBalance] = (*destSle)[sfBalance] + accountBalance; + (*srcSle)[sfBalance] = (*srcSle)[sfBalance] - accountBalance; + applyCtx.deliver(accountBalance); + + // DA: Pseudo accounts can have 0 balance, so we skip this assert. + // FIX FIX FIX: DA FIX + // XRPL_ASSERT( + // (*srcSle)[sfBalance] == XRPAmount(0), + // "xrpl::DeleteAccount::doApply : source balance is zero"); + + // If there's still an owner directory associated with the source account + // delete it. + if (view.exists(ownerDirKeylet) && !view.emptyDirDelete(ownerDirKeylet)) + { + JLOG(j.error()) << "DeleteAccount cannot delete root dir node of " << toBase58(account); + return tecHAS_OBLIGATIONS; + } + + // Re-arm the password change fee if we can and need to. + if (accountBalance > XRPAmount(0) && (*destSle).isFlag(lsfPasswordSpent)) + (*destSle).clearFlag(lsfPasswordSpent); + + view.update(destSle); + view.erase(srcSle); + + return tesSUCCESS; +} + +} // namespace xrpl diff --git a/src/xrpld/app/misc/DeleteUtils.h b/src/xrpld/app/misc/DeleteUtils.h new file mode 100644 index 0000000000..6bcca1fb5f --- /dev/null +++ b/src/xrpld/app/misc/DeleteUtils.h @@ -0,0 +1,42 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace xrpl { + +// Define a function pointer type that can be used to delete ledger node types. +using DeleterFuncPtr = TER (*)( + ServiceRegistry& registry, + ApplyView& view, + AccountID const& account, + uint256 const& delIndex, + std::shared_ptr const& sleDel, + beast::Journal j); + +DeleterFuncPtr +nonObligationDeleter(LedgerEntryType t); + +TER +deletePreclaim( + PreclaimContext const& ctx, + std::uint32_t seqDelta, + AccountID const account, + AccountID const dest, + bool isPseudoAccount = false); + +TER +deleteDoApply( + ApplyContext& applyCtx, + STAmount const& accountBalance, + AccountID const& account, + AccountID const& dest); + +} // namespace xrpl diff --git a/src/xrpld/app/misc/NetworkOPs.cpp b/src/xrpld/app/misc/NetworkOPs.cpp index 84ddb6b5f1..a2c13bcd7b 100644 --- a/src/xrpld/app/misc/NetworkOPs.cpp +++ b/src/xrpld/app/misc/NetworkOPs.cpp @@ -537,6 +537,12 @@ public: subConsensus(InfoSub::ref ispListener) override; bool unsubConsensus(std::uint64_t uListener) override; + bool + subContractEvent(InfoSub::ref ispListener) override; + bool + unsubContractEvent(std::uint64_t uListener) override; + void + pubContractEvent(std::string const& name, STJson const& event) override; InfoSub::pointer findRpcSub(std::string const& strUrl) override; @@ -735,6 +741,7 @@ private: sPeerStatus, // Peer status changes. sConsensusPhase, // Consensus phase sBookChanges, // Per-ledger order book changes + sContractEvents, // Contract events sLastEntry // Any new entry must be ADDED ABOVE this one }; @@ -2268,6 +2275,34 @@ NetworkOPsImp::pubConsensus(ConsensusPhase phase) } } +void +NetworkOPsImp::pubContractEvent(std::string const& name, STJson const& event) +{ + std::lock_guard sl(mSubLock); + + auto& streamMap = mStreamMaps[sContractEvents]; + if (!streamMap.empty()) + { + Json::Value jvObj(Json::objectValue); + jvObj[jss::type] = "contractEvent"; + jvObj[jss::name] = name; + jvObj[jss::data] = event.getJson(JsonOptions::none); + + for (auto i = streamMap.begin(); i != streamMap.end();) + { + if (auto p = i->second.lock()) + { + p->send(jvObj, true); + ++i; + } + else + { + i = streamMap.erase(i); + } + } + } +} + void NetworkOPsImp::pubValidation(std::shared_ptr const& val) { @@ -4230,6 +4265,22 @@ NetworkOPsImp::unsubConsensus(std::uint64_t uSeq) return mStreamMaps[sConsensusPhase].erase(uSeq) != 0u; } +// <-- bool: true=added, false=already there +bool +NetworkOPsImp::subContractEvent(InfoSub::ref isrListener) +{ + std::lock_guard sl(mSubLock); + return mStreamMaps[sContractEvents].emplace(isrListener->getSeq(), isrListener).second; +} + +// <-- bool: true=erased, false=was not there +bool +NetworkOPsImp::unsubContractEvent(std::uint64_t uSeq) +{ + std::lock_guard sl(mSubLock); + return mStreamMaps[sContractEvents].erase(uSeq); +} + InfoSub::pointer NetworkOPsImp::findRpcSub(std::string const& strUrl) { diff --git a/src/xrpld/app/wasm/detail/ContractHostFuncImpl.cpp b/src/xrpld/app/wasm/detail/ContractHostFuncImpl.cpp new file mode 100644 index 0000000000..2c7c43dbe8 --- /dev/null +++ b/src/xrpld/app/wasm/detail/ContractHostFuncImpl.cpp @@ -0,0 +1,1053 @@ +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +namespace xrpl { + +Expected +getFieldBytesFromSTData(xrpl::STData const& funcParam, std::uint32_t stTypeId) +{ + switch (stTypeId) + { + case STI_UINT8: { + if (funcParam.getInnerSType() != STI_UINT8) + return Unexpected(HostFunctionError::INVALID_PARAMS); + uint8_t data = funcParam.getFieldU8(); + return Bytes{data}; + } + case STI_UINT16: { + if (funcParam.getInnerSType() != STI_UINT16) + return Unexpected(HostFunctionError::INVALID_PARAMS); + uint16_t data = funcParam.getFieldU16(); + return Bytes{ + static_cast(data & 0xFF), + static_cast((data >> 8) & 0xFF)}; + } + case STI_UINT32: { + if (funcParam.getInnerSType() != STI_UINT32) + return Unexpected(HostFunctionError::INVALID_PARAMS); + uint32_t data = funcParam.getFieldU32(); + return Bytes{ + static_cast(data & 0xFF), + static_cast((data >> 8) & 0xFF), + static_cast((data >> 16) & 0xFF), + static_cast((data >> 24) & 0xFF)}; + } + case STI_UINT64: { + if (funcParam.getInnerSType() != STI_UINT64) + return Unexpected(HostFunctionError::INVALID_PARAMS); + uint64_t data = funcParam.getFieldU64(); + return Bytes{ + static_cast(data & 0xFF), + static_cast((data >> 8) & 0xFF), + static_cast((data >> 16) & 0xFF), + static_cast((data >> 24) & 0xFF), + static_cast((data >> 32) & 0xFF), + static_cast((data >> 40) & 0xFF), + static_cast((data >> 48) & 0xFF), + static_cast((data >> 56) & 0xFF)}; + } + case STI_UINT128: { + if (funcParam.getInnerSType() != STI_UINT128) + return Unexpected(HostFunctionError::INVALID_PARAMS); + uint128 data = funcParam.getFieldH128(); + return Bytes{ + reinterpret_cast(&data), + reinterpret_cast(&data) + sizeof(uint128)}; + } + case STI_UINT160: { + if (funcParam.getInnerSType() != STI_UINT160) + return Unexpected(HostFunctionError::INVALID_PARAMS); + uint160 data = funcParam.getFieldH160(); + return Bytes{data.begin(), data.end()}; + } + case STI_UINT192: { + if (funcParam.getInnerSType() != STI_UINT192) + return Unexpected(HostFunctionError::INVALID_PARAMS); + uint192 data = funcParam.getFieldH192(); + return Bytes{data.begin(), data.end()}; + } + case STI_UINT256: { + if (funcParam.getInnerSType() != STI_UINT256) + return Unexpected(HostFunctionError::INVALID_PARAMS); + uint256 data = funcParam.getFieldH256(); + return Bytes{data.begin(), data.end()}; + } + case STI_VL: { + if (funcParam.getInnerSType() != STI_VL) + return Unexpected(HostFunctionError::INVALID_PARAMS); + auto data = funcParam.getFieldVL(); + return Bytes{data.begin(), data.end()}; + } + case STI_ACCOUNT: { + if (funcParam.getInnerSType() != STI_ACCOUNT) + return Unexpected(HostFunctionError::INVALID_PARAMS); + AccountID data = funcParam.getAccountID(); + return Bytes{data.data(), data.data() + data.size()}; + } + case STI_AMOUNT: { + if (funcParam.getInnerSType() != STI_AMOUNT) + return Unexpected(HostFunctionError::INVALID_PARAMS); + STAmount data = funcParam.getFieldAmount(); + Serializer s; + data.add(s); + auto const& serialized = s.getData(); + return Bytes{serialized.begin(), serialized.end()}; + } + case STI_NUMBER: { + if (funcParam.getInnerSType() != STI_NUMBER) + return Unexpected(HostFunctionError::INVALID_PARAMS); + STNumber data = funcParam.getFieldNumber(); + Serializer s; + data.add(s); + auto const& serialized = s.getData(); + return Bytes{serialized.begin(), serialized.end()}; + } + case STI_ISSUE: { + if (funcParam.getInnerSType() != STI_ISSUE) + return Unexpected(HostFunctionError::INVALID_PARAMS); + STIssue data = funcParam.getFieldIssue(); + Serializer s; + data.add(s); + auto const& serialized = s.getData(); + return Bytes{serialized.begin(), serialized.end()}; + } + case STI_CURRENCY: { + if (funcParam.getInnerSType() != STI_CURRENCY) + return Unexpected(HostFunctionError::INVALID_PARAMS); + STCurrency data = funcParam.getFieldCurrency(); + Serializer s; + data.add(s); + auto const& serialized = s.getData(); + return Bytes{serialized.begin(), serialized.end()}; + } + case STI_PATHSET: + case STI_VECTOR256: + case STI_XCHAIN_BRIDGE: + case STI_DATA: + case STI_DATATYPE: + case STI_JSON: + default: + return Unexpected(HostFunctionError::INVALID_PARAMS); + } + return Unexpected(HostFunctionError::INVALID_PARAMS); +} + +Expected +ContractHostFunctionsImpl::instanceParam(std::uint32_t index, std::uint32_t stTypeId) +{ + auto j = getJournal(); + auto const& instanceParams = contractCtx.instanceParameters; + + if (instanceParams.size() <= index) + { + JLOG(j.trace()) << "WasmTrace[" << contractId + << "]: " << "instanceParam: Index out of bounds"; + return Unexpected(HostFunctionError::INDEX_OUT_OF_BOUNDS); + } + + xrpl::STData const& instParam = instanceParams[index].value; + return getFieldBytesFromSTData(instParam, stTypeId); +} + +Expected +ContractHostFunctionsImpl::functionParam(std::uint32_t index, std::uint32_t stTypeId) +{ + auto j = getJournal(); + auto const& funcParams = contractCtx.functionParameters; + + if (funcParams.size() <= index) + { + JLOG(j.trace()) << "WasmTrace[" << contractId + << "]: " << "functionParam: Index out of bounds"; + return Unexpected(HostFunctionError::INDEX_OUT_OF_BOUNDS); + } + + xrpl::STData const& funcParam = funcParams[index].value; + return getFieldBytesFromSTData(funcParam, stTypeId); +} + +inline std::optional const>> +getDataCache(ContractContext& contractCtx, xrpl::AccountID const& account) +{ + auto& dataMap = contractCtx.result.dataMap; + if (dataMap.find(account) == dataMap.end()) + return std::nullopt; + + auto const& ret = dataMap[account]; + return std::cref(ret); +} + +inline std::pair +getDataOrCache(ContractContext& contractCtx, AccountID const& account) +{ + auto cacheEntryLookup = getDataCache(contractCtx, account); + if (!cacheEntryLookup) + { + AccountID const& contractAccount = contractCtx.result.contractAccount; + auto const dataKeylet = keylet::contractData(account, contractAccount); + auto& view = contractCtx.applyCtx.view(); + auto const dataSle = view.read(dataKeylet); + if (dataSle) + { + // Return the STJson from the SLE + STJson data = dataSle->getFieldJson(sfContractJson); + return {data.isObject(), data}; + } + + // Return New STJson if not found + STJson data; + return {true, data}; + } + + // Return the cached STJson + auto const& cacheEntry = cacheEntryLookup->get(); + return {cacheEntry.second.isObject(), cacheEntry.second}; +} + +inline HostFunctionError +setDataCache( + ContractContext& contractCtx, + AccountID const& account, + STJson const& data, + beast::Journal const& j, + bool modified = true) +{ + auto& dataMap = contractCtx.result.dataMap; + auto& view = contractCtx.applyCtx.view(); + auto const contractId = contractCtx.result.contractKeylet.key; + + auto const sleAccount = view.read(keylet::account(account)); + if (!sleAccount) + { + JLOG(j.trace()) << "WasmTrace[" << contractId << "]: " << "setDataCache: Account not found"; + return HostFunctionError::INVALID_ACCOUNT; + } + + uint32_t maxDataModifications = 1000u; + + if (modified && dataMap.modifiedCount >= maxDataModifications) + { + JLOG(j.trace()) << "WasmTrace[" << contractId << "]: " + << "setDataCache: Exceeded max data modifications"; + return HostFunctionError::INTERNAL; + } + + if (dataMap.find(account) == dataMap.end()) + { + auto const& fees = contractCtx.applyCtx.view().fees(); + STAmount bal = sleAccount->getFieldAmount(sfBalance); + + int64_t availableForReserves = + bal.xrp().drops() - fees.accountReserve(sleAccount->getFieldU32(sfOwnerCount)).drops(); + int64_t increment = fees.increment.drops(); + if (increment <= 0) + increment = 1; + + availableForReserves /= increment; + + if (availableForReserves < 1 && modified) + { + JLOG(j.trace()) << "WasmTrace[" << contractId + << "]: " << "setDataCache: Insufficient reserve"; + return HostFunctionError::INTERNAL; + } + + dataMap.modifiedCount++; + dataMap[account] = {modified, data}; + + // for (auto const& [acct, entry] : dataMap) + // { + // JLOG(j.trace()) + // << "Account: " << to_string(acct) + // << ", Modified: " << entry.first << ", Data: " + // << entry.second.getJson(JsonOptions::none).toStyledString(); + // } + + return HostFunctionError::SUCCESS; + } + + // auto& availableForReserves = std::get<0>(dataMap[account]); + // bool const canReserveNew = availableForReserves > 0; + if (modified) + { + // if (!canReserveNew) + // return HostFunctionError::INSUFFICIENT_RESERVE; + + // availableForReserves--; + dataMap.modifiedCount++; + } + + dataMap[account] = {modified, data}; + // for (auto const& [acct, entry] : dataMap) + // { + // JLOG(j.trace()) + // << "Account: " << to_string(acct) << ", Modified: " << + // entry.first + // << ", Data: " + // << entry.second.getJson(JsonOptions::none).toStyledString(); + // } + return HostFunctionError::SUCCESS; +} + +Expected +ContractHostFunctionsImpl::getDataObjectField(AccountID const& account, std::string_view const& key) +{ + auto j = getJournal(); + auto& view = contractCtx.applyCtx.view(); + AccountID const& contractAccount = contractCtx.result.contractAccount; + try + { + auto const sleAccount = view.read(keylet::account(account)); + if (!sleAccount) + { + JLOG(j.trace()) << "WasmTrace[" << contractId + << "]: " << "getDataObjectField: Account not found"; + return Unexpected(HostFunctionError::INVALID_ACCOUNT); + } + + // first check if the requested state was previously cached this session + auto cacheEntryLookup = getDataCache(contractCtx, account); + if (cacheEntryLookup) + { + auto const& cacheEntry = cacheEntryLookup->get(); + STJson const data = cacheEntry.second; + auto const keyValue = data.getObjectField(std::string(key)); + if (!keyValue) + { + JLOG(j.trace()) << "WasmTrace[" << contractId + << "]: " << "getDataObjectField: Invalid field"; + return Unexpected(HostFunctionError::INVALID_FIELD); + } + + Serializer s; + keyValue.value()->add(s); + return Bytes{s.peekData().data(), s.peekData().data() + s.peekData().size()}; + } + + auto const dataKeylet = keylet::contractData(account, contractAccount); + auto const dataSle = view.read(dataKeylet); + if (!dataSle) + { + JLOG(j.trace()) << "WasmTrace[" << contractId + << "]: " << "getDataObjectField: Data SLE not found"; + return Unexpected(HostFunctionError::INTERNAL); + } + + STJson const data = dataSle->getFieldJson(sfContractJson); + // it exists add it to cache and return it + if (setDataCache(contractCtx, account, data, j, false) != HostFunctionError::SUCCESS) + { + JLOG(j.trace()) << "WasmTrace[" << contractId << "]: " + << "getDataObjectField: Failed to set data cache"; + return Unexpected(HostFunctionError::INTERNAL); + } + + auto const keyValue = data.getObjectField(std::string(key)); + if (!keyValue) + { + JLOG(j.trace()) << "WasmTrace[" << contractId + << "]: " << "getDataObjectField: Invalid field"; + return Unexpected(HostFunctionError::INVALID_FIELD); + } + + Serializer s; + keyValue.value()->add(s); + return Bytes{s.peekData().data(), s.peekData().data() + s.peekData().size()}; + } + catch (std::exception const& e) + { + JLOG(j.trace()) << "WasmTrace[" << contractId << "]: " + << "getDataObjectField: Exception: " << e.what(); + return Unexpected(HostFunctionError::INTERNAL); + } +} + +Expected +ContractHostFunctionsImpl::getDataNestedObjectField( + AccountID const& account, + std::string_view const& key, + std::string_view const& nestedKey) +{ + auto j = getJournal(); + auto& view = contractCtx.applyCtx.view(); + AccountID const& contractAccount = contractCtx.result.contractAccount; + try + { + auto const sleAccount = view.read(keylet::account(account)); + if (!sleAccount) + { + JLOG(j.trace()) << "WasmTrace[" << contractId << "]: " + << "getDataNestedObjectField: Account not found"; + return Unexpected(HostFunctionError::INVALID_ACCOUNT); + } + + // first check if the requested state was previously cached this session + auto cacheEntryLookup = getDataCache(contractCtx, account); + if (cacheEntryLookup) + { + auto const& cacheEntry = cacheEntryLookup->get(); + STJson const data = cacheEntry.second; + auto const keyValue = + data.getNestedObjectField(std::string(key), std::string(nestedKey)); + if (!keyValue) + { + JLOG(j.trace()) << "WasmTrace[" << contractId + << "]: " << "getDataNestedObjectField: Invalid field"; + return Unexpected(HostFunctionError::INVALID_FIELD); + } + + Serializer s; + keyValue.value()->add(s); + return Bytes{s.peekData().data(), s.peekData().data() + s.peekData().size()}; + } + + auto const dataKeylet = keylet::contractData(account, contractAccount); + auto const dataSle = view.read(dataKeylet); + if (!dataSle) + { + JLOG(j.trace()) << "WasmTrace[" << contractId << "]: " + << "getDataNestedObjectField: Data SLE not found"; + return Unexpected(HostFunctionError::INTERNAL); + } + + STJson const data = dataSle->getFieldJson(sfContractJson); + // it exists add it to cache and return it + if (setDataCache(contractCtx, account, data, j, false) != HostFunctionError::SUCCESS) + { + JLOG(j.trace()) << "WasmTrace[" << contractId << "]: " + << "getDataNestedObjectField: Failed to set data cache"; + return Unexpected(HostFunctionError::INTERNAL); + } + + auto const keyValue = data.getNestedObjectField(std::string(key), std::string(nestedKey)); + if (!keyValue) + { + JLOG(j.trace()) << "WasmTrace[" << contractId + << "]: " << "getDataNestedObjectField: Invalid field"; + return Unexpected(HostFunctionError::INVALID_FIELD); + } + + Serializer s; + keyValue.value()->add(s); + return Bytes{s.peekData().data(), s.peekData().data() + s.peekData().size()}; + } + catch (std::exception const& e) + { + JLOG(j.trace()) << "WasmTrace[" << contractId << "]: " + << "getDataNestedObjectField: Exception: " << e.what(); + return Unexpected(HostFunctionError::INTERNAL); + } +} + +Expected +ContractHostFunctionsImpl::setDataObjectField( + AccountID const& account, + std::string_view const& key, + STJson::Value const& value) +{ + auto j = getJournal(); + try + { + auto [isObject, data] = getDataOrCache(contractCtx, account); + if (!isObject) + { + JLOG(j.trace()) << "WasmTrace[" << contractId << "]: " + << "setDataObjectField: Invalid state: not an object"; + return Unexpected(HostFunctionError::INVALID_STATE); + } + + data.setObjectField(std::string(key), value); + if (HostFunctionError ret = setDataCache(contractCtx, account, data, j, true); + ret != HostFunctionError::SUCCESS) + { + JLOG(j.trace()) << "WasmTrace[" << contractId << "]: " + << "setDataObjectField: Failed to set object field"; + return Unexpected(ret); + } + + return static_cast(HostFunctionError::SUCCESS); + } + catch (std::exception const& e) + { + JLOG(j.trace()) << "WasmTrace[" << contractId << "]: " + << "setDataObjectField: Exception: " << e.what(); + return Unexpected(HostFunctionError::INTERNAL); + } +} + +Expected +ContractHostFunctionsImpl::setDataNestedObjectField( + AccountID const& account, + std::string_view const& key, + std::string_view const& nestedKey, + STJson::Value const& value) +{ + auto j = getJournal(); + try + { + auto [isObject, data] = getDataOrCache(contractCtx, account); + if (!isObject) + { + JLOG(j.trace()) << "WasmTrace[" << contractId << "]: " + << "setDataNestedObjectField: Invalid state: not an object"; + return Unexpected(HostFunctionError::INVALID_STATE); + } + + data.setNestedObjectField(std::string(key), std::string(nestedKey), value); + if (HostFunctionError ret = setDataCache(contractCtx, account, data, j, true); + ret != HostFunctionError::SUCCESS) + { + JLOG(j.trace()) << "WasmTrace[" << contractId << "]: " + << "setDataNestedObjectField: Failed to set nested " + "object field"; + return Unexpected(ret); + } + + return static_cast(HostFunctionError::SUCCESS); + } + catch (std::exception const& e) + { + JLOG(j.trace()) << "WasmTrace[" << contractId << "]: " + << "setDataNestedObjectField: Exception: " << e.what(); + return Unexpected(HostFunctionError::INTERNAL); + } +} + +Expected +ContractHostFunctionsImpl::getDataArrayElementField( + AccountID const& account, + size_t index, + std::string_view const& key) +{ + auto j = getJournal(); + auto& view = contractCtx.applyCtx.view(); + AccountID const& contractAccount = contractCtx.result.contractAccount; + try + { + auto const sleAccount = view.read(keylet::account(account)); + if (!sleAccount) + { + JLOG(j.trace()) << "WasmTrace[" << contractId << "]: " + << "getDataArrayElementField: Account not found"; + return Unexpected(HostFunctionError::INVALID_ACCOUNT); + } + + // first check if the requested state was previously cached this session + auto cacheEntryLookup = getDataCache(contractCtx, account); + if (cacheEntryLookup) + { + auto const& cacheEntry = cacheEntryLookup->get(); + STJson const data = cacheEntry.second; + + if (!data.isArray()) + { + JLOG(j.trace()) << "WasmTrace[" << contractId << "]: " + << "getDataArrayElementField: Invalid state: not an array"; + return Unexpected(HostFunctionError::INVALID_STATE); + } + + auto const fieldValue = data.getArrayElementField(index, std::string(key)); + if (!fieldValue) + { + JLOG(j.trace()) << "WasmTrace[" << contractId << "]: " + << "getDataArrayElementField: Failed to get array " + "element field"; + return Unexpected(HostFunctionError::INVALID_FIELD); + } + + Serializer s; + fieldValue.value()->add(s); + return Bytes{s.peekData().data(), s.peekData().data() + s.peekData().size()}; + } + + auto const dataKeylet = keylet::contractData(account, contractAccount); + auto const dataSle = view.read(dataKeylet); + if (!dataSle) + { + JLOG(j.trace()) << "WasmTrace[" << contractId << "]: " + << "getDataArrayElementField: Failed to read contract data"; + return Unexpected(HostFunctionError::INTERNAL); + } + + STJson const data = dataSle->getFieldJson(sfContractJson); + + if (!data.isArray()) + { + JLOG(j.trace()) << "WasmTrace[" << contractId << "]: " + << "getDataArrayElementField: Invalid state: not an array"; + return Unexpected(HostFunctionError::INVALID_STATE); + } + + // it exists add it to cache and return it + if (setDataCache(contractCtx, account, data, j, false) != HostFunctionError::SUCCESS) + { + JLOG(j.trace()) << "WasmTrace[" << contractId << "]: " + << "setDataArrayElementField: Failed to set array " + "element field"; + return Unexpected(HostFunctionError::INTERNAL); + } + + auto const fieldValue = data.getArrayElementField(index, std::string(key)); + if (!fieldValue) + { + JLOG(j.trace()) << "WasmTrace[" << contractId << "]: " + << "getDataArrayElementField: Failed to get array " + "element field"; + return Unexpected(HostFunctionError::INVALID_FIELD); + } + + Serializer s; + fieldValue.value()->add(s); + return Bytes{s.peekData().data(), s.peekData().data() + s.peekData().size()}; + } + catch (std::exception const& e) + { + JLOG(j.trace()) << "WasmTrace[" << contractId << "]: " + << "getDataArrayElementField: Exception: " << e.what(); + return Unexpected(HostFunctionError::INTERNAL); + } +} + +Expected +ContractHostFunctionsImpl::getDataNestedArrayElementField( + AccountID const& account, + std::string_view const& key, + size_t index, + std::string_view const& nestedKey) +{ + auto j = getJournal(); + auto& view = contractCtx.applyCtx.view(); + AccountID const& contractAccount = contractCtx.result.contractAccount; + try + { + auto const sleAccount = view.read(keylet::account(account)); + if (!sleAccount) + { + JLOG(j.trace()) << "WasmTrace[" << contractId << "]: " + << "getDataNestedArrayElementField: Account not found"; + return Unexpected(HostFunctionError::INVALID_ACCOUNT); + } + + // if (account != contractCtx.result.otxnAccount) + // { + // JLOG(j.trace()) << "WasmTrace[" << contractId << "]: " + // << "getDataNestedArrayElementField: Unauthorized + // access to account data"; + // return Unexpected(HostFunctionError::INVALID_ACCOUNT); + // } + + // first check if the requested state was previously cached this session + auto cacheEntryLookup = getDataCache(contractCtx, account); + if (cacheEntryLookup) + { + auto const& cacheEntry = cacheEntryLookup->get(); + STJson const data = cacheEntry.second; + + if (!data.isObject()) + { + JLOG(j.trace()) << "WasmTrace[" << contractId << "]: " + << "getDataNestedArrayElementField: Invalid state: " + "not an object"; + return Unexpected(HostFunctionError::INVALID_STATE); + } + + auto const fieldValue = + data.getNestedArrayElementField(std::string(key), index, std::string(nestedKey)); + if (!fieldValue) + { + JLOG(j.trace()) << "WasmTrace[" << contractId << "]: " + << "getDataNestedArrayElementField: Failed to get " + "nested array element field"; + return Unexpected(HostFunctionError::INVALID_FIELD); + } + + Serializer s; + fieldValue.value()->add(s); + return Bytes{s.peekData().data(), s.peekData().data() + s.peekData().size()}; + } + + auto const dataKeylet = keylet::contractData(account, contractAccount); + auto const dataSle = view.read(dataKeylet); + if (!dataSle) + { + JLOG(j.trace()) << "WasmTrace[" << contractId << "]: " + << "getDataNestedArrayElementField: Failed to read " + "contract data"; + return Unexpected(HostFunctionError::INTERNAL); + } + + STJson const data = dataSle->getFieldJson(sfContractJson); + + if (!data.isObject()) + { + JLOG(j.trace()) << "WasmTrace[" << contractId << "]: " + << "getDataNestedArrayElementField: Invalid state: " + "not an object"; + return Unexpected(HostFunctionError::INVALID_STATE); + } + + // it exists add it to cache and return it + if (setDataCache(contractCtx, account, data, j, false) != HostFunctionError::SUCCESS) + { + JLOG(j.trace()) << "WasmTrace[" << contractId << "]: " + << "setDataNestedArrayElementField: Failed to set " + "nested array element field"; + return Unexpected(HostFunctionError::INTERNAL); + } + + auto const fieldValue = + data.getNestedArrayElementField(std::string(key), index, std::string(nestedKey)); + if (!fieldValue) + { + JLOG(j.trace()) << "WasmTrace[" << contractId << "]: " + << "getDataNestedArrayElementField: Failed to get " + "nested array element field"; + return Unexpected(HostFunctionError::INVALID_FIELD); + } + + Serializer s; + fieldValue.value()->add(s); + return Bytes{s.peekData().data(), s.peekData().data() + s.peekData().size()}; + } + catch (std::exception const& e) + { + JLOG(j.trace()) << "WasmTrace[" << contractId << "]: " + << "getDataNestedArrayElementField: Exception: " << e.what(); + return Unexpected(HostFunctionError::INTERNAL); + } +} + +Expected +ContractHostFunctionsImpl::setDataArrayElementField( + AccountID const& account, + size_t index, + std::string_view const& key, + STJson::Value const& value) +{ + auto j = getJournal(); + auto [isObject, data] = getDataOrCache(contractCtx, account); + + try + { + // For array operations, we expect isObject to be false (indicating it's + // an array) But getDataOrCache returns isObject=true for new data, so + // we need to check the actual type + if (isObject && data.getMap().size() > 0) + { + JLOG(j.trace()) << "WasmTrace[" << contractId << "]: " + << "setDataArrayElementField: Invalid state: not an array"; + return Unexpected(HostFunctionError::INVALID_STATE); + } + + // If it's a new empty object, convert it to an array + if (isObject && data.getMap().empty()) + { + data = STJson(STJson::Array{}); + } + + if (!data.isArray()) + { + JLOG(j.trace()) << "WasmTrace[" << contractId << "]: " + << "setDataArrayElementField: Invalid state: not an array"; + return Unexpected(HostFunctionError::INVALID_STATE); + } + + data.setArrayElementField(index, std::string(key), value); + if (HostFunctionError ret = setDataCache(contractCtx, account, data, j, true); + ret != HostFunctionError::SUCCESS) + { + JLOG(j.trace()) << "WasmTrace[" << contractId << "]: " + << "setDataArrayElementField: Failed to set array " + "element field"; + return Unexpected(ret); + } + + return static_cast(HostFunctionError::SUCCESS); + } + catch (std::exception const& e) + { + JLOG(j.trace()) << "WasmTrace[" << contractId << "]: " + << "setDataArrayElementField: Exception: " << e.what(); + return Unexpected(HostFunctionError::INTERNAL); + } +} + +Expected +ContractHostFunctionsImpl::setDataNestedArrayElementField( + AccountID const& account, + std::string_view const& key, + size_t index, + std::string_view const& nestedKey, + STJson::Value const& value) +{ + auto j = getJournal(); + try + { + auto [isObject, data] = getDataOrCache(contractCtx, account); + if (!isObject) + { + JLOG(j.trace()) << "WasmTrace[" << contractId << "]: " + << "setDataNestedArrayElementField: Invalid state: " + "not an object"; + return Unexpected(HostFunctionError::INVALID_STATE); + } + + data.setNestedArrayElementField(std::string(key), index, std::string(nestedKey), value); + if (HostFunctionError ret = setDataCache(contractCtx, account, data, j, true); + ret != HostFunctionError::SUCCESS) + { + JLOG(j.trace()) << "WasmTrace[" << contractId << "]: " + << "setDataNestedArrayElementField: Failed to set " + "nested array element field"; + return Unexpected(ret); + } + + return static_cast(HostFunctionError::SUCCESS); + } + catch (std::exception const& e) + { + JLOG(j.trace()) << "WasmTrace[" << contractId << "]: " + << "setDataNestedArrayElementField: Exception: " << e.what(); + return Unexpected(HostFunctionError::INTERNAL); + } +} + +Expected +ContractHostFunctionsImpl::buildTxn(std::uint16_t const& txType) +{ + auto j = getJournal(); + auto& app = contractCtx.applyCtx.registry; + + if (!Emitable::getInstance().isEmitable(txType)) + { + JLOG(j.trace()) << "Transaction type: " << txType << " is not emitable."; + return Unexpected(HostFunctionError::SUBMIT_TXN_FAILURE); + } + + try + { + auto jv = Json::Value(Json::objectValue); + auto item = TxFormats::getInstance().findByType(safe_cast(txType)); + jv[sfTransactionType] = item->getName(); + jv[sfFee] = "0"; + jv[sfFlags] = 1073741824; + jv[sfSequence] = contractCtx.result.nextSequence; + jv[sfAccount] = to_string(contractCtx.result.contractAccount); + jv[sfSigningPubKey] = ""; + if (auto const networkID = app.get().getNetworkIDService().getNetworkID(); networkID != 0) + jv[sfNetworkID] = networkID; + + STParsedJSONObject parsed("txn", jv); + contractCtx.built_txns.push_back(*parsed.object); + contractCtx.result.nextSequence += 1; + return contractCtx.built_txns.size() - 1; + } + catch (std::exception const& e) + { + JLOG(j.trace()) << "WasmTrace[" << contractId << "]: Exception in buildTxn: " << e.what(); + return Unexpected(HostFunctionError::INTERNAL); + } +} + +Expected +ContractHostFunctionsImpl::addTxnField( + std::uint32_t const& index, + SField const& field, + Slice const& data) +{ + auto j = getJournal(); + try + { + if (index >= contractCtx.built_txns.size()) + { + JLOG(j.trace()) << "WasmTrace[" << contractId << "]: " + << "addTxnField: index out of bounds: " << index; + return Unexpected(HostFunctionError::INDEX_OUT_OF_BOUNDS); + } + + // Get the transaction STObject + auto& obj = contractCtx.built_txns[index]; + + // Ensure the transaction has a TransactionType field + if (!obj.isFieldPresent(sfTransactionType)) + { + JLOG(j.trace()) << "WasmTrace[" << contractId << "]: " + << "addTxnField: TransactionType field not present " + "in transaction."; + return Unexpected(HostFunctionError::FIELD_NOT_FOUND); + } + + // Extract the numeric tx type from the STObject and convert to TxType + auto txTypeVal = obj.getFieldU16(sfTransactionType); + auto txFormat = TxFormats::getInstance().findByType(safe_cast(txTypeVal)); + if (!txFormat) + { + JLOG(j.trace()) << "WasmTrace[" << contractId << "]: " + << "addTxnField: Invalid TransactionType: " << txTypeVal; + return Unexpected(HostFunctionError::FIELD_NOT_FOUND); + } + + // Check if the provided field is allowed for this transaction type + bool found = false; + for (auto const& e : txFormat->getSOTemplate()) + { + if (e.sField().getName() == field.getName()) + { + found = true; + break; + } + } + if (!found) + { + JLOG(j.trace()) << "WasmTrace[" << contractId << "]: " << "addTxnField: Field " + << field.getName() << " not allowed in transaction type " + << txFormat->getName(); + return Unexpected(HostFunctionError::FIELD_NOT_FOUND); + } + + obj.addFieldFromSlice(field, data); + JLOG(j.trace()) << "WasmTrace[" << contractId << "]: " << "addTxnField: TXN: " + << obj.getJson(JsonOptions::none).toStyledString(); + return static_cast(HostFunctionError::SUCCESS); + } + catch (std::exception const& e) + { + JLOG(j.trace()) << "WasmTrace[" << contractId + << "]: Exception in addTxnField: " << e.what(); + return Unexpected(HostFunctionError::INTERNAL); + } +} + +Expected +ContractHostFunctionsImpl::emitBuiltTxn(std::uint32_t const& index) +{ + auto j = getJournal(); + auto& app = contractCtx.applyCtx.registry; + auto& parentTx = contractCtx.applyCtx.tx; + auto const parentBatchId = parentTx.getTransactionID(); + try + { + if (index >= contractCtx.built_txns.size()) + { + JLOG(j.trace()) << "WasmTrace[" << parentBatchId + << "]: " << "emitBuiltTxn: index out of bounds: " << index; + return Unexpected(HostFunctionError::INDEX_OUT_OF_BOUNDS); + } + + // Ensure tfInnerBatchTxn is always set, even if the contract + // overwrote sfFlags via addTxnField. + contractCtx.built_txns[index].setFlag(tfInnerBatchTxn); + auto stxPtr = std::make_shared(std::move(contractCtx.built_txns[index])); + + std::string reason; + auto tpTrans = std::make_shared(stxPtr, reason, app.get().getApp()); + if (tpTrans->getStatus() != NEW) + { + JLOG(j.trace()) << "WasmTrace[" << parentBatchId << "]: " + << "emitBuiltTxn: Failed to decode transaction: " << reason; + return Unexpected(HostFunctionError::SUBMIT_TXN_FAILURE); + } + + OpenView wholeBatchView(batch_view, contractCtx.applyCtx.openView()); + auto applyOneTransaction = + [&app, &j, &parentBatchId, &wholeBatchView](std::shared_ptr const& tx) { + auto const pfResult = + preflight(app, wholeBatchView.rules(), parentBatchId, *tx, tapBATCH, j); + auto const ret = preclaim(pfResult, app, wholeBatchView); + JLOG(j.trace()) << "WasmTrace[" << parentBatchId << "]: " << tx->getTransactionID() + << " " << transToken(ret.ter); + return ret; + }; + + auto const stx = tpTrans->getSTransaction(); + auto const result = applyOneTransaction(stx); + if (isTesSuccess(result.ter)) + contractCtx.result.emittedTxns.push(stx); + return TERtoInt(result.ter); + } + catch (std::exception const& e) + { + JLOG(j.trace()) << "WasmTrace[" << parentBatchId + << "]: Exception in emitBuiltTxn: " << e.what(); + return Unexpected(HostFunctionError::INTERNAL); + } +} + +Expected +ContractHostFunctionsImpl::emitTxn(std::shared_ptr const& stxPtr) +{ + auto& app = contractCtx.applyCtx.registry; + auto& parentTx = contractCtx.applyCtx.tx; + auto j = getJournal(); + + try + { + // Ensure tfInnerBatchTxn is always set on emitted transactions. + // Since STTx is const, create a mutable copy if the flag is missing. + std::shared_ptr txPtr = stxPtr; + if (!stxPtr->isFlag(tfInnerBatchTxn)) + { + STObject obj(static_cast(*stxPtr)); + obj.setFlag(tfInnerBatchTxn); + txPtr = std::make_shared(std::move(obj)); + } + + std::string reason; + auto tpTrans = std::make_shared(txPtr, reason, app.get().getApp()); + if (tpTrans->getStatus() != NEW) + return Unexpected(HostFunctionError::SUBMIT_TXN_FAILURE); + + OpenView wholeBatchView(batch_view, contractCtx.applyCtx.openView()); + auto const parentBatchId = parentTx.getTransactionID(); + auto applyOneTransaction = + [&app, &j, &parentBatchId, &wholeBatchView](std::shared_ptr const& tx) { + auto const pfResult = + preflight(app, wholeBatchView.rules(), parentBatchId, *tx, tapBATCH, j); + auto const ret = preclaim(pfResult, app, wholeBatchView); + JLOG(j.trace()) << "WasmTrace[" << parentBatchId << "]: " << tx->getTransactionID() + << " " << transToken(ret.ter); + return ret; + }; + + auto const stx = tpTrans->getSTransaction(); + auto const result = applyOneTransaction(stx); + if (isTesSuccess(result.ter)) + contractCtx.result.emittedTxns.push(stx); + return TERtoInt(result.ter); + } + catch (std::exception const& e) + { + JLOG(j.trace()) << "WasmTrace[" << parentTx.getTransactionID() + << "]: Exception in emitTxn: " << e.what(); + return Unexpected(HostFunctionError::INTERNAL); + } +} + +Expected +ContractHostFunctionsImpl::emitEvent(std::string_view const& eventName, STJson const& eventData) +{ + auto j = getJournal(); + + try + { + // TODO: Validation + auto& eventMap = contractCtx.result.eventMap; + eventMap[std::string(eventName)] = eventData; + return static_cast(HostFunctionError::SUCCESS); + } + catch (std::exception const& e) + { + JLOG(j.trace()) << "WasmTrace[" << contractId << "]: Exception in emitEvent: " << e.what(); + return Unexpected(HostFunctionError::INTERNAL); + } +} + +} // namespace xrpl diff --git a/src/xrpld/core/Config.h b/src/xrpld/core/Config.h index 89f4dc7743..1b3a5fff03 100644 --- a/src/xrpld/core/Config.h +++ b/src/xrpld/core/Config.h @@ -72,7 +72,14 @@ struct FeeSetup Fees toFees() const { - return Fees{reference_fee, account_reserve, owner_reserve}; + Fees f; + f.base = reference_fee; + f.reserve = account_reserve; + f.increment = owner_reserve; + f.extensionComputeLimit = extension_compute_limit; + f.extensionSizeLimit = extension_size_limit; + f.gasPrice = gas_price; + return f; } }; diff --git a/src/xrpld/rpc/detail/Handler.cpp b/src/xrpld/rpc/detail/Handler.cpp index 05fc1cb0b4..983d9d7327 100644 --- a/src/xrpld/rpc/detail/Handler.cpp +++ b/src/xrpld/rpc/detail/Handler.cpp @@ -85,6 +85,7 @@ Handler const handlerArray[]{ {"channel_verify", byRef(&doChannelVerify), Role::USER, NO_CONDITION}, {"connect", byRef(&doConnect), Role::ADMIN, NO_CONDITION}, {"consensus_info", byRef(&doConsensusInfo), Role::ADMIN, NO_CONDITION}, + {"contract_info", byRef(&doContractInfo), Role::USER, NO_CONDITION}, {"deposit_authorized", byRef(&doDepositAuthorized), Role::USER, NO_CONDITION}, {"feature", byRef(&doFeature), Role::USER, NO_CONDITION}, {"fee", byRef(&doFee), Role::USER, NEEDS_CURRENT_LEDGER}, diff --git a/src/xrpld/rpc/detail/RPCHelpers.cpp b/src/xrpld/rpc/detail/RPCHelpers.cpp index b4a0685bd6..9e3b444dcd 100644 --- a/src/xrpld/rpc/detail/RPCHelpers.cpp +++ b/src/xrpld/rpc/detail/RPCHelpers.cpp @@ -5,12 +5,12 @@ #include #include +#include #include #include #include #include #include -#include #include #include diff --git a/src/xrpld/rpc/detail/RPCLedgerHelpers.cpp b/src/xrpld/rpc/detail/RPCLedgerHelpers.cpp index 955533c776..0e78b2d726 100644 --- a/src/xrpld/rpc/detail/RPCLedgerHelpers.cpp +++ b/src/xrpld/rpc/detail/RPCLedgerHelpers.cpp @@ -475,5 +475,41 @@ getOrAcquireLedger(RPC::JsonContext const& context) RPC::make_error(rpcNOT_READY, "findCreate failed to return an inbound ledger")); } +/** + * @brief Injects JSON describing a ledger entry. + * + * @param jv The JSON value to populate. + * @param sle The ledger entry to describe. + * + * @details + * Populates the provided JSON value with the description of the specified + * ledger entry. If the entry is an account root and contains an email hash, + * adds a 'urlgravatar' field with the corresponding Gravatar URL. + * If the entry is not an account root, sets the 'Invalid' field to true. + */ +void +injectSLE(Json::Value& jv, SLE const& sle) +{ + jv = sle.getJson(JsonOptions::none); + if (sle.getType() == ltACCOUNT_ROOT) + { + if (sle.isFieldPresent(sfEmailHash)) + { + auto const& hash = sle.getFieldH128(sfEmailHash); + Blob const b(hash.begin(), hash.end()); + std::string md5 = strHex(makeSlice(b)); + boost::to_lower(md5); + // VFALCO TODO Give a name and move this constant + // to a more visible location. Also + // shouldn't this be https? + jv[jss::urlgravatar] = str(boost::format("http://www.gravatar.com/avatar/%s") % md5); + } + } + else + { + jv[jss::Invalid] = true; + } +} + } // namespace RPC } // namespace xrpl diff --git a/src/xrpld/rpc/detail/RPCLedgerHelpers.h b/src/xrpld/rpc/detail/RPCLedgerHelpers.h index 6a04ab41d6..ec4ac2cdfa 100644 --- a/src/xrpld/rpc/detail/RPCLedgerHelpers.h +++ b/src/xrpld/rpc/detail/RPCLedgerHelpers.h @@ -170,6 +170,9 @@ ledgerFromSpecifier( Expected, Json::Value> getOrAcquireLedger(RPC::JsonContext const& context); +void +injectSLE(Json::Value& jv, SLE const& sle); + } // namespace RPC } // namespace xrpl diff --git a/src/xrpld/rpc/handlers/ContractInfo.cpp b/src/xrpld/rpc/handlers/ContractInfo.cpp new file mode 100644 index 0000000000..b07fa60d99 --- /dev/null +++ b/src/xrpld/rpc/handlers/ContractInfo.cpp @@ -0,0 +1,162 @@ +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +namespace xrpl { + +// { +// contract_account: , +// function : // optional +// user_account : +// ledger_index : +// } + +Json::Value +doContractInfo(RPC::JsonContext& context) +{ + auto& params = context.params; + + std::string contractAccount; + if (params.isMember(jss::contract_account)) + { + if (!params[jss::contract_account].isString()) + return RPC::invalid_field_error(jss::contract_account); + contractAccount = params[jss::contract_account].asString(); + } + else + return RPC::missing_field_error(jss::contract_account); + + std::string functionName; + if (params.isMember(jss::function)) + { + if (!params[jss::function].isString()) + return RPC::invalid_field_error(jss::function); + functionName = params[jss::function].asString(); + } + + std::string account; + if (params.isMember(jss::account)) + { + if (!params[jss::account].isString()) + return RPC::invalid_field_error(jss::account); + account = params[jss::account].asString(); + } + + std::shared_ptr ledger; + auto result = RPC::lookupLedger(ledger, context); + + if (!ledger) + return result; + + // contract account + auto caid = parseBase58(contractAccount); + if (!caid) + { + RPC::inject_error(rpcACT_MALFORMED, result); + return result; + } + auto const caID{std::move(caid.value())}; + auto const caSle = ledger->read(keylet::account(caID)); + if (!caSle) + { + result[jss::contract_account] = toBase58(caID); + RPC::inject_error(rpcACT_NOT_FOUND, result); + } + + uint256 const contractID = caSle->getFieldH256(sfContractID); + auto const contractSle = ledger->read(keylet::contract(contractID)); + if (!contractSle) + { + result[jss::contract_account] = toBase58(caID); + RPC::inject_error(rpcOBJECT_NOT_FOUND, result); + } + + // contract source + if (!contractSle->at(sfContractHash)) + { + result[jss::contract_account] = toBase58(caID); + RPC::inject_error(rpcUNKNOWN, result); + } + + auto const sourceSle = ledger->read(keylet::contractSource(contractSle->at(sfContractHash))); + if (!sourceSle) + { + result[jss::contract_account] = toBase58(caID); + RPC::inject_error(rpcOBJECT_NOT_FOUND, result); + } + + result[jss::contract_account] = toBase58(caID); + result[jss::code] = strHex(sourceSle->at(sfContractCode)); + result[jss::hash] = to_string(sourceSle->at(sfContractHash)); + + // lambda to format the functions response: + // name: + // params: [: , : , : ] + auto formatFunctions = [](Json::Value& jv, std::shared_ptr const& slePtr) { + if (slePtr && slePtr->isFieldPresent(sfFunctions)) + { + auto const& functions = slePtr->getFieldArray(sfFunctions); + for (auto const& function : functions) + { + Json::Value jvFunction(Json::objectValue); + jvFunction[jss::name] = strHex(function.getFieldVL(sfFunctionName)); + Json::Value jvParams(Json::arrayValue); + for (auto const& param : function.getFieldArray(sfParameters)) + { + Json::Value jvParam(Json::objectValue); + jvParam[jss::flags] = param.getFieldU32(sfParameterFlag); + jvParam[jss::type] = + param.getFieldDataType(sfParameterType).getInnerTypeString(); + jvParams.append(jvParam); + } + jvFunction[jss::params] = std::move(jvParams); + jv.append(std::move(jvFunction)); + } + } + }; + if (sourceSle->isFieldPresent(sfFunctions)) + formatFunctions(result[jss::functions], sourceSle); + if (contractSle->isFieldPresent(sfURI)) + result[jss::source_code_uri] = strHex(contractSle->at(sfURI)); + + Json::Value jvAccepted(Json::objectValue); + RPC::injectSLE(jvAccepted, *caSle); + result[jss::account_data] = jvAccepted; + + auto const dataSle = ledger->read(keylet::contractData(caID, caID)); + if (dataSle) + result[jss::contract_data] = + dataSle->getFieldJson(sfContractJson).getJson(JsonOptions::none); + + if (!account.empty()) + { + auto id = parseBase58(account); + if (!id) + { + RPC::inject_error(rpcACT_MALFORMED, result); + return result; + } + auto const accountID = id.value(); + if (ledger->exists(keylet::account(accountID))) + { + if (auto dataSle = ledger->read(keylet::contractData(accountID, caID))) + result[jss::user_data] = + dataSle->getFieldJson(sfContractJson).getJson(JsonOptions::none); + } + } + + return result; +} + +} // namespace xrpl diff --git a/src/xrpld/rpc/handlers/Handlers.h b/src/xrpld/rpc/handlers/Handlers.h index 23328cf52a..a28e95239c 100644 --- a/src/xrpld/rpc/handlers/Handlers.h +++ b/src/xrpld/rpc/handlers/Handlers.h @@ -39,6 +39,8 @@ doConnect(RPC::JsonContext&); Json::Value doConsensusInfo(RPC::JsonContext&); Json::Value +doContractInfo(RPC::JsonContext&); +Json::Value doDepositAuthorized(RPC::JsonContext&); Json::Value doFeature(RPC::JsonContext&); diff --git a/src/xrpld/rpc/handlers/account/AccountInfo.cpp b/src/xrpld/rpc/handlers/account/AccountInfo.cpp index becaea8a51..72b3e8874e 100644 --- a/src/xrpld/rpc/handlers/account/AccountInfo.cpp +++ b/src/xrpld/rpc/handlers/account/AccountInfo.cpp @@ -16,42 +16,6 @@ namespace xrpl { -/** - * @brief Injects JSON describing a ledger entry. - * - * @param jv The JSON value to populate. - * @param sle The ledger entry to describe. - * - * @details - * Populates the provided JSON value with the description of the specified - * ledger entry. If the entry is an account root and contains an email hash, - * adds a 'urlgravatar' field with the corresponding Gravatar URL. - * If the entry is not an account root, sets the 'Invalid' field to true. - */ -void -injectSLE(Json::Value& jv, SLE const& sle) -{ - jv = sle.getJson(JsonOptions::none); - if (sle.getType() == ltACCOUNT_ROOT) - { - if (sle.isFieldPresent(sfEmailHash)) - { - auto const& hash = sle.getFieldH128(sfEmailHash); - Blob const b(hash.begin(), hash.end()); - std::string md5 = strHex(makeSlice(b)); - boost::to_lower(md5); - // VFALCO TODO Give a name and move this constant - // to a more visible location. Also - // shouldn't this be https? - jv[jss::urlgravatar] = str(boost::format("http://www.gravatar.com/avatar/%s") % md5); - } - } - else - { - jv[jss::Invalid] = true; - } -} - // { // account: , // ledger_hash : @@ -142,7 +106,7 @@ doAccountInfo(RPC::JsonContext& context) } Json::Value jvAccepted(Json::objectValue); - injectSLE(jvAccepted, *sleAccepted); + RPC::injectSLE(jvAccepted, *sleAccepted); result[jss::account_data] = jvAccepted; Json::Value acctFlags{Json::objectValue}; diff --git a/src/xrpld/rpc/handlers/account/AccountNFTs.cpp b/src/xrpld/rpc/handlers/account/AccountNFTs.cpp index b879968e4e..424a3143d9 100644 --- a/src/xrpld/rpc/handlers/account/AccountNFTs.cpp +++ b/src/xrpld/rpc/handlers/account/AccountNFTs.cpp @@ -4,13 +4,13 @@ #include #include +#include #include #include #include #include #include #include -#include namespace xrpl { diff --git a/src/xrpld/rpc/handlers/ledger/LedgerEntry.cpp b/src/xrpld/rpc/handlers/ledger/LedgerEntry.cpp index 377983718e..e9b77c0c31 100644 --- a/src/xrpld/rpc/handlers/ledger/LedgerEntry.cpp +++ b/src/xrpld/rpc/handlers/ledger/LedgerEntry.cpp @@ -802,6 +802,72 @@ parseXChainOwnedCreateAccountClaimID( return keylet.key; } +static Expected +parseContractSource( + Json::Value const& params, + Json::StaticString const fieldName, + [[maybe_unused]] unsigned const apiVersion) +{ + if (!params.isObject()) + { + return parseObjectID(params, fieldName); + } + + auto const id = LedgerEntryHelpers::requiredAccountID(params, jss::owner, "malformedOwner"); + if (!id) + return Unexpected(id.error()); + + auto const seq = LedgerEntryHelpers::requiredUInt32(params, jss::seq, "malformedRequest"); + if (!seq) + return Unexpected(seq.error()); + + return keylet::vault(*id, *seq).key; +} + +static Expected +parseContract( + Json::Value const& params, + Json::StaticString const fieldName, + [[maybe_unused]] unsigned const apiVersion) +{ + if (!params.isObject()) + { + return parseObjectID(params, fieldName); + } + + auto const id = LedgerEntryHelpers::requiredAccountID(params, jss::owner, "malformedOwner"); + if (!id) + return Unexpected(id.error()); + + auto const seq = LedgerEntryHelpers::requiredUInt32(params, jss::seq, "malformedRequest"); + if (!seq) + return Unexpected(seq.error()); + + return keylet::vault(*id, *seq).key; +} + +static Expected +parseContractData( + Json::Value const& params, + Json::StaticString const fieldName, + [[maybe_unused]] unsigned const apiVersion) +{ + if (!params.isObject()) + { + return parseObjectID(params, fieldName); + } + + auto const id = LedgerEntryHelpers::requiredAccountID(params, jss::owner, "malformedOwner"); + if (!id) + return Unexpected(id.error()); + + auto const seq = LedgerEntryHelpers::requiredUInt32(params, jss::seq, "malformedRequest"); + if (!seq) + return Unexpected(seq.error()); + + return keylet::vault(*id, *seq).key; +} + struct LedgerEntry { Json::StaticString fieldName; @@ -817,7 +883,7 @@ struct LedgerEntry Json::Value doLedgerEntry(RPC::JsonContext& context) { - static auto ledgerEntryParsers = std::to_array({ + static LedgerEntry ledgerEntryParsers[] = { #pragma push_macro("LEDGER_ENTRY") #undef LEDGER_ENTRY @@ -831,7 +897,7 @@ doLedgerEntry(RPC::JsonContext& context) // aliases {jss::account_root, parseAccountRoot, ltACCOUNT_ROOT}, {jss::ripple_state, parseRippleState, ltRIPPLE_STATE}, - }); + }; auto const hasMoreThanOneMember = [&]() { int count = 0; diff --git a/src/xrpld/rpc/handlers/server_info/ServerDefinitions.cpp b/src/xrpld/rpc/handlers/server_info/ServerDefinitions.cpp index f99f427ca8..7d34a6ddd3 100644 --- a/src/xrpld/rpc/handlers/server_info/ServerDefinitions.cpp +++ b/src/xrpld/rpc/handlers/server_info/ServerDefinitions.cpp @@ -79,6 +79,7 @@ ServerDefinitions::translate(std::string const& inp) {"PATHSET", "PathSet"}, {"VL", "Blob"}, {"XCHAIN_BRIDGE", "XChainBridge"}, + {"DATATYPE", "DataType"}, }; if (auto const& it = replacements.find(inp); it != replacements.end()) diff --git a/src/xrpld/rpc/handlers/subscribe/Subscribe.cpp b/src/xrpld/rpc/handlers/subscribe/Subscribe.cpp index af3e998a58..d96fb461bd 100644 --- a/src/xrpld/rpc/handlers/subscribe/Subscribe.cpp +++ b/src/xrpld/rpc/handlers/subscribe/Subscribe.cpp @@ -147,6 +147,10 @@ doSubscribe(RPC::JsonContext& context) { context.netOps.subConsensus(ispSub); } + else if (streamName == "contract_events") + { + context.netOps.subContractEvent(ispSub); + } else { return rpcError(rpcSTREAM_MALFORMED); diff --git a/src/xrpld/rpc/handlers/subscribe/Unsubscribe.cpp b/src/xrpld/rpc/handlers/subscribe/Unsubscribe.cpp index d3e36cc612..3745b3aa37 100644 --- a/src/xrpld/rpc/handlers/subscribe/Unsubscribe.cpp +++ b/src/xrpld/rpc/handlers/subscribe/Unsubscribe.cpp @@ -84,6 +84,10 @@ doUnsubscribe(RPC::JsonContext& context) { context.netOps.unsubConsensus(ispSub->getSeq()); } + else if (streamName == "contract_events") + { + context.netOps.unsubContractEvent(ispSub->getSeq()); + } else { return rpcError(rpcSTREAM_MALFORMED);