mirror of
https://github.com/XRPLF/rippled.git
synced 2025-11-21 19:45:53 +00:00
Merge branch 'develop' into bthomee/proto
This commit is contained in:
9
.github/workflows/clang-format.yml
vendored
9
.github/workflows/clang-format.yml
vendored
@@ -11,6 +11,15 @@ jobs:
|
||||
runs-on: ubuntu-24.04
|
||||
container: ghcr.io/xrplf/ci/tools-rippled-clang-format
|
||||
steps:
|
||||
# For jobs running in containers, $GITHUB_WORKSPACE and ${{ github.workspace }} might not be the
|
||||
# same directory. The actions/checkout step is *supposed* to checkout into $GITHUB_WORKSPACE and
|
||||
# then add it to safe.directory (see instructions at https://github.com/actions/checkout)
|
||||
# but that's apparently not happening for some container images. We can't be sure what is actually
|
||||
# happening, so let's pre-emptively add both directories to safe.directory. There's a
|
||||
# Github issue opened in 2022 and not resolved in 2025 https://github.com/actions/runner/issues/2058 ¯\_(ツ)_/¯
|
||||
- run: |
|
||||
git config --global --add safe.directory $GITHUB_WORKSPACE
|
||||
git config --global --add safe.directory ${{ github.workspace }}
|
||||
- uses: actions/checkout@v4
|
||||
- name: Format first-party sources
|
||||
run: |
|
||||
|
||||
2
.github/workflows/macos.yml
vendored
2
.github/workflows/macos.yml
vendored
@@ -24,8 +24,6 @@ env:
|
||||
CONAN_GLOBAL_CONF: |
|
||||
core.download:parallel={{os.cpu_count()}}
|
||||
core.upload:parallel={{os.cpu_count()}}
|
||||
core:default_build_profile=libxrpl
|
||||
core:default_profile=libxrpl
|
||||
tools.build:jobs={{ (os.cpu_count() * 4/5) | int }}
|
||||
tools.build:verbosity=verbose
|
||||
tools.compilation:verbosity=verbose
|
||||
|
||||
55
.github/workflows/nix.yml
vendored
55
.github/workflows/nix.yml
vendored
@@ -19,14 +19,12 @@ concurrency:
|
||||
# This part of Conan configuration is specific to this workflow only; we do not want
|
||||
# to pollute conan/profiles directory with settings which might not work for others
|
||||
env:
|
||||
#CONAN_URL: http://18.143.149.228:8081/artifactory/api/conan/dev
|
||||
CONAN_URL: http://18.143.149.228:8081/artifactory/api/conan/dev
|
||||
CONAN_LOGIN_USERNAME_RIPPLE: ${{ secrets.CONAN_USERNAME }}
|
||||
CONAN_PASSWORD_RIPPLE: ${{ secrets.CONAN_TOKEN }}
|
||||
CONAN_GLOBAL_CONF: |
|
||||
core.download:parallel={{ os.cpu_count() }}
|
||||
core.upload:parallel={{ os.cpu_count() }}
|
||||
core:default_build_profile=libxrpl
|
||||
core:default_profile=libxrpl
|
||||
tools.build:jobs={{ (os.cpu_count() * 4/5) | int }}
|
||||
tools.build:verbosity=verbose
|
||||
tools.compilation:verbosity=verbose
|
||||
@@ -359,37 +357,44 @@ jobs:
|
||||
cmake --build .
|
||||
./example | grep '^[[:digit:]]\+\.[[:digit:]]\+\.[[:digit:]]\+'
|
||||
|
||||
# NOTE we are not using dependencies built above because it lags with
|
||||
# compiler versions. Instrumentation requires clang version 16 or
|
||||
# later
|
||||
|
||||
instrumentation-build:
|
||||
if: ${{ github.event_name == 'push' || github.event.pull_request.draft != true || contains(github.event.pull_request.labels.*.name, 'DraftRunCI') }}
|
||||
needs: dependencies
|
||||
runs-on: [self-hosted, heavy]
|
||||
container: ghcr.io/xrplf/ci/debian-bookworm:clang-16
|
||||
|
||||
env:
|
||||
build_dir: .build
|
||||
steps:
|
||||
- name: download cache
|
||||
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093
|
||||
with:
|
||||
name: linux-clang-Debug
|
||||
|
||||
- name: extract cache
|
||||
run: |
|
||||
mkdir -p ${CONAN_HOME}
|
||||
tar -xzf conan.tar.gz -C ${CONAN_HOME}
|
||||
|
||||
- name: check environment
|
||||
run: |
|
||||
echo ${PATH} | tr ':' '\n'
|
||||
conan --version
|
||||
cmake --version
|
||||
env | sort
|
||||
ls ${CONAN_HOME}
|
||||
|
||||
- name: checkout
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
|
||||
|
||||
- name: dependencies
|
||||
uses: ./.github/actions/dependencies
|
||||
with:
|
||||
configuration: Debug
|
||||
|
||||
- name: prepare environment
|
||||
run: |
|
||||
mkdir ${GITHUB_WORKSPACE}/.build
|
||||
echo "SOURCE_DIR=$GITHUB_WORKSPACE" >> $GITHUB_ENV
|
||||
echo "BUILD_DIR=$GITHUB_WORKSPACE/.build" >> $GITHUB_ENV
|
||||
|
||||
- name: configure Conan
|
||||
run: |
|
||||
echo "${CONAN_GLOBAL_CONF}" >> $(conan config home)/global.conf
|
||||
conan config install conan/profiles/ -tf $(conan config home)/profiles/
|
||||
conan profile show
|
||||
- name: build dependencies
|
||||
run: |
|
||||
cd ${BUILD_DIR}
|
||||
conan install ${SOURCE_DIR} \
|
||||
--output-folder ${BUILD_DIR} \
|
||||
--build missing \
|
||||
--settings:all build_type=Debug
|
||||
mkdir -p ${build_dir}
|
||||
echo "SOURCE_DIR=$(pwd)" >> $GITHUB_ENV
|
||||
echo "BUILD_DIR=$(pwd)/${build_dir}" >> $GITHUB_ENV
|
||||
|
||||
- name: build with instrumentation
|
||||
run: |
|
||||
|
||||
2
.github/workflows/windows.yml
vendored
2
.github/workflows/windows.yml
vendored
@@ -27,8 +27,6 @@ env:
|
||||
CONAN_GLOBAL_CONF: |
|
||||
core.download:parallel={{os.cpu_count()}}
|
||||
core.upload:parallel={{os.cpu_count()}}
|
||||
core:default_build_profile=libxrpl
|
||||
core:default_profile=libxrpl
|
||||
tools.build:jobs=24
|
||||
tools.build:verbosity=verbose
|
||||
tools.compilation:verbosity=verbose
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
[settings]
|
||||
os={{ os }}
|
||||
arch={{ arch }}
|
||||
build_type=Debug
|
||||
compiler={{compiler}}
|
||||
compiler.version={{ compiler_version }}
|
||||
compiler.cppstd=20
|
||||
@@ -17,3 +18,20 @@ compiler.runtime=static
|
||||
{% else %}
|
||||
compiler.libcxx={{detect_api.detect_libcxx(compiler, version, compiler_exe)}}
|
||||
{% endif %}
|
||||
|
||||
[conf]
|
||||
{% if compiler == "clang" and compiler_version >= 19 %}
|
||||
tools.build:cxxflags=['-Wno-missing-template-arg-list-after-template-kw']
|
||||
{% endif %}
|
||||
{% if compiler == "apple-clang" and compiler_version >= 17 %}
|
||||
tools.build:cxxflags=['-Wno-missing-template-arg-list-after-template-kw']
|
||||
{% endif %}
|
||||
{% if compiler == "clang" and compiler_version == 16 %}
|
||||
tools.build:cxxflags=['-DBOOST_ASIO_DISABLE_CONCEPTS']
|
||||
{% endif %}
|
||||
{% if compiler == "gcc" and compiler_version < 13 %}
|
||||
tools.build:cxxflags=['-Wno-restrict']
|
||||
{% endif %}
|
||||
|
||||
[tool_requires]
|
||||
!cmake/*: cmake/[>=3 <4]
|
||||
11
conanfile.py
11
conanfile.py
@@ -55,6 +55,15 @@ class Xrpl(ConanFile):
|
||||
'date/*:header_only': True,
|
||||
'grpc/*:shared': False,
|
||||
'grpc/*:secure': True,
|
||||
'grpc/*:codegen': False,
|
||||
'grpc/*:csharp_ext': False,
|
||||
'grpc/*:csharp_plugin': False,
|
||||
'grpc/*:node_plugin': False,
|
||||
'grpc/*:objective_c_plugin': False,
|
||||
'grpc/*:php_plugin': False,
|
||||
'grpc/*:python_plugin': False,
|
||||
'grpc/*:ruby_plugin': False,
|
||||
'grpc/*:otel_plugin': False,
|
||||
'libarchive/*:shared': False,
|
||||
'libarchive/*:with_acl': False,
|
||||
'libarchive/*:with_bzip2': False,
|
||||
@@ -143,8 +152,6 @@ class Xrpl(ConanFile):
|
||||
tc.variables['static'] = self.options.static
|
||||
tc.variables['unity'] = self.options.unity
|
||||
tc.variables['xrpld'] = self.options.xrpld
|
||||
if self.settings.compiler == 'clang' and self.settings.compiler.version == 16:
|
||||
tc.extra_cxxflags = ["-DBOOST_ASIO_DISABLE_CONCEPTS"]
|
||||
tc.generate()
|
||||
|
||||
def build(self):
|
||||
|
||||
@@ -31,38 +31,28 @@ namespace beast {
|
||||
|
||||
template <class Generator>
|
||||
void
|
||||
rngfill(void* buffer, std::size_t bytes, Generator& g)
|
||||
rngfill(void* const buffer, std::size_t const bytes, Generator& g)
|
||||
{
|
||||
using result_type = typename Generator::result_type;
|
||||
constexpr std::size_t result_size = sizeof(result_type);
|
||||
|
||||
while (bytes >= sizeof(result_type))
|
||||
std::uint8_t* const buffer_start = static_cast<std::uint8_t*>(buffer);
|
||||
std::size_t const complete_iterations = bytes / result_size;
|
||||
std::size_t const bytes_remaining = bytes % result_size;
|
||||
|
||||
for (std::size_t count = 0; count < complete_iterations; ++count)
|
||||
{
|
||||
auto const v = g();
|
||||
std::memcpy(buffer, &v, sizeof(v));
|
||||
buffer = reinterpret_cast<std::uint8_t*>(buffer) + sizeof(v);
|
||||
bytes -= sizeof(v);
|
||||
result_type const v = g();
|
||||
std::size_t const offset = count * result_size;
|
||||
std::memcpy(buffer_start + offset, &v, result_size);
|
||||
}
|
||||
|
||||
XRPL_ASSERT(
|
||||
bytes < sizeof(result_type), "beast::rngfill(void*) : maximum bytes");
|
||||
|
||||
#ifdef __GNUC__
|
||||
// gcc 11.1 (falsely) warns about an array-bounds overflow in release mode.
|
||||
// gcc 12.1 (also falsely) warns about an string overflow in release mode.
|
||||
#pragma GCC diagnostic push
|
||||
#pragma GCC diagnostic ignored "-Warray-bounds"
|
||||
#pragma GCC diagnostic ignored "-Wstringop-overflow"
|
||||
#endif
|
||||
|
||||
if (bytes > 0)
|
||||
if (bytes_remaining > 0)
|
||||
{
|
||||
auto const v = g();
|
||||
std::memcpy(buffer, &v, bytes);
|
||||
result_type const v = g();
|
||||
std::size_t const offset = complete_iterations * result_size;
|
||||
std::memcpy(buffer_start + offset, &v, bytes_remaining);
|
||||
}
|
||||
|
||||
#ifdef __GNUC__
|
||||
#pragma GCC diagnostic pop
|
||||
#endif
|
||||
}
|
||||
|
||||
template <
|
||||
|
||||
@@ -53,19 +53,19 @@
|
||||
* then change the macro parameter in features.macro to
|
||||
* `VoteBehavior::DefaultYes`. The communication process is beyond
|
||||
* the scope of these instructions.
|
||||
|
||||
* 5) If a supported feature (`Supported::yes`) was _ever_ in a released
|
||||
* version, it can never be changed back to `Supported::no`, because
|
||||
* it _may_ still become enabled at any time. This would cause newer
|
||||
* versions of `rippled` to become amendment blocked.
|
||||
* Instead, to prevent newer versions from voting on the feature, use
|
||||
* `VoteBehavior::Obsolete`. Obsolete features can not be voted for
|
||||
* by any versions of `rippled` built with that setting, but will still
|
||||
* work correctly if they get enabled. If a feature remains obsolete
|
||||
* for long enough that _all_ clients that could vote for it are
|
||||
* amendment blocked, the feature can be removed from the code
|
||||
* as if it was unsupported.
|
||||
*
|
||||
* 5) A feature marked as Obsolete can mean either:
|
||||
* 1) It is in the ledger (marked as Supported::yes) and it is on its way to
|
||||
* become Retired
|
||||
* 2) The feature is not in the ledger (has always been marked as
|
||||
* Supported::no) and the code to support it has been removed
|
||||
*
|
||||
* If we want to discontinue a feature that we've never fully supported and
|
||||
* the feature has never been enabled, we should remove all the related
|
||||
* code, and mark the feature as "abandoned". To do this:
|
||||
*
|
||||
* 1) Open features.macro, move the feature to the abandoned section and
|
||||
* change the macro to XRPL_ABANDON
|
||||
*
|
||||
* When a feature has been enabled for several years, the conditional code
|
||||
* may be removed, and the feature "retired". To retire a feature:
|
||||
@@ -99,13 +99,10 @@ namespace detail {
|
||||
#undef XRPL_FIX
|
||||
#pragma push_macro("XRPL_RETIRE")
|
||||
#undef XRPL_RETIRE
|
||||
#pragma push_macro("XRPL_ABANDON")
|
||||
#undef XRPL_ABANDON
|
||||
|
||||
#define XRPL_FEATURE(name, supported, vote) +1
|
||||
#define XRPL_FIX(name, supported, vote) +1
|
||||
#define XRPL_RETIRE(name) +1
|
||||
#define XRPL_ABANDON(name) +1
|
||||
|
||||
// This value SHOULD be equal to the number of amendments registered in
|
||||
// Feature.cpp. Because it's only used to reserve storage, and determine how
|
||||
@@ -122,8 +119,6 @@ static constexpr std::size_t numFeatures =
|
||||
#pragma pop_macro("XRPL_FIX")
|
||||
#undef XRPL_FEATURE
|
||||
#pragma pop_macro("XRPL_FEATURE")
|
||||
#undef XRPL_ABANDON
|
||||
#pragma pop_macro("XRPL_ABANDON")
|
||||
|
||||
/** Amendments that this server supports and the default voting behavior.
|
||||
Whether they are enabled depends on the Rules defined in the validated
|
||||
@@ -365,13 +360,10 @@ foreachFeature(FeatureBitset bs, F&& f)
|
||||
#undef XRPL_FIX
|
||||
#pragma push_macro("XRPL_RETIRE")
|
||||
#undef XRPL_RETIRE
|
||||
#pragma push_macro("XRPL_ABANDON")
|
||||
#undef XRPL_ABANDON
|
||||
|
||||
#define XRPL_FEATURE(name, supported, vote) extern uint256 const feature##name;
|
||||
#define XRPL_FIX(name, supported, vote) extern uint256 const fix##name;
|
||||
#define XRPL_RETIRE(name)
|
||||
#define XRPL_ABANDON(name)
|
||||
|
||||
#include <xrpl/protocol/detail/features.macro>
|
||||
|
||||
@@ -381,8 +373,6 @@ foreachFeature(FeatureBitset bs, F&& f)
|
||||
#pragma pop_macro("XRPL_FIX")
|
||||
#undef XRPL_FEATURE
|
||||
#pragma pop_macro("XRPL_FEATURE")
|
||||
#undef XRPL_ABANDON
|
||||
#pragma pop_macro("XRPL_ABANDON")
|
||||
|
||||
} // namespace ripple
|
||||
|
||||
|
||||
@@ -26,9 +26,6 @@
|
||||
#if !defined(XRPL_RETIRE)
|
||||
#error "undefined macro: XRPL_RETIRE"
|
||||
#endif
|
||||
#if !defined(XRPL_ABANDON)
|
||||
#error "undefined macro: XRPL_ABANDON"
|
||||
#endif
|
||||
|
||||
// Add new amendments to the top of this list.
|
||||
// Keep it sorted in reverse chronological order.
|
||||
@@ -133,11 +130,6 @@ XRPL_FIX (NFTokenDirV1, Supported::yes, VoteBehavior::Obsolete)
|
||||
XRPL_FEATURE(NonFungibleTokensV1, Supported::yes, VoteBehavior::Obsolete)
|
||||
XRPL_FEATURE(CryptoConditionsSuite, Supported::yes, VoteBehavior::Obsolete)
|
||||
|
||||
// The following amendments were never supported, never enabled, and
|
||||
// we've abanded them. These features should never be in the ledger,
|
||||
// and we've removed all the related code.
|
||||
XRPL_ABANDON(OwnerPaysFee)
|
||||
|
||||
// The following amendments have been active for at least two years. Their
|
||||
// pre-amendment code has been removed and the identifiers are deprecated.
|
||||
// All known amendments and amendments that may appear in a validated
|
||||
|
||||
@@ -482,8 +482,7 @@ LEDGER_ENTRY(ltDELEGATE, 0x0083, Delegate, delegate, ({
|
||||
}))
|
||||
|
||||
/** A ledger object representing a single asset vault.
|
||||
|
||||
\sa keylet::mptoken
|
||||
\sa keylet::vault
|
||||
*/
|
||||
LEDGER_ENTRY(ltVAULT, 0x0084, Vault, vault, ({
|
||||
{sfPreviousTxnID, soeREQUIRED},
|
||||
|
||||
@@ -409,6 +409,7 @@ TRANSACTION(ttMPTOKEN_ISSUANCE_CREATE, 54, MPTokenIssuanceCreate, Delegation::de
|
||||
{sfTransferFee, soeOPTIONAL},
|
||||
{sfMaximumAmount, soeOPTIONAL},
|
||||
{sfMPTokenMetadata, soeOPTIONAL},
|
||||
{sfDomainID, soeOPTIONAL},
|
||||
}))
|
||||
|
||||
/** This transaction type destroys a MPTokensIssuance instance */
|
||||
@@ -420,6 +421,7 @@ TRANSACTION(ttMPTOKEN_ISSUANCE_DESTROY, 55, MPTokenIssuanceDestroy, Delegation::
|
||||
TRANSACTION(ttMPTOKEN_ISSUANCE_SET, 56, MPTokenIssuanceSet, Delegation::delegatable, ({
|
||||
{sfMPTokenIssuanceID, soeREQUIRED},
|
||||
{sfHolder, soeOPTIONAL},
|
||||
{sfDomainID, soeOPTIONAL},
|
||||
}))
|
||||
|
||||
/** This transaction type authorizes a MPToken instance */
|
||||
@@ -478,7 +480,7 @@ TRANSACTION(ttVAULT_CREATE, 65, VaultCreate, Delegation::delegatable, ({
|
||||
{sfAsset, soeREQUIRED, soeMPTSupported},
|
||||
{sfAssetsMaximum, soeOPTIONAL},
|
||||
{sfMPTokenMetadata, soeOPTIONAL},
|
||||
{sfDomainID, soeOPTIONAL}, // PermissionedDomainID
|
||||
{sfDomainID, soeOPTIONAL},
|
||||
{sfWithdrawalPolicy, soeOPTIONAL},
|
||||
{sfData, soeOPTIONAL},
|
||||
}))
|
||||
@@ -487,7 +489,7 @@ TRANSACTION(ttVAULT_CREATE, 65, VaultCreate, Delegation::delegatable, ({
|
||||
TRANSACTION(ttVAULT_SET, 66, VaultSet, Delegation::delegatable, ({
|
||||
{sfVaultID, soeREQUIRED},
|
||||
{sfAssetsMaximum, soeOPTIONAL},
|
||||
{sfDomainID, soeOPTIONAL}, // PermissionedDomainID
|
||||
{sfDomainID, soeOPTIONAL},
|
||||
{sfData, soeOPTIONAL},
|
||||
}))
|
||||
|
||||
|
||||
@@ -254,7 +254,7 @@ FeatureCollections::registerFeature(
|
||||
{
|
||||
check(!readOnly, "Attempting to register a feature after startup.");
|
||||
check(
|
||||
support == Supported::yes || vote != VoteBehavior::DefaultYes,
|
||||
support == Supported::yes || vote == VoteBehavior::DefaultNo,
|
||||
"Invalid feature parameters. Must be supported to be up-voted.");
|
||||
Feature const* i = getByName(name);
|
||||
if (!i)
|
||||
@@ -268,7 +268,7 @@ FeatureCollections::registerFeature(
|
||||
features.emplace_back(name, f);
|
||||
|
||||
auto const getAmendmentSupport = [=]() {
|
||||
if (vote == VoteBehavior::Obsolete && support == Supported::yes)
|
||||
if (vote == VoteBehavior::Obsolete)
|
||||
return AmendmentSupport::Retired;
|
||||
return support == Supported::yes ? AmendmentSupport::Supported
|
||||
: AmendmentSupport::Unsupported;
|
||||
@@ -398,14 +398,6 @@ retireFeature(std::string const& name)
|
||||
return registerFeature(name, Supported::yes, VoteBehavior::Obsolete);
|
||||
}
|
||||
|
||||
// Abandoned features are not in the ledger and have no code controlled by the
|
||||
// feature. They were never supported, and cannot be voted on.
|
||||
uint256
|
||||
abandonFeature(std::string const& name)
|
||||
{
|
||||
return registerFeature(name, Supported::no, VoteBehavior::Obsolete);
|
||||
}
|
||||
|
||||
/** Tell FeatureCollections when registration is complete. */
|
||||
bool
|
||||
registrationIsDone()
|
||||
@@ -440,8 +432,6 @@ featureToName(uint256 const& f)
|
||||
#undef XRPL_FIX
|
||||
#pragma push_macro("XRPL_RETIRE")
|
||||
#undef XRPL_RETIRE
|
||||
#pragma push_macro("XRPL_ABANDON")
|
||||
#undef XRPL_ABANDON
|
||||
|
||||
#define XRPL_FEATURE(name, supported, vote) \
|
||||
uint256 const feature##name = registerFeature(#name, supported, vote);
|
||||
@@ -453,11 +443,6 @@ featureToName(uint256 const& f)
|
||||
[[deprecated("The referenced amendment has been retired")]] \
|
||||
[[maybe_unused]] \
|
||||
uint256 const retired##name = retireFeature(#name);
|
||||
|
||||
#define XRPL_ABANDON(name) \
|
||||
[[deprecated("The referenced amendment has been abandoned")]] \
|
||||
[[maybe_unused]] \
|
||||
uint256 const abandoned##name = abandonFeature(#name);
|
||||
// clang-format on
|
||||
|
||||
#include <xrpl/protocol/detail/features.macro>
|
||||
@@ -468,8 +453,6 @@ featureToName(uint256 const& f)
|
||||
#pragma pop_macro("XRPL_FIX")
|
||||
#undef XRPL_FEATURE
|
||||
#pragma pop_macro("XRPL_FEATURE")
|
||||
#undef XRPL_ABANDON
|
||||
#pragma pop_macro("XRPL_ABANDON")
|
||||
|
||||
// All of the features should now be registered, since variables in a cpp file
|
||||
// are initialized from top to bottom.
|
||||
|
||||
@@ -3652,14 +3652,18 @@ class Batch_test : public beast::unit_test::suite
|
||||
{
|
||||
// Submit a tx with tfInnerBatchTxn
|
||||
uint256 const txBad = submitTx(tfInnerBatchTxn);
|
||||
BEAST_EXPECT(env.app().getHashRouter().getFlags(txBad) == 0);
|
||||
BEAST_EXPECT(
|
||||
env.app().getHashRouter().getFlags(txBad) ==
|
||||
HashRouterFlags::UNDEFINED);
|
||||
}
|
||||
|
||||
// Validate: NetworkOPs::processTransaction()
|
||||
{
|
||||
uint256 const txid = processTxn(tfInnerBatchTxn);
|
||||
// HashRouter::getFlags() should return SF_BAD
|
||||
BEAST_EXPECT(env.app().getHashRouter().getFlags(txid) == SF_BAD);
|
||||
// HashRouter::getFlags() should return LedgerFlags::BAD
|
||||
BEAST_EXPECT(
|
||||
env.app().getHashRouter().getFlags(txid) ==
|
||||
HashRouterFlags::BAD);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -45,15 +45,19 @@ class HashRouter_test : public beast::unit_test::suite
|
||||
TestStopwatch stopwatch;
|
||||
HashRouter router(getSetup(2s, 1s), stopwatch);
|
||||
|
||||
uint256 const key1(1);
|
||||
uint256 const key2(2);
|
||||
uint256 const key3(3);
|
||||
HashRouterFlags key1(HashRouterFlags::PRIVATE1);
|
||||
HashRouterFlags key2(HashRouterFlags::PRIVATE2);
|
||||
HashRouterFlags key3(HashRouterFlags::PRIVATE3);
|
||||
|
||||
auto const ukey1 = uint256{static_cast<std::uint64_t>(key1)};
|
||||
auto const ukey2 = uint256{static_cast<std::uint64_t>(key2)};
|
||||
auto const ukey3 = uint256{static_cast<std::uint64_t>(key3)};
|
||||
|
||||
// t=0
|
||||
router.setFlags(key1, 11111);
|
||||
BEAST_EXPECT(router.getFlags(key1) == 11111);
|
||||
router.setFlags(key2, 22222);
|
||||
BEAST_EXPECT(router.getFlags(key2) == 22222);
|
||||
router.setFlags(ukey1, HashRouterFlags::PRIVATE1);
|
||||
BEAST_EXPECT(router.getFlags(ukey1) == HashRouterFlags::PRIVATE1);
|
||||
router.setFlags(ukey2, HashRouterFlags::PRIVATE2);
|
||||
BEAST_EXPECT(router.getFlags(ukey2) == HashRouterFlags::PRIVATE2);
|
||||
// key1 : 0
|
||||
// key2 : 0
|
||||
// key3: null
|
||||
@@ -62,7 +66,7 @@ class HashRouter_test : public beast::unit_test::suite
|
||||
|
||||
// Because we are accessing key1 here, it
|
||||
// will NOT be expired for another two ticks
|
||||
BEAST_EXPECT(router.getFlags(key1) == 11111);
|
||||
BEAST_EXPECT(router.getFlags(ukey1) == HashRouterFlags::PRIVATE1);
|
||||
// key1 : 1
|
||||
// key2 : 0
|
||||
// key3 null
|
||||
@@ -70,9 +74,9 @@ class HashRouter_test : public beast::unit_test::suite
|
||||
++stopwatch;
|
||||
|
||||
// t=3
|
||||
router.setFlags(key3, 33333); // force expiration
|
||||
BEAST_EXPECT(router.getFlags(key1) == 11111);
|
||||
BEAST_EXPECT(router.getFlags(key2) == 0);
|
||||
router.setFlags(ukey3, HashRouterFlags::PRIVATE3); // force expiration
|
||||
BEAST_EXPECT(router.getFlags(ukey1) == HashRouterFlags::PRIVATE1);
|
||||
BEAST_EXPECT(router.getFlags(ukey2) == HashRouterFlags::UNDEFINED);
|
||||
}
|
||||
|
||||
void
|
||||
@@ -83,15 +87,21 @@ class HashRouter_test : public beast::unit_test::suite
|
||||
TestStopwatch stopwatch;
|
||||
HashRouter router(getSetup(2s, 1s), stopwatch);
|
||||
|
||||
uint256 const key1(1);
|
||||
uint256 const key2(2);
|
||||
uint256 const key3(3);
|
||||
uint256 const key4(4);
|
||||
HashRouterFlags key1(HashRouterFlags::PRIVATE1);
|
||||
HashRouterFlags key2(HashRouterFlags::PRIVATE2);
|
||||
HashRouterFlags key3(HashRouterFlags::PRIVATE3);
|
||||
HashRouterFlags key4(HashRouterFlags::PRIVATE4);
|
||||
|
||||
auto const ukey1 = uint256{static_cast<std::uint64_t>(key1)};
|
||||
auto const ukey2 = uint256{static_cast<std::uint64_t>(key2)};
|
||||
auto const ukey3 = uint256{static_cast<std::uint64_t>(key3)};
|
||||
auto const ukey4 = uint256{static_cast<std::uint64_t>(key4)};
|
||||
|
||||
BEAST_EXPECT(key1 != key2 && key2 != key3 && key3 != key4);
|
||||
|
||||
// t=0
|
||||
router.setFlags(key1, 12345);
|
||||
BEAST_EXPECT(router.getFlags(key1) == 12345);
|
||||
router.setFlags(ukey1, HashRouterFlags::BAD);
|
||||
BEAST_EXPECT(router.getFlags(ukey1) == HashRouterFlags::BAD);
|
||||
// key1 : 0
|
||||
// key2 : null
|
||||
// key3 : null
|
||||
@@ -103,26 +113,27 @@ class HashRouter_test : public beast::unit_test::suite
|
||||
// so key1 will be expired after the second
|
||||
// call to setFlags.
|
||||
// t=1
|
||||
router.setFlags(key2, 9999);
|
||||
BEAST_EXPECT(router.getFlags(key1) == 12345);
|
||||
BEAST_EXPECT(router.getFlags(key2) == 9999);
|
||||
|
||||
router.setFlags(ukey2, HashRouterFlags::PRIVATE5);
|
||||
BEAST_EXPECT(router.getFlags(ukey1) == HashRouterFlags::BAD);
|
||||
BEAST_EXPECT(router.getFlags(ukey2) == HashRouterFlags::PRIVATE5);
|
||||
// key1 : 1
|
||||
// key2 : 1
|
||||
// key3 : null
|
||||
|
||||
++stopwatch;
|
||||
// t=2
|
||||
BEAST_EXPECT(router.getFlags(key2) == 9999);
|
||||
BEAST_EXPECT(router.getFlags(ukey2) == HashRouterFlags::PRIVATE5);
|
||||
// key1 : 1
|
||||
// key2 : 2
|
||||
// key3 : null
|
||||
|
||||
++stopwatch;
|
||||
// t=3
|
||||
router.setFlags(key3, 2222);
|
||||
BEAST_EXPECT(router.getFlags(key1) == 0);
|
||||
BEAST_EXPECT(router.getFlags(key2) == 9999);
|
||||
BEAST_EXPECT(router.getFlags(key3) == 2222);
|
||||
router.setFlags(ukey3, HashRouterFlags::BAD);
|
||||
BEAST_EXPECT(router.getFlags(ukey1) == HashRouterFlags::UNDEFINED);
|
||||
BEAST_EXPECT(router.getFlags(ukey2) == HashRouterFlags::PRIVATE5);
|
||||
BEAST_EXPECT(router.getFlags(ukey3) == HashRouterFlags::BAD);
|
||||
// key1 : 3
|
||||
// key2 : 3
|
||||
// key3 : 3
|
||||
@@ -130,10 +141,10 @@ class HashRouter_test : public beast::unit_test::suite
|
||||
++stopwatch;
|
||||
// t=4
|
||||
// No insertion, no expiration
|
||||
router.setFlags(key1, 7654);
|
||||
BEAST_EXPECT(router.getFlags(key1) == 7654);
|
||||
BEAST_EXPECT(router.getFlags(key2) == 9999);
|
||||
BEAST_EXPECT(router.getFlags(key3) == 2222);
|
||||
router.setFlags(ukey1, HashRouterFlags::SAVED);
|
||||
BEAST_EXPECT(router.getFlags(ukey1) == HashRouterFlags::SAVED);
|
||||
BEAST_EXPECT(router.getFlags(ukey2) == HashRouterFlags::PRIVATE5);
|
||||
BEAST_EXPECT(router.getFlags(ukey3) == HashRouterFlags::BAD);
|
||||
// key1 : 4
|
||||
// key2 : 4
|
||||
// key3 : 4
|
||||
@@ -142,11 +153,11 @@ class HashRouter_test : public beast::unit_test::suite
|
||||
++stopwatch;
|
||||
|
||||
// t=6
|
||||
router.setFlags(key4, 7890);
|
||||
BEAST_EXPECT(router.getFlags(key1) == 0);
|
||||
BEAST_EXPECT(router.getFlags(key2) == 0);
|
||||
BEAST_EXPECT(router.getFlags(key3) == 0);
|
||||
BEAST_EXPECT(router.getFlags(key4) == 7890);
|
||||
router.setFlags(ukey4, HashRouterFlags::TRUSTED);
|
||||
BEAST_EXPECT(router.getFlags(ukey1) == HashRouterFlags::UNDEFINED);
|
||||
BEAST_EXPECT(router.getFlags(ukey2) == HashRouterFlags::UNDEFINED);
|
||||
BEAST_EXPECT(router.getFlags(ukey3) == HashRouterFlags::UNDEFINED);
|
||||
BEAST_EXPECT(router.getFlags(ukey4) == HashRouterFlags::TRUSTED);
|
||||
// key1 : 6
|
||||
// key2 : 6
|
||||
// key3 : 6
|
||||
@@ -168,18 +179,18 @@ class HashRouter_test : public beast::unit_test::suite
|
||||
uint256 const key4(4);
|
||||
BEAST_EXPECT(key1 != key2 && key2 != key3 && key3 != key4);
|
||||
|
||||
int flags = 12345; // This value is ignored
|
||||
HashRouterFlags flags(HashRouterFlags::BAD); // This value is ignored
|
||||
router.addSuppression(key1);
|
||||
BEAST_EXPECT(router.addSuppressionPeer(key2, 15));
|
||||
BEAST_EXPECT(router.addSuppressionPeer(key3, 20, flags));
|
||||
BEAST_EXPECT(flags == 0);
|
||||
BEAST_EXPECT(flags == HashRouterFlags::UNDEFINED);
|
||||
|
||||
++stopwatch;
|
||||
|
||||
BEAST_EXPECT(!router.addSuppressionPeer(key1, 2));
|
||||
BEAST_EXPECT(!router.addSuppressionPeer(key2, 3));
|
||||
BEAST_EXPECT(!router.addSuppressionPeer(key3, 4, flags));
|
||||
BEAST_EXPECT(flags == 0);
|
||||
BEAST_EXPECT(flags == HashRouterFlags::UNDEFINED);
|
||||
BEAST_EXPECT(router.addSuppressionPeer(key4, 5));
|
||||
}
|
||||
|
||||
@@ -192,9 +203,9 @@ class HashRouter_test : public beast::unit_test::suite
|
||||
HashRouter router(getSetup(2s, 1s), stopwatch);
|
||||
|
||||
uint256 const key1(1);
|
||||
BEAST_EXPECT(router.setFlags(key1, 10));
|
||||
BEAST_EXPECT(!router.setFlags(key1, 10));
|
||||
BEAST_EXPECT(router.setFlags(key1, 20));
|
||||
BEAST_EXPECT(router.setFlags(key1, HashRouterFlags::PRIVATE1));
|
||||
BEAST_EXPECT(!router.setFlags(key1, HashRouterFlags::PRIVATE1));
|
||||
BEAST_EXPECT(router.setFlags(key1, HashRouterFlags::PRIVATE2));
|
||||
}
|
||||
|
||||
void
|
||||
@@ -250,7 +261,7 @@ class HashRouter_test : public beast::unit_test::suite
|
||||
HashRouter router(getSetup(5s, 1s), stopwatch);
|
||||
uint256 const key(1);
|
||||
HashRouter::PeerShortID peer = 1;
|
||||
int flags;
|
||||
HashRouterFlags flags;
|
||||
|
||||
BEAST_EXPECT(router.shouldProcess(key, peer, flags, 1s));
|
||||
BEAST_EXPECT(!router.shouldProcess(key, peer, flags, 1s));
|
||||
@@ -364,6 +375,39 @@ class HashRouter_test : public beast::unit_test::suite
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
testFlagsOps()
|
||||
{
|
||||
testcase("Bitwise Operations");
|
||||
|
||||
using HF = HashRouterFlags;
|
||||
using UHF = std::underlying_type_t<HF>;
|
||||
|
||||
HF f1 = HF::BAD;
|
||||
HF f2 = HF::SAVED;
|
||||
HF combined = f1 | f2;
|
||||
|
||||
BEAST_EXPECT(
|
||||
static_cast<UHF>(combined) ==
|
||||
(static_cast<UHF>(f1) | static_cast<UHF>(f2)));
|
||||
|
||||
HF temp = f1;
|
||||
temp |= f2;
|
||||
BEAST_EXPECT(temp == combined);
|
||||
|
||||
HF intersect = combined & f1;
|
||||
BEAST_EXPECT(intersect == f1);
|
||||
|
||||
HF temp2 = combined;
|
||||
temp2 &= f1;
|
||||
BEAST_EXPECT(temp2 == f1);
|
||||
|
||||
BEAST_EXPECT(any(f1));
|
||||
BEAST_EXPECT(any(f2));
|
||||
BEAST_EXPECT(any(combined));
|
||||
BEAST_EXPECT(!any(HF::UNDEFINED));
|
||||
}
|
||||
|
||||
public:
|
||||
void
|
||||
run() override
|
||||
@@ -375,6 +419,7 @@ public:
|
||||
testRelay();
|
||||
testProcess();
|
||||
testSetup();
|
||||
testFlagsOps();
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -18,10 +18,16 @@
|
||||
//==============================================================================
|
||||
|
||||
#include <test/jtx.h>
|
||||
#include <test/jtx/credentials.h>
|
||||
#include <test/jtx/permissioned_domains.h>
|
||||
#include <test/jtx/trust.h>
|
||||
#include <test/jtx/xchain_bridge.h>
|
||||
|
||||
#include <xrpl/basics/base_uint.h>
|
||||
#include <xrpl/beast/utility/Zero.h>
|
||||
#include <xrpl/protocol/Feature.h>
|
||||
#include <xrpl/protocol/TER.h>
|
||||
#include <xrpl/protocol/TxFlags.h>
|
||||
#include <xrpl/protocol/jss.h>
|
||||
|
||||
namespace ripple {
|
||||
@@ -61,6 +67,48 @@ class MPToken_test : public beast::unit_test::suite
|
||||
.metadata = "test",
|
||||
.err = temMALFORMED});
|
||||
|
||||
if (!features[featureSingleAssetVault])
|
||||
{
|
||||
// tries to set DomainID when SAV is disabled
|
||||
mptAlice.create(
|
||||
{.maxAmt = 100,
|
||||
.assetScale = 0,
|
||||
.metadata = "test",
|
||||
.flags = tfMPTRequireAuth,
|
||||
.domainID = uint256(42),
|
||||
.err = temDISABLED});
|
||||
}
|
||||
else if (!features[featurePermissionedDomains])
|
||||
{
|
||||
// tries to set DomainID when PD is disabled
|
||||
mptAlice.create(
|
||||
{.maxAmt = 100,
|
||||
.assetScale = 0,
|
||||
.metadata = "test",
|
||||
.flags = tfMPTRequireAuth,
|
||||
.domainID = uint256(42),
|
||||
.err = temDISABLED});
|
||||
}
|
||||
else
|
||||
{
|
||||
// tries to set DomainID when RequireAuth is not set
|
||||
mptAlice.create(
|
||||
{.maxAmt = 100,
|
||||
.assetScale = 0,
|
||||
.metadata = "test",
|
||||
.domainID = uint256(42),
|
||||
.err = temMALFORMED});
|
||||
|
||||
// tries to set zero DomainID
|
||||
mptAlice.create(
|
||||
{.maxAmt = 100,
|
||||
.assetScale = 0,
|
||||
.metadata = "test",
|
||||
.flags = tfMPTRequireAuth,
|
||||
.domainID = beast::zero,
|
||||
.err = temMALFORMED});
|
||||
}
|
||||
|
||||
// tries to set a txfee greater than max
|
||||
mptAlice.create(
|
||||
{.maxAmt = 100,
|
||||
@@ -140,6 +188,48 @@ class MPToken_test : public beast::unit_test::suite
|
||||
BEAST_EXPECT(
|
||||
result[sfMaximumAmount.getJsonName()] == "9223372036854775807");
|
||||
}
|
||||
|
||||
if (features[featureSingleAssetVault])
|
||||
{
|
||||
// Add permissioned domain
|
||||
Account const credIssuer1{"credIssuer1"};
|
||||
std::string const credType = "credential";
|
||||
|
||||
pdomain::Credentials const credentials1{
|
||||
{.issuer = credIssuer1, .credType = credType}};
|
||||
|
||||
{
|
||||
Env env{*this, features};
|
||||
env.fund(XRP(1000), credIssuer1);
|
||||
|
||||
env(pdomain::setTx(credIssuer1, credentials1));
|
||||
auto const domainId1 = [&]() {
|
||||
auto tx = env.tx()->getJson(JsonOptions::none);
|
||||
return pdomain::getNewDomain(env.meta());
|
||||
}();
|
||||
|
||||
MPTTester mptAlice(env, alice);
|
||||
mptAlice.create({
|
||||
.maxAmt = maxMPTokenAmount, // 9'223'372'036'854'775'807
|
||||
.assetScale = 1,
|
||||
.transferFee = 10,
|
||||
.metadata = "123",
|
||||
.ownerCount = 1,
|
||||
.flags = tfMPTCanLock | tfMPTRequireAuth | tfMPTCanEscrow |
|
||||
tfMPTCanTrade | tfMPTCanTransfer | tfMPTCanClawback,
|
||||
.domainID = domainId1,
|
||||
});
|
||||
|
||||
// Get the hash for the most recent transaction.
|
||||
std::string const txHash{
|
||||
env.tx()->getJson(JsonOptions::none)[jss::hash].asString()};
|
||||
|
||||
Json::Value const result = env.rpc("tx", txHash)[jss::result];
|
||||
BEAST_EXPECT(
|
||||
result[sfMaximumAmount.getJsonName()] ==
|
||||
"9223372036854775807");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
@@ -499,6 +589,59 @@ class MPToken_test : public beast::unit_test::suite
|
||||
.flags = 0x00000008,
|
||||
.err = temINVALID_FLAG});
|
||||
|
||||
if (!features[featureSingleAssetVault])
|
||||
{
|
||||
// test invalid flags - nothing is being changed
|
||||
mptAlice.set(
|
||||
{.account = alice,
|
||||
.flags = 0x00000000,
|
||||
.err = tecNO_PERMISSION});
|
||||
|
||||
mptAlice.set(
|
||||
{.account = alice,
|
||||
.holder = bob,
|
||||
.flags = 0x00000000,
|
||||
.err = tecNO_PERMISSION});
|
||||
|
||||
// cannot set DomainID since SAV is not enabled
|
||||
mptAlice.set(
|
||||
{.account = alice,
|
||||
.domainID = uint256(42),
|
||||
.err = temDISABLED});
|
||||
}
|
||||
else
|
||||
{
|
||||
// test invalid flags - nothing is being changed
|
||||
mptAlice.set(
|
||||
{.account = alice,
|
||||
.flags = 0x00000000,
|
||||
.err = temMALFORMED});
|
||||
|
||||
mptAlice.set(
|
||||
{.account = alice,
|
||||
.holder = bob,
|
||||
.flags = 0x00000000,
|
||||
.err = temMALFORMED});
|
||||
|
||||
if (!features[featurePermissionedDomains])
|
||||
{
|
||||
// cannot set DomainID since PD is not enabled
|
||||
mptAlice.set(
|
||||
{.account = alice,
|
||||
.domainID = uint256(42),
|
||||
.err = temDISABLED});
|
||||
}
|
||||
else
|
||||
{
|
||||
// cannot set DomainID since Holder is set
|
||||
mptAlice.set(
|
||||
{.account = alice,
|
||||
.holder = bob,
|
||||
.domainID = uint256(42),
|
||||
.err = temMALFORMED});
|
||||
}
|
||||
}
|
||||
|
||||
// set both lock and unlock flags at the same time will fail
|
||||
mptAlice.set(
|
||||
{.account = alice,
|
||||
@@ -582,6 +725,53 @@ class MPToken_test : public beast::unit_test::suite
|
||||
mptAlice.set(
|
||||
{.holder = cindy, .flags = tfMPTLock, .err = tecNO_DST});
|
||||
}
|
||||
|
||||
if (features[featureSingleAssetVault] &&
|
||||
features[featurePermissionedDomains])
|
||||
{
|
||||
// Add permissioned domain
|
||||
Account const credIssuer1{"credIssuer1"};
|
||||
std::string const credType = "credential";
|
||||
|
||||
pdomain::Credentials const credentials1{
|
||||
{.issuer = credIssuer1, .credType = credType}};
|
||||
|
||||
{
|
||||
Env env{*this, features};
|
||||
|
||||
MPTTester mptAlice(env, alice);
|
||||
mptAlice.create({});
|
||||
|
||||
// Trying to set DomainID on a public MPTokenIssuance
|
||||
mptAlice.set(
|
||||
{.domainID = uint256(42), .err = tecNO_PERMISSION});
|
||||
|
||||
mptAlice.set(
|
||||
{.domainID = beast::zero, .err = tecNO_PERMISSION});
|
||||
}
|
||||
|
||||
{
|
||||
Env env{*this, features};
|
||||
|
||||
MPTTester mptAlice(env, alice);
|
||||
mptAlice.create({.flags = tfMPTRequireAuth});
|
||||
|
||||
// Trying to set non-existing DomainID
|
||||
mptAlice.set(
|
||||
{.domainID = uint256(42), .err = tecOBJECT_NOT_FOUND});
|
||||
|
||||
// Trying to lock but locking is disabled
|
||||
mptAlice.set(
|
||||
{.flags = tfMPTUnlock,
|
||||
.domainID = uint256(42),
|
||||
.err = tecNO_PERMISSION});
|
||||
|
||||
mptAlice.set(
|
||||
{.flags = tfMPTUnlock,
|
||||
.domainID = beast::zero,
|
||||
.err = tecNO_PERMISSION});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
@@ -590,71 +780,136 @@ class MPToken_test : public beast::unit_test::suite
|
||||
testcase("Enabled set transaction");
|
||||
|
||||
using namespace test::jtx;
|
||||
|
||||
// Test locking and unlocking
|
||||
Env env{*this, features};
|
||||
Account const alice("alice"); // issuer
|
||||
Account const bob("bob"); // holder
|
||||
|
||||
MPTTester mptAlice(env, alice, {.holders = {bob}});
|
||||
|
||||
// create a mptokenissuance with locking
|
||||
mptAlice.create(
|
||||
{.ownerCount = 1, .holderCount = 0, .flags = tfMPTCanLock});
|
||||
|
||||
mptAlice.authorize({.account = bob, .holderCount = 1});
|
||||
|
||||
// locks bob's mptoken
|
||||
mptAlice.set({.account = alice, .holder = bob, .flags = tfMPTLock});
|
||||
|
||||
// trying to lock bob's mptoken again will still succeed
|
||||
// but no changes to the objects
|
||||
mptAlice.set({.account = alice, .holder = bob, .flags = tfMPTLock});
|
||||
|
||||
// alice locks the mptissuance
|
||||
mptAlice.set({.account = alice, .flags = tfMPTLock});
|
||||
|
||||
// alice tries to lock up both mptissuance and mptoken again
|
||||
// it will not change the flags and both will remain locked.
|
||||
mptAlice.set({.account = alice, .flags = tfMPTLock});
|
||||
mptAlice.set({.account = alice, .holder = bob, .flags = tfMPTLock});
|
||||
|
||||
// alice unlocks bob's mptoken
|
||||
mptAlice.set({.account = alice, .holder = bob, .flags = tfMPTUnlock});
|
||||
|
||||
// locks up bob's mptoken again
|
||||
mptAlice.set({.account = alice, .holder = bob, .flags = tfMPTLock});
|
||||
if (!features[featureSingleAssetVault])
|
||||
{
|
||||
// Delete bobs' mptoken even though it is locked
|
||||
mptAlice.authorize({.account = bob, .flags = tfMPTUnauthorize});
|
||||
// Test locking and unlocking
|
||||
Env env{*this, features};
|
||||
|
||||
MPTTester mptAlice(env, alice, {.holders = {bob}});
|
||||
|
||||
// create a mptokenissuance with locking
|
||||
mptAlice.create(
|
||||
{.ownerCount = 1, .holderCount = 0, .flags = tfMPTCanLock});
|
||||
|
||||
mptAlice.authorize({.account = bob, .holderCount = 1});
|
||||
|
||||
// locks bob's mptoken
|
||||
mptAlice.set({.account = alice, .holder = bob, .flags = tfMPTLock});
|
||||
|
||||
// trying to lock bob's mptoken again will still succeed
|
||||
// but no changes to the objects
|
||||
mptAlice.set({.account = alice, .holder = bob, .flags = tfMPTLock});
|
||||
|
||||
// alice locks the mptissuance
|
||||
mptAlice.set({.account = alice, .flags = tfMPTLock});
|
||||
|
||||
// alice tries to lock up both mptissuance and mptoken again
|
||||
// it will not change the flags and both will remain locked.
|
||||
mptAlice.set({.account = alice, .flags = tfMPTLock});
|
||||
mptAlice.set({.account = alice, .holder = bob, .flags = tfMPTLock});
|
||||
|
||||
// alice unlocks bob's mptoken
|
||||
mptAlice.set(
|
||||
{.account = alice,
|
||||
.holder = bob,
|
||||
.flags = tfMPTUnlock,
|
||||
.err = tecOBJECT_NOT_FOUND});
|
||||
{.account = alice, .holder = bob, .flags = tfMPTUnlock});
|
||||
|
||||
return;
|
||||
// locks up bob's mptoken again
|
||||
mptAlice.set({.account = alice, .holder = bob, .flags = tfMPTLock});
|
||||
if (!features[featureSingleAssetVault])
|
||||
{
|
||||
// Delete bobs' mptoken even though it is locked
|
||||
mptAlice.authorize({.account = bob, .flags = tfMPTUnauthorize});
|
||||
|
||||
mptAlice.set(
|
||||
{.account = alice,
|
||||
.holder = bob,
|
||||
.flags = tfMPTUnlock,
|
||||
.err = tecOBJECT_NOT_FOUND});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// Cannot delete locked MPToken
|
||||
mptAlice.authorize(
|
||||
{.account = bob,
|
||||
.flags = tfMPTUnauthorize,
|
||||
.err = tecNO_PERMISSION});
|
||||
|
||||
// alice unlocks mptissuance
|
||||
mptAlice.set({.account = alice, .flags = tfMPTUnlock});
|
||||
|
||||
// alice unlocks bob's mptoken
|
||||
mptAlice.set(
|
||||
{.account = alice, .holder = bob, .flags = tfMPTUnlock});
|
||||
|
||||
// alice unlocks mptissuance and bob's mptoken again despite that
|
||||
// they are already unlocked. Make sure this will not change the
|
||||
// flags
|
||||
mptAlice.set(
|
||||
{.account = alice, .holder = bob, .flags = tfMPTUnlock});
|
||||
mptAlice.set({.account = alice, .flags = tfMPTUnlock});
|
||||
}
|
||||
|
||||
// Cannot delete locked MPToken
|
||||
mptAlice.authorize(
|
||||
{.account = bob,
|
||||
.flags = tfMPTUnauthorize,
|
||||
.err = tecNO_PERMISSION});
|
||||
if (features[featureSingleAssetVault])
|
||||
{
|
||||
// Add permissioned domain
|
||||
std::string const credType = "credential";
|
||||
|
||||
// alice unlocks mptissuance
|
||||
mptAlice.set({.account = alice, .flags = tfMPTUnlock});
|
||||
// Test setting and resetting domain ID
|
||||
Env env{*this, features};
|
||||
|
||||
// alice unlocks bob's mptoken
|
||||
mptAlice.set({.account = alice, .holder = bob, .flags = tfMPTUnlock});
|
||||
auto const domainId1 = [&]() {
|
||||
Account const credIssuer1{"credIssuer1"};
|
||||
env.fund(XRP(1000), credIssuer1);
|
||||
|
||||
// alice unlocks mptissuance and bob's mptoken again despite that
|
||||
// they are already unlocked. Make sure this will not change the
|
||||
// flags
|
||||
mptAlice.set({.account = alice, .holder = bob, .flags = tfMPTUnlock});
|
||||
mptAlice.set({.account = alice, .flags = tfMPTUnlock});
|
||||
pdomain::Credentials const credentials1{
|
||||
{.issuer = credIssuer1, .credType = credType}};
|
||||
|
||||
env(pdomain::setTx(credIssuer1, credentials1));
|
||||
return [&]() {
|
||||
auto tx = env.tx()->getJson(JsonOptions::none);
|
||||
return pdomain::getNewDomain(env.meta());
|
||||
}();
|
||||
}();
|
||||
|
||||
auto const domainId2 = [&]() {
|
||||
Account const credIssuer2{"credIssuer2"};
|
||||
env.fund(XRP(1000), credIssuer2);
|
||||
|
||||
pdomain::Credentials const credentials2{
|
||||
{.issuer = credIssuer2, .credType = credType}};
|
||||
|
||||
env(pdomain::setTx(credIssuer2, credentials2));
|
||||
return [&]() {
|
||||
auto tx = env.tx()->getJson(JsonOptions::none);
|
||||
return pdomain::getNewDomain(env.meta());
|
||||
}();
|
||||
}();
|
||||
|
||||
MPTTester mptAlice(env, alice, {.holders = {bob}});
|
||||
|
||||
// create a mptokenissuance with auth.
|
||||
mptAlice.create(
|
||||
{.ownerCount = 1, .holderCount = 0, .flags = tfMPTRequireAuth});
|
||||
BEAST_EXPECT(mptAlice.checkDomainID(std::nullopt));
|
||||
|
||||
// reset "domain not set" to "domain not set", i.e. no change
|
||||
mptAlice.set({.domainID = beast::zero});
|
||||
BEAST_EXPECT(mptAlice.checkDomainID(std::nullopt));
|
||||
|
||||
// reset "domain not set" to domain1
|
||||
mptAlice.set({.domainID = domainId1});
|
||||
BEAST_EXPECT(mptAlice.checkDomainID(domainId1));
|
||||
|
||||
// reset domain1 to domain2
|
||||
mptAlice.set({.domainID = domainId2});
|
||||
BEAST_EXPECT(mptAlice.checkDomainID(domainId2));
|
||||
|
||||
// reset domain to "domain not set"
|
||||
mptAlice.set({.domainID = beast::zero});
|
||||
BEAST_EXPECT(mptAlice.checkDomainID(std::nullopt));
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
@@ -889,6 +1144,200 @@ class MPToken_test : public beast::unit_test::suite
|
||||
mptAlice.pay(bob, alice, 100, tecNO_AUTH);
|
||||
}
|
||||
|
||||
if (features[featureSingleAssetVault] &&
|
||||
features[featurePermissionedDomains])
|
||||
{
|
||||
// If RequireAuth is enabled and domain is a match, payment succeeds
|
||||
{
|
||||
Env env{*this, features};
|
||||
std::string const credType = "credential";
|
||||
Account const credIssuer1{"credIssuer1"};
|
||||
env.fund(XRP(1000), credIssuer1, bob);
|
||||
|
||||
auto const domainId1 = [&]() {
|
||||
pdomain::Credentials const credentials1{
|
||||
{.issuer = credIssuer1, .credType = credType}};
|
||||
|
||||
env(pdomain::setTx(credIssuer1, credentials1));
|
||||
return [&]() {
|
||||
auto tx = env.tx()->getJson(JsonOptions::none);
|
||||
return pdomain::getNewDomain(env.meta());
|
||||
}();
|
||||
}();
|
||||
// bob is authorized via domain
|
||||
env(credentials::create(bob, credIssuer1, credType));
|
||||
env(credentials::accept(bob, credIssuer1, credType));
|
||||
env.close();
|
||||
|
||||
MPTTester mptAlice(env, alice, {});
|
||||
env.close();
|
||||
|
||||
mptAlice.create({
|
||||
.ownerCount = 1,
|
||||
.holderCount = 0,
|
||||
.flags = tfMPTRequireAuth | tfMPTCanTransfer,
|
||||
.domainID = domainId1,
|
||||
});
|
||||
|
||||
mptAlice.authorize({.account = bob});
|
||||
env.close();
|
||||
|
||||
// bob is authorized via domain
|
||||
mptAlice.pay(alice, bob, 100);
|
||||
mptAlice.set({.domainID = beast::zero});
|
||||
|
||||
// bob is no longer authorized
|
||||
mptAlice.pay(alice, bob, 100, tecNO_AUTH);
|
||||
}
|
||||
|
||||
{
|
||||
Env env{*this, features};
|
||||
std::string const credType = "credential";
|
||||
Account const credIssuer1{"credIssuer1"};
|
||||
env.fund(XRP(1000), credIssuer1, bob);
|
||||
|
||||
auto const domainId1 = [&]() {
|
||||
pdomain::Credentials const credentials1{
|
||||
{.issuer = credIssuer1, .credType = credType}};
|
||||
|
||||
env(pdomain::setTx(credIssuer1, credentials1));
|
||||
return [&]() {
|
||||
auto tx = env.tx()->getJson(JsonOptions::none);
|
||||
return pdomain::getNewDomain(env.meta());
|
||||
}();
|
||||
}();
|
||||
// bob is authorized via domain
|
||||
env(credentials::create(bob, credIssuer1, credType));
|
||||
env(credentials::accept(bob, credIssuer1, credType));
|
||||
env.close();
|
||||
|
||||
MPTTester mptAlice(env, alice, {});
|
||||
env.close();
|
||||
|
||||
mptAlice.create({
|
||||
.ownerCount = 1,
|
||||
.holderCount = 0,
|
||||
.flags = tfMPTRequireAuth | tfMPTCanTransfer,
|
||||
.domainID = domainId1,
|
||||
});
|
||||
|
||||
// bob creates an empty MPToken
|
||||
mptAlice.authorize({.account = bob});
|
||||
|
||||
// alice authorizes bob to hold funds
|
||||
mptAlice.authorize({.account = alice, .holder = bob});
|
||||
|
||||
// alice sends 100 MPT to bob
|
||||
mptAlice.pay(alice, bob, 100);
|
||||
|
||||
// alice UNAUTHORIZES bob
|
||||
mptAlice.authorize(
|
||||
{.account = alice,
|
||||
.holder = bob,
|
||||
.flags = tfMPTUnauthorize});
|
||||
|
||||
// bob is still authorized, via domain
|
||||
mptAlice.pay(bob, alice, 10);
|
||||
|
||||
mptAlice.set({.domainID = beast::zero});
|
||||
|
||||
// bob fails to send back to alice because he is no longer
|
||||
// authorize to move his funds!
|
||||
mptAlice.pay(bob, alice, 10, tecNO_AUTH);
|
||||
}
|
||||
|
||||
{
|
||||
Env env{*this, features};
|
||||
std::string const credType = "credential";
|
||||
// credIssuer1 is the owner of domainId1 and a credential issuer
|
||||
Account const credIssuer1{"credIssuer1"};
|
||||
// credIssuer2 is the owner of domainId2 and a credential issuer
|
||||
// Note, domainId2 also lists credentials issued by credIssuer1
|
||||
Account const credIssuer2{"credIssuer2"};
|
||||
env.fund(XRP(1000), credIssuer1, credIssuer2, bob, carol);
|
||||
|
||||
auto const domainId1 = [&]() {
|
||||
pdomain::Credentials const credentials{
|
||||
{.issuer = credIssuer1, .credType = credType}};
|
||||
|
||||
env(pdomain::setTx(credIssuer1, credentials));
|
||||
return [&]() {
|
||||
auto tx = env.tx()->getJson(JsonOptions::none);
|
||||
return pdomain::getNewDomain(env.meta());
|
||||
}();
|
||||
}();
|
||||
|
||||
auto const domainId2 = [&]() {
|
||||
pdomain::Credentials const credentials{
|
||||
{.issuer = credIssuer1, .credType = credType},
|
||||
{.issuer = credIssuer2, .credType = credType}};
|
||||
|
||||
env(pdomain::setTx(credIssuer2, credentials));
|
||||
return [&]() {
|
||||
auto tx = env.tx()->getJson(JsonOptions::none);
|
||||
return pdomain::getNewDomain(env.meta());
|
||||
}();
|
||||
}();
|
||||
|
||||
// bob is authorized via credIssuer1 which is recognized by both
|
||||
// domainId1 and domainId2
|
||||
env(credentials::create(bob, credIssuer1, credType));
|
||||
env(credentials::accept(bob, credIssuer1, credType));
|
||||
env.close();
|
||||
|
||||
// carol is authorized via credIssuer2, only recognized by
|
||||
// domainId2
|
||||
env(credentials::create(carol, credIssuer2, credType));
|
||||
env(credentials::accept(carol, credIssuer2, credType));
|
||||
env.close();
|
||||
|
||||
MPTTester mptAlice(env, alice, {});
|
||||
env.close();
|
||||
|
||||
mptAlice.create({
|
||||
.ownerCount = 1,
|
||||
.holderCount = 0,
|
||||
.flags = tfMPTRequireAuth | tfMPTCanTransfer,
|
||||
.domainID = domainId1,
|
||||
});
|
||||
|
||||
// bob and carol create an empty MPToken
|
||||
mptAlice.authorize({.account = bob});
|
||||
mptAlice.authorize({.account = carol});
|
||||
env.close();
|
||||
|
||||
// alice sends 50 MPT to bob but cannot send to carol
|
||||
mptAlice.pay(alice, bob, 50);
|
||||
mptAlice.pay(alice, carol, 50, tecNO_AUTH);
|
||||
env.close();
|
||||
|
||||
// bob cannot send to carol because they are not on the same
|
||||
// domain (since credIssuer2 is not recognized by domainId1)
|
||||
mptAlice.pay(bob, carol, 10, tecNO_AUTH);
|
||||
env.close();
|
||||
|
||||
// alice updates domainID to domainId2 which recognizes both
|
||||
// credIssuer1 and credIssuer2
|
||||
mptAlice.set({.domainID = domainId2});
|
||||
// alice can now send to carol
|
||||
mptAlice.pay(alice, carol, 10);
|
||||
env.close();
|
||||
|
||||
// bob can now send to carol because both are in the same
|
||||
// domain
|
||||
mptAlice.pay(bob, carol, 10);
|
||||
env.close();
|
||||
|
||||
// bob loses his authorization and can no longer send MPT
|
||||
env(credentials::deleteCred(
|
||||
credIssuer1, bob, credIssuer1, credType));
|
||||
env.close();
|
||||
|
||||
mptAlice.pay(bob, carol, 10, tecNO_AUTH);
|
||||
mptAlice.pay(bob, alice, 10, tecNO_AUTH);
|
||||
}
|
||||
}
|
||||
|
||||
// Non-issuer cannot send to each other if MPTCanTransfer isn't set
|
||||
{
|
||||
Env env(*this, features);
|
||||
@@ -1340,10 +1789,8 @@ class MPToken_test : public beast::unit_test::suite
|
||||
}
|
||||
|
||||
void
|
||||
testDepositPreauth()
|
||||
testDepositPreauth(FeatureBitset features)
|
||||
{
|
||||
testcase("DepositPreauth");
|
||||
|
||||
using namespace test::jtx;
|
||||
Account const alice("alice"); // issuer
|
||||
Account const bob("bob"); // holder
|
||||
@@ -1352,8 +1799,11 @@ class MPToken_test : public beast::unit_test::suite
|
||||
|
||||
char const credType[] = "abcde";
|
||||
|
||||
if (features[featureCredentials])
|
||||
{
|
||||
Env env(*this);
|
||||
testcase("DepositPreauth");
|
||||
|
||||
Env env(*this, features);
|
||||
|
||||
env.fund(XRP(50000), diana, dpIssuer);
|
||||
env.close();
|
||||
@@ -2297,6 +2747,8 @@ public:
|
||||
|
||||
// MPTokenIssuanceCreate
|
||||
testCreateValidation(all - featureSingleAssetVault);
|
||||
testCreateValidation(
|
||||
(all | featureSingleAssetVault) - featurePermissionedDomains);
|
||||
testCreateValidation(all | featureSingleAssetVault);
|
||||
testCreateEnabled(all - featureSingleAssetVault);
|
||||
testCreateEnabled(all | featureSingleAssetVault);
|
||||
@@ -2314,7 +2766,11 @@ public:
|
||||
testAuthorizeEnabled(all | featureSingleAssetVault);
|
||||
|
||||
// MPTokenIssuanceSet
|
||||
testSetValidation(all);
|
||||
testSetValidation(all - featureSingleAssetVault);
|
||||
testSetValidation(
|
||||
(all | featureSingleAssetVault) - featurePermissionedDomains);
|
||||
testSetValidation(all | featureSingleAssetVault);
|
||||
|
||||
testSetEnabled(all - featureSingleAssetVault);
|
||||
testSetEnabled(all | featureSingleAssetVault);
|
||||
|
||||
@@ -2323,8 +2779,9 @@ public:
|
||||
testClawback(all);
|
||||
|
||||
// Test Direct Payment
|
||||
testPayment(all);
|
||||
testDepositPreauth();
|
||||
testPayment(all | featureSingleAssetVault);
|
||||
testDepositPreauth(all);
|
||||
testDepositPreauth(all - featureCredentials);
|
||||
|
||||
// Test MPT Amount is invalid in Tx, which don't support MPT
|
||||
testMPTInvalidInTx(all);
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -19,6 +19,7 @@
|
||||
|
||||
#include <test/jtx.h>
|
||||
|
||||
#include <xrpl/protocol/SField.h>
|
||||
#include <xrpl/protocol/jss.h>
|
||||
|
||||
namespace ripple {
|
||||
@@ -99,6 +100,8 @@ MPTTester::create(MPTCreate const& arg)
|
||||
jv[sfMPTokenMetadata] = strHex(*arg.metadata);
|
||||
if (arg.maxAmt)
|
||||
jv[sfMaximumAmount] = std::to_string(*arg.maxAmt);
|
||||
if (arg.domainID)
|
||||
jv[sfDomainID] = to_string(*arg.domainID);
|
||||
if (submit(arg, jv) != tesSUCCESS)
|
||||
{
|
||||
// Verify issuance doesn't exist
|
||||
@@ -235,6 +238,8 @@ MPTTester::set(MPTSet const& arg)
|
||||
jv[sfHolder] = arg.holder->human();
|
||||
if (arg.delegate)
|
||||
jv[sfDelegate] = arg.delegate->human();
|
||||
if (arg.domainID)
|
||||
jv[sfDomainID] = to_string(*arg.domainID);
|
||||
if (submit(arg, jv) == tesSUCCESS && arg.flags.value_or(0))
|
||||
{
|
||||
auto require = [&](std::optional<Account> const& holder,
|
||||
@@ -272,6 +277,16 @@ MPTTester::forObject(
|
||||
return false;
|
||||
}
|
||||
|
||||
[[nodiscard]] bool
|
||||
MPTTester::checkDomainID(std::optional<uint256> expected) const
|
||||
{
|
||||
return forObject([&](SLEP const& sle) -> bool {
|
||||
if (sle->isFieldPresent(sfDomainID))
|
||||
return expected == sle->getFieldH256(sfDomainID);
|
||||
return (!expected.has_value());
|
||||
});
|
||||
}
|
||||
|
||||
[[nodiscard]] bool
|
||||
MPTTester::checkMPTokenAmount(
|
||||
Account const& holder_,
|
||||
|
||||
@@ -106,6 +106,7 @@ struct MPTCreate
|
||||
std::optional<std::uint32_t> holderCount = std::nullopt;
|
||||
bool fund = true;
|
||||
std::optional<std::uint32_t> flags = {0};
|
||||
std::optional<uint256> domainID = std::nullopt;
|
||||
std::optional<TER> err = std::nullopt;
|
||||
};
|
||||
|
||||
@@ -139,6 +140,7 @@ struct MPTSet
|
||||
std::optional<std::uint32_t> holderCount = std::nullopt;
|
||||
std::optional<std::uint32_t> flags = std::nullopt;
|
||||
std::optional<Account> delegate = std::nullopt;
|
||||
std::optional<uint256> domainID = std::nullopt;
|
||||
std::optional<TER> err = std::nullopt;
|
||||
};
|
||||
|
||||
@@ -165,6 +167,9 @@ public:
|
||||
void
|
||||
set(MPTSet const& set = {});
|
||||
|
||||
[[nodiscard]] bool
|
||||
checkDomainID(std::optional<uint256> expected) const;
|
||||
|
||||
[[nodiscard]] bool
|
||||
checkMPTokenAmount(Account const& holder, std::int64_t expectedAmount)
|
||||
const;
|
||||
|
||||
@@ -131,6 +131,9 @@ public:
|
||||
BEAST_EXPECT(jv.isMember(jss::id) && jv[jss::id] == 5);
|
||||
}
|
||||
BEAST_EXPECT(jv[jss::result][jss::ledger_index] == 2);
|
||||
BEAST_EXPECT(
|
||||
jv[jss::result][jss::network_id] ==
|
||||
env.app().config().NETWORK_ID);
|
||||
}
|
||||
|
||||
{
|
||||
@@ -139,7 +142,8 @@ public:
|
||||
|
||||
// Check stream update
|
||||
BEAST_EXPECT(wsc->findMsg(5s, [&](auto const& jv) {
|
||||
return jv[jss::ledger_index] == 3;
|
||||
return jv[jss::ledger_index] == 3 &&
|
||||
jv[jss::network_id] == env.app().config().NETWORK_ID;
|
||||
}));
|
||||
}
|
||||
|
||||
@@ -149,7 +153,8 @@ public:
|
||||
|
||||
// Check stream update
|
||||
BEAST_EXPECT(wsc->findMsg(5s, [&](auto const& jv) {
|
||||
return jv[jss::ledger_index] == 4;
|
||||
return jv[jss::ledger_index] == 4 &&
|
||||
jv[jss::network_id] == env.app().config().NETWORK_ID;
|
||||
}));
|
||||
}
|
||||
|
||||
@@ -509,6 +514,11 @@ public:
|
||||
if (!jv.isMember(jss::validated_hash))
|
||||
return false;
|
||||
|
||||
uint32_t netID = env.app().config().NETWORK_ID;
|
||||
if (!jv.isMember(jss::network_id) ||
|
||||
jv[jss::network_id] != netID)
|
||||
return false;
|
||||
|
||||
// Certain fields are only added on a flag ledger.
|
||||
bool const isFlagLedger =
|
||||
(env.closed()->info().seq + 1) % 256 == 0;
|
||||
@@ -567,6 +577,7 @@ public:
|
||||
jv[jss::streams][0u] = "ledger";
|
||||
jr = env.rpc("json", "subscribe", to_string(jv))[jss::result];
|
||||
BEAST_EXPECT(jr[jss::status] == "success");
|
||||
BEAST_EXPECT(jr[jss::network_id] == env.app().config().NETWORK_ID);
|
||||
|
||||
jr = env.rpc("json", "unsubscribe", to_string(jv))[jss::result];
|
||||
BEAST_EXPECT(jr[jss::status] == "success");
|
||||
|
||||
@@ -996,7 +996,8 @@ pendSaveValidated(
|
||||
bool isSynchronous,
|
||||
bool isCurrent)
|
||||
{
|
||||
if (!app.getHashRouter().setFlags(ledger->info().hash, SF_SAVED))
|
||||
if (!app.getHashRouter().setFlags(
|
||||
ledger->info().hash, HashRouterFlags::SAVED))
|
||||
{
|
||||
// We have tried to save this ledger recently
|
||||
auto stream = app.journal("Ledger").debug();
|
||||
|
||||
@@ -65,7 +65,10 @@ HashRouter::addSuppressionPeerWithStatus(uint256 const& key, PeerShortID peer)
|
||||
}
|
||||
|
||||
bool
|
||||
HashRouter::addSuppressionPeer(uint256 const& key, PeerShortID peer, int& flags)
|
||||
HashRouter::addSuppressionPeer(
|
||||
uint256 const& key,
|
||||
PeerShortID peer,
|
||||
HashRouterFlags& flags)
|
||||
{
|
||||
std::lock_guard lock(mutex_);
|
||||
|
||||
@@ -79,7 +82,7 @@ bool
|
||||
HashRouter::shouldProcess(
|
||||
uint256 const& key,
|
||||
PeerShortID peer,
|
||||
int& flags,
|
||||
HashRouterFlags& flags,
|
||||
std::chrono::seconds tx_interval)
|
||||
{
|
||||
std::lock_guard lock(mutex_);
|
||||
@@ -91,7 +94,7 @@ HashRouter::shouldProcess(
|
||||
return s.shouldProcess(suppressionMap_.clock().now(), tx_interval);
|
||||
}
|
||||
|
||||
int
|
||||
HashRouterFlags
|
||||
HashRouter::getFlags(uint256 const& key)
|
||||
{
|
||||
std::lock_guard lock(mutex_);
|
||||
@@ -100,9 +103,10 @@ HashRouter::getFlags(uint256 const& key)
|
||||
}
|
||||
|
||||
bool
|
||||
HashRouter::setFlags(uint256 const& key, int flags)
|
||||
HashRouter::setFlags(uint256 const& key, HashRouterFlags flags)
|
||||
{
|
||||
XRPL_ASSERT(flags, "ripple::HashRouter::setFlags : valid input");
|
||||
XRPL_ASSERT(
|
||||
static_cast<bool>(flags), "ripple::HashRouter::setFlags : valid input");
|
||||
|
||||
std::lock_guard lock(mutex_);
|
||||
|
||||
|
||||
@@ -31,20 +31,59 @@
|
||||
|
||||
namespace ripple {
|
||||
|
||||
// TODO convert these macros to int constants or an enum
|
||||
#define SF_BAD 0x02 // Temporarily bad
|
||||
#define SF_SAVED 0x04
|
||||
#define SF_HELD 0x08 // Held by LedgerMaster after potential processing failure
|
||||
#define SF_TRUSTED 0x10 // comes from trusted source
|
||||
enum class HashRouterFlags : std::uint16_t {
|
||||
// Public flags
|
||||
UNDEFINED = 0x00,
|
||||
BAD = 0x02, // Temporarily bad
|
||||
SAVED = 0x04,
|
||||
HELD = 0x08, // Held by LedgerMaster after potential processing failure
|
||||
TRUSTED = 0x10, // Comes from a trusted source
|
||||
|
||||
// Private flags, used internally in apply.cpp.
|
||||
// Do not attempt to read, set, or reuse.
|
||||
#define SF_PRIVATE1 0x0100
|
||||
#define SF_PRIVATE2 0x0200
|
||||
#define SF_PRIVATE3 0x0400
|
||||
#define SF_PRIVATE4 0x0800
|
||||
#define SF_PRIVATE5 0x1000
|
||||
#define SF_PRIVATE6 0x2000
|
||||
// Private flags (used internally in apply.cpp)
|
||||
// Do not attempt to read, set, or reuse.
|
||||
PRIVATE1 = 0x0100,
|
||||
PRIVATE2 = 0x0200,
|
||||
PRIVATE3 = 0x0400,
|
||||
PRIVATE4 = 0x0800,
|
||||
PRIVATE5 = 0x1000,
|
||||
PRIVATE6 = 0x2000
|
||||
};
|
||||
|
||||
constexpr HashRouterFlags
|
||||
operator|(HashRouterFlags lhs, HashRouterFlags rhs)
|
||||
{
|
||||
return static_cast<HashRouterFlags>(
|
||||
static_cast<std::underlying_type_t<HashRouterFlags>>(lhs) |
|
||||
static_cast<std::underlying_type_t<HashRouterFlags>>(rhs));
|
||||
}
|
||||
|
||||
constexpr HashRouterFlags&
|
||||
operator|=(HashRouterFlags& lhs, HashRouterFlags rhs)
|
||||
{
|
||||
lhs = lhs | rhs;
|
||||
return lhs;
|
||||
}
|
||||
|
||||
constexpr HashRouterFlags
|
||||
operator&(HashRouterFlags lhs, HashRouterFlags rhs)
|
||||
{
|
||||
return static_cast<HashRouterFlags>(
|
||||
static_cast<std::underlying_type_t<HashRouterFlags>>(lhs) &
|
||||
static_cast<std::underlying_type_t<HashRouterFlags>>(rhs));
|
||||
}
|
||||
|
||||
constexpr HashRouterFlags&
|
||||
operator&=(HashRouterFlags& lhs, HashRouterFlags rhs)
|
||||
{
|
||||
lhs = lhs & rhs;
|
||||
return lhs;
|
||||
}
|
||||
|
||||
constexpr bool
|
||||
any(HashRouterFlags flags)
|
||||
{
|
||||
return static_cast<std::underlying_type_t<HashRouterFlags>>(flags) != 0;
|
||||
}
|
||||
|
||||
class Config;
|
||||
|
||||
@@ -101,14 +140,14 @@ private:
|
||||
peers_.insert(peer);
|
||||
}
|
||||
|
||||
int
|
||||
HashRouterFlags
|
||||
getFlags(void) const
|
||||
{
|
||||
return flags_;
|
||||
}
|
||||
|
||||
void
|
||||
setFlags(int flagsToSet)
|
||||
setFlags(HashRouterFlags flagsToSet)
|
||||
{
|
||||
flags_ |= flagsToSet;
|
||||
}
|
||||
@@ -154,7 +193,7 @@ private:
|
||||
}
|
||||
|
||||
private:
|
||||
int flags_ = 0;
|
||||
HashRouterFlags flags_ = HashRouterFlags::UNDEFINED;
|
||||
std::set<PeerShortID> peers_;
|
||||
// This could be generalized to a map, if more
|
||||
// than one flag needs to expire independently.
|
||||
@@ -190,14 +229,17 @@ public:
|
||||
addSuppressionPeerWithStatus(uint256 const& key, PeerShortID peer);
|
||||
|
||||
bool
|
||||
addSuppressionPeer(uint256 const& key, PeerShortID peer, int& flags);
|
||||
addSuppressionPeer(
|
||||
uint256 const& key,
|
||||
PeerShortID peer,
|
||||
HashRouterFlags& flags);
|
||||
|
||||
// Add a peer suppression and return whether the entry should be processed
|
||||
bool
|
||||
shouldProcess(
|
||||
uint256 const& key,
|
||||
PeerShortID peer,
|
||||
int& flags,
|
||||
HashRouterFlags& flags,
|
||||
std::chrono::seconds tx_interval);
|
||||
|
||||
/** Set the flags on a hash.
|
||||
@@ -205,9 +247,9 @@ public:
|
||||
@return `true` if the flags were changed. `false` if unchanged.
|
||||
*/
|
||||
bool
|
||||
setFlags(uint256 const& key, int flags);
|
||||
setFlags(uint256 const& key, HashRouterFlags flags);
|
||||
|
||||
int
|
||||
HashRouterFlags
|
||||
getFlags(uint256 const& key);
|
||||
|
||||
/** Determines whether the hashed item should be relayed.
|
||||
|
||||
@@ -1207,7 +1207,7 @@ NetworkOPsImp::submitTransaction(std::shared_ptr<STTx const> const& iTrans)
|
||||
auto const txid = trans->getTransactionID();
|
||||
auto const flags = app_.getHashRouter().getFlags(txid);
|
||||
|
||||
if ((flags & SF_BAD) != 0)
|
||||
if ((flags & HashRouterFlags::BAD) != HashRouterFlags::UNDEFINED)
|
||||
{
|
||||
JLOG(m_journal.warn()) << "Submitted transaction cached bad";
|
||||
return;
|
||||
@@ -1251,7 +1251,7 @@ NetworkOPsImp::preProcessTransaction(std::shared_ptr<Transaction>& transaction)
|
||||
{
|
||||
auto const newFlags = app_.getHashRouter().getFlags(transaction->getID());
|
||||
|
||||
if ((newFlags & SF_BAD) != 0)
|
||||
if ((newFlags & HashRouterFlags::BAD) != HashRouterFlags::UNDEFINED)
|
||||
{
|
||||
// cached bad
|
||||
JLOG(m_journal.warn()) << transaction->getID() << ": cached bad!\n";
|
||||
@@ -1270,7 +1270,8 @@ NetworkOPsImp::preProcessTransaction(std::shared_ptr<Transaction>& transaction)
|
||||
{
|
||||
transaction->setStatus(INVALID);
|
||||
transaction->setResult(temINVALID_FLAG);
|
||||
app_.getHashRouter().setFlags(transaction->getID(), SF_BAD);
|
||||
app_.getHashRouter().setFlags(
|
||||
transaction->getID(), HashRouterFlags::BAD);
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -1289,7 +1290,8 @@ NetworkOPsImp::preProcessTransaction(std::shared_ptr<Transaction>& transaction)
|
||||
JLOG(m_journal.info()) << "Transaction has bad signature: " << reason;
|
||||
transaction->setStatus(INVALID);
|
||||
transaction->setResult(temBAD_SIGNATURE);
|
||||
app_.getHashRouter().setFlags(transaction->getID(), SF_BAD);
|
||||
app_.getHashRouter().setFlags(
|
||||
transaction->getID(), HashRouterFlags::BAD);
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -1412,7 +1414,8 @@ NetworkOPsImp::processTransactionSet(CanonicalTXSet const& set)
|
||||
JLOG(m_journal.trace())
|
||||
<< "Exception checking transaction: " << reason;
|
||||
}
|
||||
app_.getHashRouter().setFlags(tx->getTransactionID(), SF_BAD);
|
||||
app_.getHashRouter().setFlags(
|
||||
tx->getTransactionID(), HashRouterFlags::BAD);
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -1538,7 +1541,8 @@ NetworkOPsImp::apply(std::unique_lock<std::mutex>& batchLock)
|
||||
e.transaction->setResult(e.result);
|
||||
|
||||
if (isTemMalformed(e.result))
|
||||
app_.getHashRouter().setFlags(e.transaction->getID(), SF_BAD);
|
||||
app_.getHashRouter().setFlags(
|
||||
e.transaction->getID(), HashRouterFlags::BAD);
|
||||
|
||||
#ifdef DEBUG
|
||||
if (e.result != tesSUCCESS)
|
||||
@@ -1626,7 +1630,8 @@ NetworkOPsImp::apply(std::unique_lock<std::mutex>& batchLock)
|
||||
// (5) ledgers into the future. (Remember that an
|
||||
// unseated optional compares as less than all seated
|
||||
// values, so it has to be checked explicitly first.)
|
||||
// 3. The SF_HELD flag is not set on the txID. (setFlags
|
||||
// 3. The HashRouterFlags::BAD flag is not set on the txID.
|
||||
// (setFlags
|
||||
// checks before setting. If the flag is set, it returns
|
||||
// false, which means it's been held once without one of
|
||||
// the other conditions, so don't hold it again. Time's
|
||||
@@ -1635,7 +1640,7 @@ NetworkOPsImp::apply(std::unique_lock<std::mutex>& batchLock)
|
||||
if (e.local ||
|
||||
(ledgersLeft && ledgersLeft <= LocalTxs::holdLedgers) ||
|
||||
app_.getHashRouter().setFlags(
|
||||
e.transaction->getID(), SF_HELD))
|
||||
e.transaction->getID(), HashRouterFlags::HELD))
|
||||
{
|
||||
// transaction should be held
|
||||
JLOG(m_journal.debug())
|
||||
@@ -2410,6 +2415,7 @@ NetworkOPsImp::pubValidation(std::shared_ptr<STValidation> const& val)
|
||||
jvObj[jss::flags] = val->getFlags();
|
||||
jvObj[jss::signing_time] = *(*val)[~sfSigningTime];
|
||||
jvObj[jss::data] = strHex(val->getSerializer().slice());
|
||||
jvObj[jss::network_id] = app_.config().NETWORK_ID;
|
||||
|
||||
if (auto version = (*val)[~sfServerVersion])
|
||||
jvObj[jss::server_version] = std::to_string(*version);
|
||||
@@ -3114,6 +3120,8 @@ NetworkOPsImp::pubLedger(std::shared_ptr<ReadView const> const& lpAccepted)
|
||||
jvObj[jss::ledger_time] = Json::Value::UInt(
|
||||
lpAccepted->info().closeTime.time_since_epoch().count());
|
||||
|
||||
jvObj[jss::network_id] = app_.config().NETWORK_ID;
|
||||
|
||||
if (!lpAccepted->rules().enabled(featureXRPFees))
|
||||
jvObj[jss::fee_ref] = Config::FEE_UNITS_DEPRECATED;
|
||||
jvObj[jss::fee_base] = lpAccepted->fees().base.jsonClipped();
|
||||
@@ -4172,6 +4180,7 @@ NetworkOPsImp::subLedger(InfoSub::ref isrListener, Json::Value& jvResult)
|
||||
jvResult[jss::reserve_base] =
|
||||
lpClosed->fees().accountReserve(0).jsonClipped();
|
||||
jvResult[jss::reserve_inc] = lpClosed->fees().increment.jsonClipped();
|
||||
jvResult[jss::network_id] = app_.config().NETWORK_ID;
|
||||
}
|
||||
|
||||
if ((mMode >= OperatingMode::SYNCING) && !isNeedNetworkLedger())
|
||||
|
||||
@@ -26,9 +26,9 @@
|
||||
#include <xrpl/basics/base_uint.h>
|
||||
#include <xrpl/beast/utility/WrappedSink.h>
|
||||
#include <xrpl/protocol/Feature.h>
|
||||
#include <xrpl/protocol/Quality.h>
|
||||
#include <xrpl/protocol/STAmount.h>
|
||||
#include <xrpl/protocol/TER.h>
|
||||
#include <xrpl/protocol/TxFlags.h>
|
||||
#include <xrpl/protocol/st.h>
|
||||
|
||||
namespace ripple {
|
||||
@@ -311,374 +311,6 @@ CreateOffer::checkAcceptAsset(
|
||||
return tesSUCCESS;
|
||||
}
|
||||
|
||||
bool
|
||||
CreateOffer::dry_offer(ApplyView& view, Offer const& offer)
|
||||
{
|
||||
if (offer.fully_consumed())
|
||||
return true;
|
||||
auto const amount = accountFunds(
|
||||
view,
|
||||
offer.owner(),
|
||||
offer.amount().out,
|
||||
fhZERO_IF_FROZEN,
|
||||
ctx_.app.journal("View"));
|
||||
return (amount <= beast::zero);
|
||||
}
|
||||
|
||||
std::pair<bool, Quality>
|
||||
CreateOffer::select_path(
|
||||
bool have_direct,
|
||||
OfferStream const& direct,
|
||||
bool have_bridge,
|
||||
OfferStream const& leg1,
|
||||
OfferStream const& leg2)
|
||||
{
|
||||
// If we don't have any viable path, why are we here?!
|
||||
XRPL_ASSERT(
|
||||
have_direct || have_bridge,
|
||||
"ripple::CreateOffer::select_path : valid inputs");
|
||||
|
||||
// If there's no bridged path, the direct is the best by default.
|
||||
if (!have_bridge)
|
||||
return std::make_pair(true, direct.tip().quality());
|
||||
|
||||
Quality const bridged_quality(
|
||||
composed_quality(leg1.tip().quality(), leg2.tip().quality()));
|
||||
|
||||
if (have_direct)
|
||||
{
|
||||
// We compare the quality of the composed quality of the bridged
|
||||
// offers and compare it against the direct offer to pick the best.
|
||||
Quality const direct_quality(direct.tip().quality());
|
||||
|
||||
if (bridged_quality < direct_quality)
|
||||
return std::make_pair(true, direct_quality);
|
||||
}
|
||||
|
||||
// Either there was no direct offer, or it didn't have a better quality
|
||||
// than the bridge.
|
||||
return std::make_pair(false, bridged_quality);
|
||||
}
|
||||
|
||||
bool
|
||||
CreateOffer::reachedOfferCrossingLimit(Taker const& taker) const
|
||||
{
|
||||
auto const crossings =
|
||||
taker.get_direct_crossings() + (2 * taker.get_bridge_crossings());
|
||||
|
||||
// The crossing limit is part of the Ripple protocol and
|
||||
// changing it is a transaction-processing change.
|
||||
return crossings >= 850;
|
||||
}
|
||||
|
||||
std::pair<TER, Amounts>
|
||||
CreateOffer::bridged_cross(
|
||||
Taker& taker,
|
||||
ApplyView& view,
|
||||
ApplyView& view_cancel,
|
||||
NetClock::time_point const when)
|
||||
{
|
||||
auto const& takerAmount = taker.original_offer();
|
||||
|
||||
XRPL_ASSERT(
|
||||
!isXRP(takerAmount.in) && !isXRP(takerAmount.out),
|
||||
"ripple::CreateOffer::bridged_cross : neither is XRP");
|
||||
|
||||
if (isXRP(takerAmount.in) || isXRP(takerAmount.out))
|
||||
Throw<std::logic_error>("Bridging with XRP and an endpoint.");
|
||||
|
||||
OfferStream offers_direct(
|
||||
view,
|
||||
view_cancel,
|
||||
Book(taker.issue_in(), taker.issue_out(), std::nullopt),
|
||||
when,
|
||||
stepCounter_,
|
||||
j_);
|
||||
|
||||
OfferStream offers_leg1(
|
||||
view,
|
||||
view_cancel,
|
||||
Book(taker.issue_in(), xrpIssue(), std::nullopt),
|
||||
when,
|
||||
stepCounter_,
|
||||
j_);
|
||||
|
||||
OfferStream offers_leg2(
|
||||
view,
|
||||
view_cancel,
|
||||
Book(xrpIssue(), taker.issue_out(), std::nullopt),
|
||||
when,
|
||||
stepCounter_,
|
||||
j_);
|
||||
|
||||
TER cross_result = tesSUCCESS;
|
||||
|
||||
// Note the subtle distinction here: self-offers encountered in the
|
||||
// bridge are taken, but self-offers encountered in the direct book
|
||||
// are not.
|
||||
bool have_bridge = offers_leg1.step() && offers_leg2.step();
|
||||
bool have_direct = step_account(offers_direct, taker);
|
||||
int count = 0;
|
||||
|
||||
auto viewJ = ctx_.app.journal("View");
|
||||
|
||||
// Modifying the order or logic of the operations in the loop will cause
|
||||
// a protocol breaking change.
|
||||
while (have_direct || have_bridge)
|
||||
{
|
||||
bool leg1_consumed = false;
|
||||
bool leg2_consumed = false;
|
||||
bool direct_consumed = false;
|
||||
|
||||
auto const [use_direct, quality] = select_path(
|
||||
have_direct, offers_direct, have_bridge, offers_leg1, offers_leg2);
|
||||
|
||||
// We are always looking at the best quality; we are done with
|
||||
// crossing as soon as we cross the quality boundary.
|
||||
if (taker.reject(quality))
|
||||
break;
|
||||
|
||||
count++;
|
||||
|
||||
if (use_direct)
|
||||
{
|
||||
if (auto stream = j_.debug())
|
||||
{
|
||||
stream << count << " Direct:";
|
||||
stream << " offer: " << offers_direct.tip();
|
||||
stream << " in: " << offers_direct.tip().amount().in;
|
||||
stream << " out: " << offers_direct.tip().amount().out;
|
||||
stream << " owner: " << offers_direct.tip().owner();
|
||||
stream << " funds: "
|
||||
<< accountFunds(
|
||||
view,
|
||||
offers_direct.tip().owner(),
|
||||
offers_direct.tip().amount().out,
|
||||
fhIGNORE_FREEZE,
|
||||
viewJ);
|
||||
}
|
||||
|
||||
cross_result = taker.cross(offers_direct.tip());
|
||||
|
||||
JLOG(j_.debug()) << "Direct Result: " << transToken(cross_result);
|
||||
|
||||
if (dry_offer(view, offers_direct.tip()))
|
||||
{
|
||||
direct_consumed = true;
|
||||
have_direct = step_account(offers_direct, taker);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (auto stream = j_.debug())
|
||||
{
|
||||
auto const owner1_funds_before = accountFunds(
|
||||
view,
|
||||
offers_leg1.tip().owner(),
|
||||
offers_leg1.tip().amount().out,
|
||||
fhIGNORE_FREEZE,
|
||||
viewJ);
|
||||
|
||||
auto const owner2_funds_before = accountFunds(
|
||||
view,
|
||||
offers_leg2.tip().owner(),
|
||||
offers_leg2.tip().amount().out,
|
||||
fhIGNORE_FREEZE,
|
||||
viewJ);
|
||||
|
||||
stream << count << " Bridge:";
|
||||
stream << " offer1: " << offers_leg1.tip();
|
||||
stream << " in: " << offers_leg1.tip().amount().in;
|
||||
stream << " out: " << offers_leg1.tip().amount().out;
|
||||
stream << " owner: " << offers_leg1.tip().owner();
|
||||
stream << " funds: " << owner1_funds_before;
|
||||
stream << " offer2: " << offers_leg2.tip();
|
||||
stream << " in: " << offers_leg2.tip().amount().in;
|
||||
stream << " out: " << offers_leg2.tip().amount().out;
|
||||
stream << " owner: " << offers_leg2.tip().owner();
|
||||
stream << " funds: " << owner2_funds_before;
|
||||
}
|
||||
|
||||
cross_result = taker.cross(offers_leg1.tip(), offers_leg2.tip());
|
||||
|
||||
JLOG(j_.debug()) << "Bridge Result: " << transToken(cross_result);
|
||||
|
||||
if (view.rules().enabled(fixTakerDryOfferRemoval))
|
||||
{
|
||||
// have_bridge can be true the next time 'round only if
|
||||
// neither of the OfferStreams are dry.
|
||||
leg1_consumed = dry_offer(view, offers_leg1.tip());
|
||||
if (leg1_consumed)
|
||||
have_bridge &= offers_leg1.step();
|
||||
|
||||
leg2_consumed = dry_offer(view, offers_leg2.tip());
|
||||
if (leg2_consumed)
|
||||
have_bridge &= offers_leg2.step();
|
||||
}
|
||||
else
|
||||
{
|
||||
// This old behavior may leave an empty offer in the book for
|
||||
// the second leg.
|
||||
if (dry_offer(view, offers_leg1.tip()))
|
||||
{
|
||||
leg1_consumed = true;
|
||||
have_bridge = (have_bridge && offers_leg1.step());
|
||||
}
|
||||
if (dry_offer(view, offers_leg2.tip()))
|
||||
{
|
||||
leg2_consumed = true;
|
||||
have_bridge = (have_bridge && offers_leg2.step());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (cross_result != tesSUCCESS)
|
||||
{
|
||||
cross_result = tecFAILED_PROCESSING;
|
||||
break;
|
||||
}
|
||||
|
||||
if (taker.done())
|
||||
{
|
||||
JLOG(j_.debug()) << "The taker reports he's done during crossing!";
|
||||
break;
|
||||
}
|
||||
|
||||
if (reachedOfferCrossingLimit(taker))
|
||||
{
|
||||
JLOG(j_.debug()) << "The offer crossing limit has been exceeded!";
|
||||
break;
|
||||
}
|
||||
|
||||
// Postcondition: If we aren't done, then we *must* have consumed at
|
||||
// least one offer fully.
|
||||
XRPL_ASSERT(
|
||||
direct_consumed || leg1_consumed || leg2_consumed,
|
||||
"ripple::CreateOffer::bridged_cross : consumed an offer");
|
||||
|
||||
if (!direct_consumed && !leg1_consumed && !leg2_consumed)
|
||||
Throw<std::logic_error>(
|
||||
"bridged crossing: nothing was fully consumed.");
|
||||
}
|
||||
|
||||
return std::make_pair(cross_result, taker.remaining_offer());
|
||||
}
|
||||
|
||||
std::pair<TER, Amounts>
|
||||
CreateOffer::direct_cross(
|
||||
Taker& taker,
|
||||
ApplyView& view,
|
||||
ApplyView& view_cancel,
|
||||
NetClock::time_point const when)
|
||||
{
|
||||
OfferStream offers(
|
||||
view,
|
||||
view_cancel,
|
||||
Book(taker.issue_in(), taker.issue_out(), std::nullopt),
|
||||
when,
|
||||
stepCounter_,
|
||||
j_);
|
||||
|
||||
TER cross_result(tesSUCCESS);
|
||||
int count = 0;
|
||||
|
||||
bool have_offer = step_account(offers, taker);
|
||||
|
||||
// Modifying the order or logic of the operations in the loop will cause
|
||||
// a protocol breaking change.
|
||||
while (have_offer)
|
||||
{
|
||||
bool direct_consumed = false;
|
||||
auto& offer(offers.tip());
|
||||
|
||||
// We are done with crossing as soon as we cross the quality boundary
|
||||
if (taker.reject(offer.quality()))
|
||||
break;
|
||||
|
||||
count++;
|
||||
|
||||
if (auto stream = j_.debug())
|
||||
{
|
||||
stream << count << " Direct:";
|
||||
stream << " offer: " << offer;
|
||||
stream << " in: " << offer.amount().in;
|
||||
stream << " out: " << offer.amount().out;
|
||||
stream << "quality: " << offer.quality();
|
||||
stream << " owner: " << offer.owner();
|
||||
stream << " funds: "
|
||||
<< accountFunds(
|
||||
view,
|
||||
offer.owner(),
|
||||
offer.amount().out,
|
||||
fhIGNORE_FREEZE,
|
||||
ctx_.app.journal("View"));
|
||||
}
|
||||
|
||||
cross_result = taker.cross(offer);
|
||||
|
||||
JLOG(j_.debug()) << "Direct Result: " << transToken(cross_result);
|
||||
|
||||
if (dry_offer(view, offer))
|
||||
{
|
||||
direct_consumed = true;
|
||||
have_offer = step_account(offers, taker);
|
||||
}
|
||||
|
||||
if (cross_result != tesSUCCESS)
|
||||
{
|
||||
cross_result = tecFAILED_PROCESSING;
|
||||
break;
|
||||
}
|
||||
|
||||
if (taker.done())
|
||||
{
|
||||
JLOG(j_.debug()) << "The taker reports he's done during crossing!";
|
||||
break;
|
||||
}
|
||||
|
||||
if (reachedOfferCrossingLimit(taker))
|
||||
{
|
||||
JLOG(j_.debug()) << "The offer crossing limit has been exceeded!";
|
||||
break;
|
||||
}
|
||||
|
||||
// Postcondition: If we aren't done, then we *must* have consumed the
|
||||
// offer on the books fully!
|
||||
XRPL_ASSERT(
|
||||
direct_consumed,
|
||||
"ripple::CreateOffer::direct_cross : consumed an offer");
|
||||
|
||||
if (!direct_consumed)
|
||||
Throw<std::logic_error>(
|
||||
"direct crossing: nothing was fully consumed.");
|
||||
}
|
||||
|
||||
return std::make_pair(cross_result, taker.remaining_offer());
|
||||
}
|
||||
|
||||
// Step through the stream for as long as possible, skipping any offers
|
||||
// that are from the taker or which cross the taker's threshold.
|
||||
// Return false if the is no offer in the book, true otherwise.
|
||||
bool
|
||||
CreateOffer::step_account(OfferStream& stream, Taker const& taker)
|
||||
{
|
||||
while (stream.step())
|
||||
{
|
||||
auto const& offer = stream.tip();
|
||||
|
||||
// This offer at the tip crosses the taker's threshold. We're done.
|
||||
if (taker.reject(offer.quality()))
|
||||
return true;
|
||||
|
||||
// This offer at the tip is not from the taker. We're done.
|
||||
if (offer.owner() != taker.account())
|
||||
return true;
|
||||
}
|
||||
|
||||
// We ran out of offers. Can't advance.
|
||||
return false;
|
||||
}
|
||||
|
||||
std::pair<TER, Amounts>
|
||||
CreateOffer::flowCross(
|
||||
PaymentSandbox& psb,
|
||||
@@ -883,21 +515,6 @@ CreateOffer::flowCross(
|
||||
return {tecINTERNAL, takerAmount};
|
||||
}
|
||||
|
||||
std::pair<TER, Amounts>
|
||||
CreateOffer::cross(
|
||||
Sandbox& sb,
|
||||
Sandbox& sbCancel,
|
||||
Amounts const& takerAmount,
|
||||
std::optional<uint256> const& domainID)
|
||||
{
|
||||
PaymentSandbox psbFlow{&sb};
|
||||
PaymentSandbox psbCancelFlow{&sbCancel};
|
||||
auto const ret = flowCross(psbFlow, psbCancelFlow, takerAmount, domainID);
|
||||
psbFlow.apply(sb);
|
||||
psbCancelFlow.apply(sbCancel);
|
||||
return ret;
|
||||
}
|
||||
|
||||
std::string
|
||||
CreateOffer::format_amount(STAmount const& amount)
|
||||
{
|
||||
@@ -907,20 +524,6 @@ CreateOffer::format_amount(STAmount const& amount)
|
||||
return txt;
|
||||
}
|
||||
|
||||
void
|
||||
CreateOffer::preCompute()
|
||||
{
|
||||
cross_type_ = CrossType::IouToIou;
|
||||
bool const pays_xrp = ctx_.tx.getFieldAmount(sfTakerPays).native();
|
||||
bool const gets_xrp = ctx_.tx.getFieldAmount(sfTakerGets).native();
|
||||
if (pays_xrp && !gets_xrp)
|
||||
cross_type_ = CrossType::IouToXrp;
|
||||
else if (gets_xrp && !pays_xrp)
|
||||
cross_type_ = CrossType::XrpToIou;
|
||||
|
||||
return Transactor::preCompute();
|
||||
}
|
||||
|
||||
TER
|
||||
CreateOffer::applyHybrid(
|
||||
Sandbox& sb,
|
||||
@@ -1084,11 +687,6 @@ CreateOffer::applyGuts(Sandbox& sb, Sandbox& sbCancel)
|
||||
// We reverse pays and gets because during crossing we are taking.
|
||||
Amounts const takerAmount(saTakerGets, saTakerPays);
|
||||
|
||||
// The amount of the offer that is unfilled after crossing has been
|
||||
// performed. It may be equal to the original amount (didn't cross),
|
||||
// empty (fully crossed), or something in-between.
|
||||
Amounts place_offer;
|
||||
|
||||
JLOG(j_.debug()) << "Attempting cross: "
|
||||
<< to_string(takerAmount.in.issue()) << " -> "
|
||||
<< to_string(takerAmount.out.issue());
|
||||
@@ -1101,8 +699,17 @@ CreateOffer::applyGuts(Sandbox& sb, Sandbox& sbCancel)
|
||||
stream << " out: " << format_amount(takerAmount.out);
|
||||
}
|
||||
|
||||
// The amount of the offer that is unfilled after crossing has been
|
||||
// performed. It may be equal to the original amount (didn't cross),
|
||||
// empty (fully crossed), or something in-between.
|
||||
Amounts place_offer;
|
||||
PaymentSandbox psbFlow{&sb};
|
||||
PaymentSandbox psbCancelFlow{&sbCancel};
|
||||
|
||||
std::tie(result, place_offer) =
|
||||
cross(sb, sbCancel, takerAmount, domainID);
|
||||
flowCross(psbFlow, psbCancelFlow, takerAmount, domainID);
|
||||
psbFlow.apply(sb);
|
||||
psbCancelFlow.apply(sbCancel);
|
||||
|
||||
// We expect the implementation of cross to succeed
|
||||
// or give a tec.
|
||||
|
||||
@@ -20,10 +20,10 @@
|
||||
#ifndef RIPPLE_TX_CREATEOFFER_H_INCLUDED
|
||||
#define RIPPLE_TX_CREATEOFFER_H_INCLUDED
|
||||
|
||||
#include <xrpld/app/tx/detail/OfferStream.h>
|
||||
#include <xrpld/app/tx/detail/Taker.h>
|
||||
#include <xrpld/app/tx/detail/Transactor.h>
|
||||
|
||||
#include <xrpl/protocol/Quality.h>
|
||||
|
||||
namespace ripple {
|
||||
|
||||
class PaymentSandbox;
|
||||
@@ -36,8 +36,7 @@ public:
|
||||
static constexpr ConsequencesFactoryType ConsequencesFactory{Custom};
|
||||
|
||||
/** Construct a Transactor subclass that creates an offer in the ledger. */
|
||||
explicit CreateOffer(ApplyContext& ctx)
|
||||
: Transactor(ctx), stepCounter_(1000, j_)
|
||||
explicit CreateOffer(ApplyContext& ctx) : Transactor(ctx)
|
||||
{
|
||||
}
|
||||
|
||||
@@ -52,10 +51,6 @@ public:
|
||||
static TER
|
||||
preclaim(PreclaimContext const& ctx);
|
||||
|
||||
/** Gather information beyond what the Transactor base class gathers. */
|
||||
void
|
||||
preCompute() override;
|
||||
|
||||
/** Precondition: fee collection is likely. Attempt to create the offer. */
|
||||
TER
|
||||
doApply() override;
|
||||
@@ -73,42 +68,6 @@ private:
|
||||
beast::Journal const j,
|
||||
Issue const& issue);
|
||||
|
||||
bool
|
||||
dry_offer(ApplyView& view, Offer const& offer);
|
||||
|
||||
static std::pair<bool, Quality>
|
||||
select_path(
|
||||
bool have_direct,
|
||||
OfferStream const& direct,
|
||||
bool have_bridge,
|
||||
OfferStream const& leg1,
|
||||
OfferStream const& leg2);
|
||||
|
||||
std::pair<TER, Amounts>
|
||||
bridged_cross(
|
||||
Taker& taker,
|
||||
ApplyView& view,
|
||||
ApplyView& view_cancel,
|
||||
NetClock::time_point const when);
|
||||
|
||||
std::pair<TER, Amounts>
|
||||
direct_cross(
|
||||
Taker& taker,
|
||||
ApplyView& view,
|
||||
ApplyView& view_cancel,
|
||||
NetClock::time_point const when);
|
||||
|
||||
// Step through the stream for as long as possible, skipping any offers
|
||||
// that are from the taker or which cross the taker's threshold.
|
||||
// Return false if the is no offer in the book, true otherwise.
|
||||
static bool
|
||||
step_account(OfferStream& stream, Taker const& taker);
|
||||
|
||||
// True if the number of offers that have been crossed
|
||||
// exceeds the limit.
|
||||
bool
|
||||
reachedOfferCrossingLimit(Taker const& taker) const;
|
||||
|
||||
// Use the payment flow code to perform offer crossing.
|
||||
std::pair<TER, Amounts>
|
||||
flowCross(
|
||||
@@ -117,17 +76,6 @@ private:
|
||||
Amounts const& takerAmount,
|
||||
std::optional<uint256> const& domainID);
|
||||
|
||||
// Temporary
|
||||
// This is a central location that invokes both versions of cross
|
||||
// so the results can be compared. Eventually this layer will be
|
||||
// removed once flowCross is determined to be stable.
|
||||
std::pair<TER, Amounts>
|
||||
cross(
|
||||
Sandbox& sb,
|
||||
Sandbox& sbCancel,
|
||||
Amounts const& takerAmount,
|
||||
std::optional<uint256> const& domainID);
|
||||
|
||||
static std::string
|
||||
format_amount(STAmount const& amount);
|
||||
|
||||
@@ -139,13 +87,6 @@ private:
|
||||
STAmount const& saTakerPays,
|
||||
STAmount const& saTakerGets,
|
||||
std::function<void(SLE::ref, std::optional<uint256>)> const& setDir);
|
||||
|
||||
private:
|
||||
// What kind of offer we are placing
|
||||
CrossType cross_type_;
|
||||
|
||||
// The number of steps to take through order books while crossing
|
||||
OfferStream::StepCounter stepCounter_;
|
||||
};
|
||||
|
||||
using OfferCreate = CreateOffer;
|
||||
|
||||
@@ -34,13 +34,13 @@
|
||||
#include <xrpl/protocol/TxFlags.h>
|
||||
#include <xrpl/protocol/XRPAmount.h>
|
||||
|
||||
namespace ripple {
|
||||
|
||||
// During an EscrowFinish, the transaction must specify both
|
||||
// a condition and a fulfillment. We track whether that
|
||||
// fulfillment matches and validates the condition.
|
||||
#define SF_CF_INVALID SF_PRIVATE5
|
||||
#define SF_CF_VALID SF_PRIVATE6
|
||||
|
||||
namespace ripple {
|
||||
constexpr HashRouterFlags SF_CF_INVALID = HashRouterFlags::PRIVATE5;
|
||||
constexpr HashRouterFlags SF_CF_VALID = HashRouterFlags::PRIVATE6;
|
||||
|
||||
/*
|
||||
Escrow
|
||||
@@ -663,7 +663,7 @@ EscrowFinish::preflight(PreflightContext const& ctx)
|
||||
// If we haven't checked the condition, check it
|
||||
// now. Whether it passes or not isn't important
|
||||
// in preflight.
|
||||
if (!(flags & (SF_CF_INVALID | SF_CF_VALID)))
|
||||
if (!any(flags & (SF_CF_INVALID | SF_CF_VALID)))
|
||||
{
|
||||
if (checkCondition(*fb, *cb))
|
||||
router.setFlags(id, SF_CF_VALID);
|
||||
@@ -1064,7 +1064,7 @@ EscrowFinish::doApply()
|
||||
// It's unlikely that the results of the check will
|
||||
// expire from the hash router, but if it happens,
|
||||
// simply re-run the check.
|
||||
if (cb && !(flags & (SF_CF_INVALID | SF_CF_VALID)))
|
||||
if (cb && !any(flags & (SF_CF_INVALID | SF_CF_VALID)))
|
||||
{
|
||||
auto const fb = ctx_.tx[~sfFulfillment];
|
||||
|
||||
@@ -1081,7 +1081,7 @@ EscrowFinish::doApply()
|
||||
|
||||
// If the check failed, then simply return an error
|
||||
// and don't look at anything else.
|
||||
if (flags & SF_CF_INVALID)
|
||||
if (any(flags & SF_CF_INVALID))
|
||||
return tecCRYPTOCONDITION_ERROR;
|
||||
|
||||
// Check against condition in the ledger entry:
|
||||
|
||||
@@ -31,6 +31,11 @@ MPTokenIssuanceCreate::preflight(PreflightContext const& ctx)
|
||||
if (!ctx.rules.enabled(featureMPTokensV1))
|
||||
return temDISABLED;
|
||||
|
||||
if (ctx.tx.isFieldPresent(sfDomainID) &&
|
||||
!(ctx.rules.enabled(featurePermissionedDomains) &&
|
||||
ctx.rules.enabled(featureSingleAssetVault)))
|
||||
return temDISABLED;
|
||||
|
||||
if (auto const ret = preflight1(ctx); !isTesSuccess(ret))
|
||||
return ret;
|
||||
|
||||
@@ -48,6 +53,16 @@ MPTokenIssuanceCreate::preflight(PreflightContext const& ctx)
|
||||
return temMALFORMED;
|
||||
}
|
||||
|
||||
if (auto const domain = ctx.tx[~sfDomainID])
|
||||
{
|
||||
if (*domain == beast::zero)
|
||||
return temMALFORMED;
|
||||
|
||||
// Domain present implies that MPTokenIssuance is not public
|
||||
if ((ctx.tx.getFlags() & tfMPTRequireAuth) == 0)
|
||||
return temMALFORMED;
|
||||
}
|
||||
|
||||
if (auto const metadata = ctx.tx[~sfMPTokenMetadata])
|
||||
{
|
||||
if (metadata->length() == 0 ||
|
||||
@@ -142,6 +157,7 @@ MPTokenIssuanceCreate::doApply()
|
||||
.assetScale = tx[~sfAssetScale],
|
||||
.transferFee = tx[~sfTransferFee],
|
||||
.metadata = tx[~sfMPTokenMetadata],
|
||||
.domainId = tx[~sfDomainID],
|
||||
});
|
||||
return result ? tesSUCCESS : result.error();
|
||||
}
|
||||
|
||||
@@ -21,6 +21,7 @@
|
||||
#include <xrpld/app/tx/detail/MPTokenIssuanceSet.h>
|
||||
|
||||
#include <xrpl/protocol/Feature.h>
|
||||
#include <xrpl/protocol/LedgerFormats.h>
|
||||
#include <xrpl/protocol/TxFlags.h>
|
||||
|
||||
namespace ripple {
|
||||
@@ -31,6 +32,14 @@ MPTokenIssuanceSet::preflight(PreflightContext const& ctx)
|
||||
if (!ctx.rules.enabled(featureMPTokensV1))
|
||||
return temDISABLED;
|
||||
|
||||
if (ctx.tx.isFieldPresent(sfDomainID) &&
|
||||
!(ctx.rules.enabled(featurePermissionedDomains) &&
|
||||
ctx.rules.enabled(featureSingleAssetVault)))
|
||||
return temDISABLED;
|
||||
|
||||
if (ctx.tx.isFieldPresent(sfDomainID) && ctx.tx.isFieldPresent(sfHolder))
|
||||
return temMALFORMED;
|
||||
|
||||
if (auto const ret = preflight1(ctx); !isTesSuccess(ret))
|
||||
return ret;
|
||||
|
||||
@@ -48,6 +57,13 @@ MPTokenIssuanceSet::preflight(PreflightContext const& ctx)
|
||||
if (holderID && accountID == holderID)
|
||||
return temMALFORMED;
|
||||
|
||||
if (ctx.rules.enabled(featureSingleAssetVault))
|
||||
{
|
||||
// Is this transaction actually changing anything ?
|
||||
if (txFlags == 0 && !ctx.tx.isFieldPresent(sfDomainID))
|
||||
return temMALFORMED;
|
||||
}
|
||||
|
||||
return preflight2(ctx);
|
||||
}
|
||||
|
||||
@@ -97,9 +113,14 @@ MPTokenIssuanceSet::preclaim(PreclaimContext const& ctx)
|
||||
if (!sleMptIssuance)
|
||||
return tecOBJECT_NOT_FOUND;
|
||||
|
||||
// if the mpt has disabled locking
|
||||
if (!((*sleMptIssuance)[sfFlags] & lsfMPTCanLock))
|
||||
return tecNO_PERMISSION;
|
||||
if (!sleMptIssuance->isFlag(lsfMPTCanLock))
|
||||
{
|
||||
// For readability two separate `if` rather than `||` of two conditions
|
||||
if (!ctx.view.rules().enabled(featureSingleAssetVault))
|
||||
return tecNO_PERMISSION;
|
||||
else if (ctx.tx.isFlag(tfMPTLock) || ctx.tx.isFlag(tfMPTUnlock))
|
||||
return tecNO_PERMISSION;
|
||||
}
|
||||
|
||||
// ensure it is issued by the tx submitter
|
||||
if ((*sleMptIssuance)[sfIssuer] != ctx.tx[sfAccount])
|
||||
@@ -117,6 +138,20 @@ MPTokenIssuanceSet::preclaim(PreclaimContext const& ctx)
|
||||
return tecOBJECT_NOT_FOUND;
|
||||
}
|
||||
|
||||
if (auto const domain = ctx.tx[~sfDomainID])
|
||||
{
|
||||
if (not sleMptIssuance->isFlag(lsfMPTRequireAuth))
|
||||
return tecNO_PERMISSION;
|
||||
|
||||
if (*domain != beast::zero)
|
||||
{
|
||||
auto const sleDomain =
|
||||
ctx.view.read(keylet::permissionedDomain(*domain));
|
||||
if (!sleDomain)
|
||||
return tecOBJECT_NOT_FOUND;
|
||||
}
|
||||
}
|
||||
|
||||
return tesSUCCESS;
|
||||
}
|
||||
|
||||
@@ -126,6 +161,7 @@ MPTokenIssuanceSet::doApply()
|
||||
auto const mptIssuanceID = ctx_.tx[sfMPTokenIssuanceID];
|
||||
auto const txFlags = ctx_.tx.getFlags();
|
||||
auto const holderID = ctx_.tx[~sfHolder];
|
||||
auto const domainID = ctx_.tx[~sfDomainID];
|
||||
std::shared_ptr<SLE> sle;
|
||||
|
||||
if (holderID)
|
||||
@@ -147,6 +183,24 @@ MPTokenIssuanceSet::doApply()
|
||||
if (flagsIn != flagsOut)
|
||||
sle->setFieldU32(sfFlags, flagsOut);
|
||||
|
||||
if (domainID)
|
||||
{
|
||||
// This is enforced in preflight.
|
||||
XRPL_ASSERT(
|
||||
sle->getType() == ltMPTOKEN_ISSUANCE,
|
||||
"MPTokenIssuanceSet::doApply : modifying MPTokenIssuance");
|
||||
|
||||
if (*domainID != beast::zero)
|
||||
{
|
||||
sle->setFieldH256(sfDomainID, *domainID);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (sle->isFieldPresent(sfDomainID))
|
||||
sle->makeFieldAbsent(sfDomainID);
|
||||
}
|
||||
}
|
||||
|
||||
view().update(sle);
|
||||
|
||||
return tesSUCCESS;
|
||||
|
||||
@@ -1,863 +0,0 @@
|
||||
//------------------------------------------------------------------------------
|
||||
/*
|
||||
This file is part of rippled: https://github.com/ripple/rippled
|
||||
Copyright (c) 2014 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 <xrpld/app/tx/detail/Taker.h>
|
||||
|
||||
#include <xrpl/basics/Log.h>
|
||||
#include <xrpl/basics/contract.h>
|
||||
|
||||
namespace ripple {
|
||||
|
||||
static std::string
|
||||
format_amount(STAmount const& amount)
|
||||
{
|
||||
std::string txt = amount.getText();
|
||||
txt += "/";
|
||||
txt += to_string(amount.issue().currency);
|
||||
return txt;
|
||||
}
|
||||
|
||||
BasicTaker::BasicTaker(
|
||||
CrossType cross_type,
|
||||
AccountID const& account,
|
||||
Amounts const& amount,
|
||||
Quality const& quality,
|
||||
std::uint32_t flags,
|
||||
Rate const& rate_in,
|
||||
Rate const& rate_out,
|
||||
beast::Journal journal)
|
||||
: account_(account)
|
||||
, quality_(quality)
|
||||
, threshold_(quality_)
|
||||
, sell_(flags & tfSell)
|
||||
, original_(amount)
|
||||
, remaining_(amount)
|
||||
, issue_in_(remaining_.in.issue())
|
||||
, issue_out_(remaining_.out.issue())
|
||||
, m_rate_in(rate_in)
|
||||
, m_rate_out(rate_out)
|
||||
, cross_type_(cross_type)
|
||||
, journal_(journal)
|
||||
{
|
||||
XRPL_ASSERT(
|
||||
remaining_.in > beast::zero,
|
||||
"ripple::BasicTaker::BasicTaker : positive remaining in");
|
||||
XRPL_ASSERT(
|
||||
remaining_.out > beast::zero,
|
||||
"ripple::BasicTaker::BasicTaker : positive remaining out");
|
||||
|
||||
XRPL_ASSERT(
|
||||
m_rate_in.value, "ripple::BasicTaker::BasicTaker : nonzero rate in");
|
||||
XRPL_ASSERT(
|
||||
m_rate_out.value, "ripple::BasicTaker::BasicTaker : nonzero rate out");
|
||||
|
||||
// If we are dealing with a particular flavor, make sure that it's the
|
||||
// flavor we expect:
|
||||
XRPL_ASSERT(
|
||||
cross_type != CrossType::XrpToIou ||
|
||||
(isXRP(issue_in()) && !isXRP(issue_out())),
|
||||
"ripple::BasicTaker::BasicTaker : valid cross to IOU");
|
||||
|
||||
XRPL_ASSERT(
|
||||
cross_type != CrossType::IouToXrp ||
|
||||
(!isXRP(issue_in()) && isXRP(issue_out())),
|
||||
"ripple::BasicTaker::BasicTaker : valid cross to XRP");
|
||||
|
||||
// And make sure we're not crossing XRP for XRP
|
||||
XRPL_ASSERT(
|
||||
!isXRP(issue_in()) || !isXRP(issue_out()),
|
||||
"ripple::BasicTaker::BasicTaker : not crossing XRP for XRP");
|
||||
|
||||
// If this is a passive order, we adjust the quality so as to prevent offers
|
||||
// at the same quality level from being consumed.
|
||||
if (flags & tfPassive)
|
||||
++threshold_;
|
||||
}
|
||||
|
||||
Rate
|
||||
BasicTaker::effective_rate(
|
||||
Rate const& rate,
|
||||
Issue const& issue,
|
||||
AccountID const& from,
|
||||
AccountID const& to)
|
||||
{
|
||||
// If there's a transfer rate, the issuer is not involved
|
||||
// and the sender isn't the same as the recipient, return
|
||||
// the actual transfer rate.
|
||||
if (rate != parityRate && from != to && from != issue.account &&
|
||||
to != issue.account)
|
||||
{
|
||||
return rate;
|
||||
}
|
||||
|
||||
return parityRate;
|
||||
}
|
||||
|
||||
bool
|
||||
BasicTaker::unfunded() const
|
||||
{
|
||||
if (get_funds(account(), remaining_.in) > beast::zero)
|
||||
return false;
|
||||
|
||||
JLOG(journal_.debug()) << "Unfunded: taker is out of funds.";
|
||||
return true;
|
||||
}
|
||||
|
||||
bool
|
||||
BasicTaker::done() const
|
||||
{
|
||||
// We are done if we have consumed all the input currency
|
||||
if (remaining_.in <= beast::zero)
|
||||
{
|
||||
JLOG(journal_.debug())
|
||||
<< "Done: all the input currency has been consumed.";
|
||||
return true;
|
||||
}
|
||||
|
||||
// We are done if using buy semantics and we received the
|
||||
// desired amount of output currency
|
||||
if (!sell_ && (remaining_.out <= beast::zero))
|
||||
{
|
||||
JLOG(journal_.debug()) << "Done: the desired amount has been received.";
|
||||
return true;
|
||||
}
|
||||
|
||||
// We are done if the taker is out of funds
|
||||
if (unfunded())
|
||||
{
|
||||
JLOG(journal_.debug()) << "Done: taker out of funds.";
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
Amounts
|
||||
BasicTaker::remaining_offer() const
|
||||
{
|
||||
// If the taker is done, then there's no offer to place.
|
||||
if (done())
|
||||
return Amounts(remaining_.in.zeroed(), remaining_.out.zeroed());
|
||||
|
||||
// Avoid math altogether if we didn't cross.
|
||||
if (original_ == remaining_)
|
||||
return original_;
|
||||
|
||||
if (sell_)
|
||||
{
|
||||
XRPL_ASSERT(
|
||||
remaining_.in > beast::zero,
|
||||
"ripple::BasicTaker::remaining_offer : positive remaining in");
|
||||
|
||||
// We scale the output based on the remaining input:
|
||||
return Amounts(
|
||||
remaining_.in,
|
||||
divRound(remaining_.in, quality_.rate(), issue_out_, true));
|
||||
}
|
||||
|
||||
XRPL_ASSERT(
|
||||
remaining_.out > beast::zero,
|
||||
"ripple::BasicTaker::remaining_offer : positive remaining out");
|
||||
|
||||
// We scale the input based on the remaining output:
|
||||
return Amounts(
|
||||
mulRound(remaining_.out, quality_.rate(), issue_in_, true),
|
||||
remaining_.out);
|
||||
}
|
||||
|
||||
Amounts const&
|
||||
BasicTaker::original_offer() const
|
||||
{
|
||||
return original_;
|
||||
}
|
||||
|
||||
// TODO: the presence of 'output' is an artifact caused by the fact that
|
||||
// Amounts carry issue information which should be decoupled.
|
||||
static STAmount
|
||||
qual_div(STAmount const& amount, Quality const& quality, STAmount const& output)
|
||||
{
|
||||
auto result = divide(amount, quality.rate(), output.issue());
|
||||
return std::min(result, output);
|
||||
}
|
||||
|
||||
static STAmount
|
||||
qual_mul(STAmount const& amount, Quality const& quality, STAmount const& output)
|
||||
{
|
||||
auto result = multiply(amount, quality.rate(), output.issue());
|
||||
return std::min(result, output);
|
||||
}
|
||||
|
||||
void
|
||||
BasicTaker::log_flow(char const* description, Flow const& flow)
|
||||
{
|
||||
auto stream = journal_.debug();
|
||||
if (!stream)
|
||||
return;
|
||||
|
||||
stream << description;
|
||||
|
||||
if (isXRP(issue_in()))
|
||||
stream << " order in: " << format_amount(flow.order.in);
|
||||
else
|
||||
stream << " order in: " << format_amount(flow.order.in)
|
||||
<< " (issuer: " << format_amount(flow.issuers.in) << ")";
|
||||
|
||||
if (isXRP(issue_out()))
|
||||
stream << " order out: " << format_amount(flow.order.out);
|
||||
else
|
||||
stream << " order out: " << format_amount(flow.order.out)
|
||||
<< " (issuer: " << format_amount(flow.issuers.out) << ")";
|
||||
}
|
||||
|
||||
BasicTaker::Flow
|
||||
BasicTaker::flow_xrp_to_iou(
|
||||
Amounts const& order,
|
||||
Quality quality,
|
||||
STAmount const& owner_funds,
|
||||
STAmount const& taker_funds,
|
||||
Rate const& rate_out)
|
||||
{
|
||||
Flow f;
|
||||
f.order = order;
|
||||
f.issuers.out = multiply(f.order.out, rate_out);
|
||||
|
||||
log_flow("flow_xrp_to_iou", f);
|
||||
|
||||
// Clamp on owner balance
|
||||
if (owner_funds < f.issuers.out)
|
||||
{
|
||||
f.issuers.out = owner_funds;
|
||||
f.order.out = divide(f.issuers.out, rate_out);
|
||||
f.order.in = qual_mul(f.order.out, quality, f.order.in);
|
||||
log_flow("(clamped on owner balance)", f);
|
||||
}
|
||||
|
||||
// Clamp if taker wants to limit the output
|
||||
if (!sell_ && remaining_.out < f.order.out)
|
||||
{
|
||||
f.order.out = remaining_.out;
|
||||
f.order.in = qual_mul(f.order.out, quality, f.order.in);
|
||||
f.issuers.out = multiply(f.order.out, rate_out);
|
||||
log_flow("(clamped on taker output)", f);
|
||||
}
|
||||
|
||||
// Clamp on the taker's funds
|
||||
if (taker_funds < f.order.in)
|
||||
{
|
||||
f.order.in = taker_funds;
|
||||
f.order.out = qual_div(f.order.in, quality, f.order.out);
|
||||
f.issuers.out = multiply(f.order.out, rate_out);
|
||||
log_flow("(clamped on taker funds)", f);
|
||||
}
|
||||
|
||||
// Clamp on remaining offer if we are not handling the second leg
|
||||
// of an autobridge.
|
||||
if (cross_type_ == CrossType::XrpToIou && (remaining_.in < f.order.in))
|
||||
{
|
||||
f.order.in = remaining_.in;
|
||||
f.order.out = qual_div(f.order.in, quality, f.order.out);
|
||||
f.issuers.out = multiply(f.order.out, rate_out);
|
||||
log_flow("(clamped on taker input)", f);
|
||||
}
|
||||
|
||||
return f;
|
||||
}
|
||||
|
||||
BasicTaker::Flow
|
||||
BasicTaker::flow_iou_to_xrp(
|
||||
Amounts const& order,
|
||||
Quality quality,
|
||||
STAmount const& owner_funds,
|
||||
STAmount const& taker_funds,
|
||||
Rate const& rate_in)
|
||||
{
|
||||
Flow f;
|
||||
f.order = order;
|
||||
f.issuers.in = multiply(f.order.in, rate_in);
|
||||
|
||||
log_flow("flow_iou_to_xrp", f);
|
||||
|
||||
// Clamp on owner's funds
|
||||
if (owner_funds < f.order.out)
|
||||
{
|
||||
f.order.out = owner_funds;
|
||||
f.order.in = qual_mul(f.order.out, quality, f.order.in);
|
||||
f.issuers.in = multiply(f.order.in, rate_in);
|
||||
log_flow("(clamped on owner funds)", f);
|
||||
}
|
||||
|
||||
// Clamp if taker wants to limit the output and we are not the
|
||||
// first leg of an autobridge.
|
||||
if (!sell_ && cross_type_ == CrossType::IouToXrp)
|
||||
{
|
||||
if (remaining_.out < f.order.out)
|
||||
{
|
||||
f.order.out = remaining_.out;
|
||||
f.order.in = qual_mul(f.order.out, quality, f.order.in);
|
||||
f.issuers.in = multiply(f.order.in, rate_in);
|
||||
log_flow("(clamped on taker output)", f);
|
||||
}
|
||||
}
|
||||
|
||||
// Clamp on the taker's input offer
|
||||
if (remaining_.in < f.order.in)
|
||||
{
|
||||
f.order.in = remaining_.in;
|
||||
f.issuers.in = multiply(f.order.in, rate_in);
|
||||
f.order.out = qual_div(f.order.in, quality, f.order.out);
|
||||
log_flow("(clamped on taker input)", f);
|
||||
}
|
||||
|
||||
// Clamp on the taker's input balance
|
||||
if (taker_funds < f.issuers.in)
|
||||
{
|
||||
f.issuers.in = taker_funds;
|
||||
f.order.in = divide(f.issuers.in, rate_in);
|
||||
f.order.out = qual_div(f.order.in, quality, f.order.out);
|
||||
log_flow("(clamped on taker funds)", f);
|
||||
}
|
||||
|
||||
return f;
|
||||
}
|
||||
|
||||
BasicTaker::Flow
|
||||
BasicTaker::flow_iou_to_iou(
|
||||
Amounts const& order,
|
||||
Quality quality,
|
||||
STAmount const& owner_funds,
|
||||
STAmount const& taker_funds,
|
||||
Rate const& rate_in,
|
||||
Rate const& rate_out)
|
||||
{
|
||||
Flow f;
|
||||
f.order = order;
|
||||
f.issuers.in = multiply(f.order.in, rate_in);
|
||||
f.issuers.out = multiply(f.order.out, rate_out);
|
||||
|
||||
log_flow("flow_iou_to_iou", f);
|
||||
|
||||
// Clamp on owner balance
|
||||
if (owner_funds < f.issuers.out)
|
||||
{
|
||||
f.issuers.out = owner_funds;
|
||||
f.order.out = divide(f.issuers.out, rate_out);
|
||||
f.order.in = qual_mul(f.order.out, quality, f.order.in);
|
||||
f.issuers.in = multiply(f.order.in, rate_in);
|
||||
log_flow("(clamped on owner funds)", f);
|
||||
}
|
||||
|
||||
// Clamp on taker's offer
|
||||
if (!sell_ && remaining_.out < f.order.out)
|
||||
{
|
||||
f.order.out = remaining_.out;
|
||||
f.order.in = qual_mul(f.order.out, quality, f.order.in);
|
||||
f.issuers.out = multiply(f.order.out, rate_out);
|
||||
f.issuers.in = multiply(f.order.in, rate_in);
|
||||
log_flow("(clamped on taker output)", f);
|
||||
}
|
||||
|
||||
// Clamp on the taker's input offer
|
||||
if (remaining_.in < f.order.in)
|
||||
{
|
||||
f.order.in = remaining_.in;
|
||||
f.issuers.in = multiply(f.order.in, rate_in);
|
||||
f.order.out = qual_div(f.order.in, quality, f.order.out);
|
||||
f.issuers.out = multiply(f.order.out, rate_out);
|
||||
log_flow("(clamped on taker input)", f);
|
||||
}
|
||||
|
||||
// Clamp on the taker's input balance
|
||||
if (taker_funds < f.issuers.in)
|
||||
{
|
||||
f.issuers.in = taker_funds;
|
||||
f.order.in = divide(f.issuers.in, rate_in);
|
||||
f.order.out = qual_div(f.order.in, quality, f.order.out);
|
||||
f.issuers.out = multiply(f.order.out, rate_out);
|
||||
log_flow("(clamped on taker funds)", f);
|
||||
}
|
||||
|
||||
return f;
|
||||
}
|
||||
|
||||
// Calculates the direct flow through the specified offer
|
||||
BasicTaker::Flow
|
||||
BasicTaker::do_cross(Amounts offer, Quality quality, AccountID const& owner)
|
||||
{
|
||||
auto const owner_funds = get_funds(owner, offer.out);
|
||||
auto const taker_funds = get_funds(account(), offer.in);
|
||||
|
||||
Flow result;
|
||||
|
||||
if (cross_type_ == CrossType::XrpToIou)
|
||||
{
|
||||
result = flow_xrp_to_iou(
|
||||
offer,
|
||||
quality,
|
||||
owner_funds,
|
||||
taker_funds,
|
||||
out_rate(owner, account()));
|
||||
}
|
||||
else if (cross_type_ == CrossType::IouToXrp)
|
||||
{
|
||||
result = flow_iou_to_xrp(
|
||||
offer,
|
||||
quality,
|
||||
owner_funds,
|
||||
taker_funds,
|
||||
in_rate(owner, account()));
|
||||
}
|
||||
else
|
||||
{
|
||||
result = flow_iou_to_iou(
|
||||
offer,
|
||||
quality,
|
||||
owner_funds,
|
||||
taker_funds,
|
||||
in_rate(owner, account()),
|
||||
out_rate(owner, account()));
|
||||
}
|
||||
|
||||
if (!result.sanity_check())
|
||||
Throw<std::logic_error>("Computed flow fails sanity check.");
|
||||
|
||||
remaining_.out -= result.order.out;
|
||||
remaining_.in -= result.order.in;
|
||||
|
||||
XRPL_ASSERT(
|
||||
remaining_.in >= beast::zero,
|
||||
"ripple::BasicTaker::do_cross : minimum remaining in");
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
// Calculates the bridged flow through the specified offers
|
||||
std::pair<BasicTaker::Flow, BasicTaker::Flow>
|
||||
BasicTaker::do_cross(
|
||||
Amounts offer1,
|
||||
Quality quality1,
|
||||
AccountID const& owner1,
|
||||
Amounts offer2,
|
||||
Quality quality2,
|
||||
AccountID const& owner2)
|
||||
{
|
||||
XRPL_ASSERT(
|
||||
!offer1.in.native(),
|
||||
"ripple::BasicTaker::do_cross : offer1 in is not XRP");
|
||||
XRPL_ASSERT(
|
||||
offer1.out.native(),
|
||||
"ripple::BasicTaker::do_cross : offer1 out is XRP");
|
||||
XRPL_ASSERT(
|
||||
offer2.in.native(), "ripple::BasicTaker::do_cross : offer2 in is XRP");
|
||||
XRPL_ASSERT(
|
||||
!offer2.out.native(),
|
||||
"ripple::BasicTaker::do_cross : offer2 out is not XRP");
|
||||
|
||||
// If the taker owns the first leg of the offer, then the taker's available
|
||||
// funds aren't the limiting factor for the input - the offer itself is.
|
||||
auto leg1_in_funds = get_funds(account(), offer1.in);
|
||||
|
||||
if (account() == owner1)
|
||||
{
|
||||
JLOG(journal_.trace()) << "The taker owns the first leg of a bridge.";
|
||||
leg1_in_funds = std::max(leg1_in_funds, offer1.in);
|
||||
}
|
||||
|
||||
// If the taker owns the second leg of the offer, then the taker's available
|
||||
// funds are not the limiting factor for the output - the offer itself is.
|
||||
auto leg2_out_funds = get_funds(owner2, offer2.out);
|
||||
|
||||
if (account() == owner2)
|
||||
{
|
||||
JLOG(journal_.trace()) << "The taker owns the second leg of a bridge.";
|
||||
leg2_out_funds = std::max(leg2_out_funds, offer2.out);
|
||||
}
|
||||
|
||||
// The amount available to flow via XRP is the amount that the owner of the
|
||||
// first leg of the bridge has, up to the first leg's output.
|
||||
//
|
||||
// But, when both legs of a bridge are owned by the same person, the amount
|
||||
// of XRP that can flow between the two legs is, essentially, infinite
|
||||
// since all the owner is doing is taking out XRP of his left pocket
|
||||
// and putting it in his right pocket. In that case, we set the available
|
||||
// XRP to the largest of the two offers.
|
||||
auto xrp_funds = get_funds(owner1, offer1.out);
|
||||
|
||||
if (owner1 == owner2)
|
||||
{
|
||||
JLOG(journal_.trace())
|
||||
<< "The bridge endpoints are owned by the same account.";
|
||||
xrp_funds = std::max(offer1.out, offer2.in);
|
||||
}
|
||||
|
||||
if (auto stream = journal_.debug())
|
||||
{
|
||||
stream << "Available bridge funds:";
|
||||
stream << " leg1 in: " << format_amount(leg1_in_funds);
|
||||
stream << " leg2 out: " << format_amount(leg2_out_funds);
|
||||
stream << " xrp: " << format_amount(xrp_funds);
|
||||
}
|
||||
|
||||
auto const leg1_rate = in_rate(owner1, account());
|
||||
auto const leg2_rate = out_rate(owner2, account());
|
||||
|
||||
// Attempt to determine the maximal flow that can be achieved across each
|
||||
// leg independent of the other.
|
||||
auto flow1 =
|
||||
flow_iou_to_xrp(offer1, quality1, xrp_funds, leg1_in_funds, leg1_rate);
|
||||
|
||||
if (!flow1.sanity_check())
|
||||
Throw<std::logic_error>("Computed flow1 fails sanity check.");
|
||||
|
||||
auto flow2 =
|
||||
flow_xrp_to_iou(offer2, quality2, leg2_out_funds, xrp_funds, leg2_rate);
|
||||
|
||||
if (!flow2.sanity_check())
|
||||
Throw<std::logic_error>("Computed flow2 fails sanity check.");
|
||||
|
||||
// We now have the maximal flows across each leg individually. We need to
|
||||
// equalize them, so that the amount of XRP that flows out of the first leg
|
||||
// is the same as the amount of XRP that flows into the second leg. We take
|
||||
// the side which is the limiting factor (if any) and adjust the other.
|
||||
if (flow1.order.out < flow2.order.in)
|
||||
{
|
||||
// Adjust the second leg of the offer down:
|
||||
flow2.order.in = flow1.order.out;
|
||||
flow2.order.out = qual_div(flow2.order.in, quality2, flow2.order.out);
|
||||
flow2.issuers.out = multiply(flow2.order.out, leg2_rate);
|
||||
log_flow("Balancing: adjusted second leg down", flow2);
|
||||
}
|
||||
else if (flow1.order.out > flow2.order.in)
|
||||
{
|
||||
// Adjust the first leg of the offer down:
|
||||
flow1.order.out = flow2.order.in;
|
||||
flow1.order.in = qual_mul(flow1.order.out, quality1, flow1.order.in);
|
||||
flow1.issuers.in = multiply(flow1.order.in, leg1_rate);
|
||||
log_flow("Balancing: adjusted first leg down", flow2);
|
||||
}
|
||||
|
||||
if (flow1.order.out != flow2.order.in)
|
||||
Throw<std::logic_error>("Bridged flow is out of balance.");
|
||||
|
||||
remaining_.out -= flow2.order.out;
|
||||
remaining_.in -= flow1.order.in;
|
||||
|
||||
return std::make_pair(flow1, flow2);
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
|
||||
Taker::Taker(
|
||||
CrossType cross_type,
|
||||
ApplyView& view,
|
||||
AccountID const& account,
|
||||
Amounts const& offer,
|
||||
std::uint32_t flags,
|
||||
beast::Journal journal)
|
||||
: BasicTaker(
|
||||
cross_type,
|
||||
account,
|
||||
offer,
|
||||
Quality(offer),
|
||||
flags,
|
||||
calculateRate(view, offer.in.getIssuer(), account),
|
||||
calculateRate(view, offer.out.getIssuer(), account),
|
||||
journal)
|
||||
, view_(view)
|
||||
, xrp_flow_(0)
|
||||
, direct_crossings_(0)
|
||||
, bridge_crossings_(0)
|
||||
{
|
||||
XRPL_ASSERT(
|
||||
issue_in() == offer.in.issue(),
|
||||
"ripple::Taker::Taker : issue in is a match");
|
||||
XRPL_ASSERT(
|
||||
issue_out() == offer.out.issue(),
|
||||
"ripple::Taker::Taker : issue out is a match");
|
||||
|
||||
if (auto stream = journal_.debug())
|
||||
{
|
||||
stream << "Crossing as: " << to_string(account);
|
||||
|
||||
if (isXRP(issue_in()))
|
||||
stream << " Offer in: " << format_amount(offer.in);
|
||||
else
|
||||
stream << " Offer in: " << format_amount(offer.in)
|
||||
<< " (issuer: " << issue_in().account << ")";
|
||||
|
||||
if (isXRP(issue_out()))
|
||||
stream << " Offer out: " << format_amount(offer.out);
|
||||
else
|
||||
stream << " Offer out: " << format_amount(offer.out)
|
||||
<< " (issuer: " << issue_out().account << ")";
|
||||
|
||||
stream << " Balance: "
|
||||
<< format_amount(get_funds(account, offer.in));
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
Taker::consume_offer(Offer& offer, Amounts const& order)
|
||||
{
|
||||
if (order.in < beast::zero)
|
||||
Throw<std::logic_error>("flow with negative input.");
|
||||
|
||||
if (order.out < beast::zero)
|
||||
Throw<std::logic_error>("flow with negative output.");
|
||||
|
||||
JLOG(journal_.debug()) << "Consuming from offer " << offer;
|
||||
|
||||
if (auto stream = journal_.trace())
|
||||
{
|
||||
auto const& available = offer.amount();
|
||||
|
||||
stream << " in:" << format_amount(available.in);
|
||||
stream << " out:" << format_amount(available.out);
|
||||
}
|
||||
|
||||
offer.consume(view_, order);
|
||||
}
|
||||
|
||||
STAmount
|
||||
Taker::get_funds(AccountID const& account, STAmount const& amount) const
|
||||
{
|
||||
return accountFunds(view_, account, amount, fhZERO_IF_FROZEN, journal_);
|
||||
}
|
||||
|
||||
TER
|
||||
Taker::transferXRP(
|
||||
AccountID const& from,
|
||||
AccountID const& to,
|
||||
STAmount const& amount)
|
||||
{
|
||||
if (!isXRP(amount))
|
||||
Throw<std::logic_error>("Using transferXRP with IOU");
|
||||
|
||||
if (from == to)
|
||||
return tesSUCCESS;
|
||||
|
||||
// Transferring zero is equivalent to not doing a transfer
|
||||
if (amount == beast::zero)
|
||||
return tesSUCCESS;
|
||||
|
||||
return ripple::transferXRP(view_, from, to, amount, journal_);
|
||||
}
|
||||
|
||||
TER
|
||||
Taker::redeemIOU(
|
||||
AccountID const& account,
|
||||
STAmount const& amount,
|
||||
Issue const& issue)
|
||||
{
|
||||
if (isXRP(amount))
|
||||
Throw<std::logic_error>("Using redeemIOU with XRP");
|
||||
|
||||
if (account == issue.account)
|
||||
return tesSUCCESS;
|
||||
|
||||
// Transferring zero is equivalent to not doing a transfer
|
||||
if (amount == beast::zero)
|
||||
return tesSUCCESS;
|
||||
|
||||
// If we are trying to redeem some amount, then the account
|
||||
// must have a credit balance.
|
||||
if (get_funds(account, amount) <= beast::zero)
|
||||
Throw<std::logic_error>("redeemIOU has no funds to redeem");
|
||||
|
||||
auto ret = ripple::redeemIOU(view_, account, amount, issue, journal_);
|
||||
|
||||
if (get_funds(account, amount) < beast::zero)
|
||||
Throw<std::logic_error>("redeemIOU redeemed more funds than available");
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
TER
|
||||
Taker::issueIOU(
|
||||
AccountID const& account,
|
||||
STAmount const& amount,
|
||||
Issue const& issue)
|
||||
{
|
||||
if (isXRP(amount))
|
||||
Throw<std::logic_error>("Using issueIOU with XRP");
|
||||
|
||||
if (account == issue.account)
|
||||
return tesSUCCESS;
|
||||
|
||||
// Transferring zero is equivalent to not doing a transfer
|
||||
if (amount == beast::zero)
|
||||
return tesSUCCESS;
|
||||
|
||||
return ripple::issueIOU(view_, account, amount, issue, journal_);
|
||||
}
|
||||
|
||||
// Performs funds transfers to fill the given offer and adjusts offer.
|
||||
TER
|
||||
Taker::fill(BasicTaker::Flow const& flow, Offer& offer)
|
||||
{
|
||||
// adjust offer
|
||||
consume_offer(offer, flow.order);
|
||||
|
||||
TER result = tesSUCCESS;
|
||||
|
||||
if (cross_type() != CrossType::XrpToIou)
|
||||
{
|
||||
XRPL_ASSERT(
|
||||
!isXRP(flow.order.in), "ripple::Taker::fill : order in is not XRP");
|
||||
|
||||
if (result == tesSUCCESS)
|
||||
result =
|
||||
redeemIOU(account(), flow.issuers.in, flow.issuers.in.issue());
|
||||
|
||||
if (result == tesSUCCESS)
|
||||
result =
|
||||
issueIOU(offer.owner(), flow.order.in, flow.order.in.issue());
|
||||
}
|
||||
else
|
||||
{
|
||||
XRPL_ASSERT(
|
||||
isXRP(flow.order.in), "ripple::Taker::fill : order in is XRP");
|
||||
|
||||
if (result == tesSUCCESS)
|
||||
result = transferXRP(account(), offer.owner(), flow.order.in);
|
||||
}
|
||||
|
||||
// Now send funds from the account whose offer we're taking
|
||||
if (cross_type() != CrossType::IouToXrp)
|
||||
{
|
||||
XRPL_ASSERT(
|
||||
!isXRP(flow.order.out),
|
||||
"ripple::Taker::fill : order out is not XRP");
|
||||
|
||||
if (result == tesSUCCESS)
|
||||
result = redeemIOU(
|
||||
offer.owner(), flow.issuers.out, flow.issuers.out.issue());
|
||||
|
||||
if (result == tesSUCCESS)
|
||||
result =
|
||||
issueIOU(account(), flow.order.out, flow.order.out.issue());
|
||||
}
|
||||
else
|
||||
{
|
||||
XRPL_ASSERT(
|
||||
isXRP(flow.order.out), "ripple::Taker::fill : order out is XRP");
|
||||
|
||||
if (result == tesSUCCESS)
|
||||
result = transferXRP(offer.owner(), account(), flow.order.out);
|
||||
}
|
||||
|
||||
if (result == tesSUCCESS)
|
||||
direct_crossings_++;
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
// Performs bridged funds transfers to fill the given offers and adjusts offers.
|
||||
TER
|
||||
Taker::fill(
|
||||
BasicTaker::Flow const& flow1,
|
||||
Offer& leg1,
|
||||
BasicTaker::Flow const& flow2,
|
||||
Offer& leg2)
|
||||
{
|
||||
// Adjust offers accordingly
|
||||
consume_offer(leg1, flow1.order);
|
||||
consume_offer(leg2, flow2.order);
|
||||
|
||||
TER result = tesSUCCESS;
|
||||
|
||||
// Taker to leg1: IOU
|
||||
if (leg1.owner() != account())
|
||||
{
|
||||
if (result == tesSUCCESS)
|
||||
result = redeemIOU(
|
||||
account(), flow1.issuers.in, flow1.issuers.in.issue());
|
||||
|
||||
if (result == tesSUCCESS)
|
||||
result =
|
||||
issueIOU(leg1.owner(), flow1.order.in, flow1.order.in.issue());
|
||||
}
|
||||
|
||||
// leg1 to leg2: bridging over XRP
|
||||
if (result == tesSUCCESS)
|
||||
result = transferXRP(leg1.owner(), leg2.owner(), flow1.order.out);
|
||||
|
||||
// leg2 to Taker: IOU
|
||||
if (leg2.owner() != account())
|
||||
{
|
||||
if (result == tesSUCCESS)
|
||||
result = redeemIOU(
|
||||
leg2.owner(), flow2.issuers.out, flow2.issuers.out.issue());
|
||||
|
||||
if (result == tesSUCCESS)
|
||||
result =
|
||||
issueIOU(account(), flow2.order.out, flow2.order.out.issue());
|
||||
}
|
||||
|
||||
if (result == tesSUCCESS)
|
||||
{
|
||||
bridge_crossings_++;
|
||||
xrp_flow_ += flow1.order.out;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
TER
|
||||
Taker::cross(Offer& offer)
|
||||
{
|
||||
// In direct crossings, at least one leg must not be XRP.
|
||||
if (isXRP(offer.amount().in) && isXRP(offer.amount().out))
|
||||
return tefINTERNAL;
|
||||
|
||||
auto const amount =
|
||||
do_cross(offer.amount(), offer.quality(), offer.owner());
|
||||
|
||||
return fill(amount, offer);
|
||||
}
|
||||
|
||||
TER
|
||||
Taker::cross(Offer& leg1, Offer& leg2)
|
||||
{
|
||||
// In bridged crossings, XRP must can't be the input to the first leg
|
||||
// or the output of the second leg.
|
||||
if (isXRP(leg1.amount().in) || isXRP(leg2.amount().out))
|
||||
return tefINTERNAL;
|
||||
|
||||
auto ret = do_cross(
|
||||
leg1.amount(),
|
||||
leg1.quality(),
|
||||
leg1.owner(),
|
||||
leg2.amount(),
|
||||
leg2.quality(),
|
||||
leg2.owner());
|
||||
|
||||
return fill(ret.first, leg1, ret.second, leg2);
|
||||
}
|
||||
|
||||
Rate
|
||||
Taker::calculateRate(
|
||||
ApplyView const& view,
|
||||
AccountID const& issuer,
|
||||
AccountID const& account)
|
||||
{
|
||||
return isXRP(issuer) || (account == issuer) ? parityRate
|
||||
: transferRate(view, issuer);
|
||||
}
|
||||
|
||||
} // namespace ripple
|
||||
@@ -1,341 +0,0 @@
|
||||
//------------------------------------------------------------------------------
|
||||
/*
|
||||
This file is part of rippled: https://github.com/ripple/rippled
|
||||
Copyright (c) 2014 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.
|
||||
*/
|
||||
//==============================================================================
|
||||
|
||||
#ifndef RIPPLE_APP_BOOK_TAKER_H_INCLUDED
|
||||
#define RIPPLE_APP_BOOK_TAKER_H_INCLUDED
|
||||
|
||||
#include <xrpld/app/tx/detail/Offer.h>
|
||||
#include <xrpld/ledger/View.h>
|
||||
|
||||
#include <xrpl/beast/utility/Journal.h>
|
||||
#include <xrpl/protocol/Quality.h>
|
||||
#include <xrpl/protocol/Rate.h>
|
||||
#include <xrpl/protocol/TER.h>
|
||||
#include <xrpl/protocol/TxFlags.h>
|
||||
|
||||
namespace ripple {
|
||||
|
||||
/** The flavor of an offer crossing */
|
||||
enum class CrossType { XrpToIou, IouToXrp, IouToIou };
|
||||
|
||||
/** State for the active party during order book or payment operations. */
|
||||
class BasicTaker
|
||||
{
|
||||
private:
|
||||
AccountID account_;
|
||||
Quality quality_;
|
||||
Quality threshold_;
|
||||
|
||||
bool sell_;
|
||||
|
||||
// The original in and out quantities.
|
||||
Amounts const original_;
|
||||
|
||||
// The amounts still left over for us to try and take.
|
||||
Amounts remaining_;
|
||||
|
||||
// The issuers for the input and output
|
||||
Issue const& issue_in_;
|
||||
Issue const& issue_out_;
|
||||
|
||||
// The rates that will be paid when the input and output currencies are
|
||||
// transfered and the currency issuer isn't involved:
|
||||
Rate const m_rate_in;
|
||||
Rate const m_rate_out;
|
||||
|
||||
// The type of crossing that we are performing
|
||||
CrossType cross_type_;
|
||||
|
||||
protected:
|
||||
beast::Journal const journal_;
|
||||
|
||||
struct Flow
|
||||
{
|
||||
explicit Flow() = default;
|
||||
|
||||
Amounts order;
|
||||
Amounts issuers;
|
||||
|
||||
bool
|
||||
sanity_check() const
|
||||
{
|
||||
using beast::zero;
|
||||
|
||||
if (isXRP(order.in) && isXRP(order.out))
|
||||
return false;
|
||||
|
||||
return order.in >= zero && order.out >= zero &&
|
||||
issuers.in >= zero && issuers.out >= zero;
|
||||
}
|
||||
};
|
||||
|
||||
private:
|
||||
void
|
||||
log_flow(char const* description, Flow const& flow);
|
||||
|
||||
Flow
|
||||
flow_xrp_to_iou(
|
||||
Amounts const& offer,
|
||||
Quality quality,
|
||||
STAmount const& owner_funds,
|
||||
STAmount const& taker_funds,
|
||||
Rate const& rate_out);
|
||||
|
||||
Flow
|
||||
flow_iou_to_xrp(
|
||||
Amounts const& offer,
|
||||
Quality quality,
|
||||
STAmount const& owner_funds,
|
||||
STAmount const& taker_funds,
|
||||
Rate const& rate_in);
|
||||
|
||||
Flow
|
||||
flow_iou_to_iou(
|
||||
Amounts const& offer,
|
||||
Quality quality,
|
||||
STAmount const& owner_funds,
|
||||
STAmount const& taker_funds,
|
||||
Rate const& rate_in,
|
||||
Rate const& rate_out);
|
||||
|
||||
// Calculates the transfer rate that we should use when calculating
|
||||
// flows for a particular issue between two accounts.
|
||||
static Rate
|
||||
effective_rate(
|
||||
Rate const& rate,
|
||||
Issue const& issue,
|
||||
AccountID const& from,
|
||||
AccountID const& to);
|
||||
|
||||
// The transfer rate for the input currency between the given accounts
|
||||
Rate
|
||||
in_rate(AccountID const& from, AccountID const& to) const
|
||||
{
|
||||
return effective_rate(m_rate_in, original_.in.issue(), from, to);
|
||||
}
|
||||
|
||||
// The transfer rate for the output currency between the given accounts
|
||||
Rate
|
||||
out_rate(AccountID const& from, AccountID const& to) const
|
||||
{
|
||||
return effective_rate(m_rate_out, original_.out.issue(), from, to);
|
||||
}
|
||||
|
||||
public:
|
||||
BasicTaker() = delete;
|
||||
BasicTaker(BasicTaker const&) = delete;
|
||||
|
||||
BasicTaker(
|
||||
CrossType cross_type,
|
||||
AccountID const& account,
|
||||
Amounts const& amount,
|
||||
Quality const& quality,
|
||||
std::uint32_t flags,
|
||||
Rate const& rate_in,
|
||||
Rate const& rate_out,
|
||||
beast::Journal journal = beast::Journal{beast::Journal::getNullSink()});
|
||||
|
||||
virtual ~BasicTaker() = default;
|
||||
|
||||
/** Returns the amount remaining on the offer.
|
||||
This is the amount at which the offer should be placed. It may either
|
||||
be for the full amount when there were no crossing offers, or for zero
|
||||
when the offer fully crossed, or any amount in between.
|
||||
It is always at the original offer quality (quality_)
|
||||
*/
|
||||
Amounts
|
||||
remaining_offer() const;
|
||||
|
||||
/** Returns the amount that the offer was originally placed at. */
|
||||
Amounts const&
|
||||
original_offer() const;
|
||||
|
||||
/** Returns the account identifier of the taker. */
|
||||
AccountID const&
|
||||
account() const noexcept
|
||||
{
|
||||
return account_;
|
||||
}
|
||||
|
||||
/** Returns `true` if the quality does not meet the taker's requirements. */
|
||||
bool
|
||||
reject(Quality const& quality) const noexcept
|
||||
{
|
||||
return quality < threshold_;
|
||||
}
|
||||
|
||||
/** Returns the type of crossing that is being performed */
|
||||
CrossType
|
||||
cross_type() const
|
||||
{
|
||||
return cross_type_;
|
||||
}
|
||||
|
||||
/** Returns the Issue associated with the input of the offer */
|
||||
Issue const&
|
||||
issue_in() const
|
||||
{
|
||||
return issue_in_;
|
||||
}
|
||||
|
||||
/** Returns the Issue associated with the output of the offer */
|
||||
Issue const&
|
||||
issue_out() const
|
||||
{
|
||||
return issue_out_;
|
||||
}
|
||||
|
||||
/** Returns `true` if the taker has run out of funds. */
|
||||
bool
|
||||
unfunded() const;
|
||||
|
||||
/** Returns `true` if order crossing should not continue.
|
||||
Order processing is stopped if the taker's order quantities have
|
||||
been reached, or if the taker has run out of input funds.
|
||||
*/
|
||||
bool
|
||||
done() const;
|
||||
|
||||
/** Perform direct crossing through given offer.
|
||||
@return an `Amounts` describing the flow achieved during cross
|
||||
*/
|
||||
BasicTaker::Flow
|
||||
do_cross(Amounts offer, Quality quality, AccountID const& owner);
|
||||
|
||||
/** Perform bridged crossing through given offers.
|
||||
@return a pair of `Amounts` describing the flow achieved during cross
|
||||
*/
|
||||
std::pair<BasicTaker::Flow, BasicTaker::Flow>
|
||||
do_cross(
|
||||
Amounts offer1,
|
||||
Quality quality1,
|
||||
AccountID const& owner1,
|
||||
Amounts offer2,
|
||||
Quality quality2,
|
||||
AccountID const& owner2);
|
||||
|
||||
virtual STAmount
|
||||
get_funds(AccountID const& account, STAmount const& funds) const = 0;
|
||||
};
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
class Taker : public BasicTaker
|
||||
{
|
||||
public:
|
||||
Taker() = delete;
|
||||
Taker(Taker const&) = delete;
|
||||
|
||||
Taker(
|
||||
CrossType cross_type,
|
||||
ApplyView& view,
|
||||
AccountID const& account,
|
||||
Amounts const& offer,
|
||||
std::uint32_t flags,
|
||||
beast::Journal journal);
|
||||
~Taker() = default;
|
||||
|
||||
void
|
||||
consume_offer(Offer& offer, Amounts const& order);
|
||||
|
||||
STAmount
|
||||
get_funds(AccountID const& account, STAmount const& funds) const override;
|
||||
|
||||
STAmount const&
|
||||
get_xrp_flow() const
|
||||
{
|
||||
return xrp_flow_;
|
||||
}
|
||||
|
||||
std::uint32_t
|
||||
get_direct_crossings() const
|
||||
{
|
||||
return direct_crossings_;
|
||||
}
|
||||
|
||||
std::uint32_t
|
||||
get_bridge_crossings() const
|
||||
{
|
||||
return bridge_crossings_;
|
||||
}
|
||||
|
||||
/** Perform a direct or bridged offer crossing as appropriate.
|
||||
Funds will be transferred accordingly, and offers will be adjusted.
|
||||
@return tesSUCCESS if successful, or an error code otherwise.
|
||||
*/
|
||||
/** @{ */
|
||||
TER
|
||||
cross(Offer& offer);
|
||||
|
||||
TER
|
||||
cross(Offer& leg1, Offer& leg2);
|
||||
/** @} */
|
||||
|
||||
private:
|
||||
static Rate
|
||||
calculateRate(
|
||||
ApplyView const& view,
|
||||
AccountID const& issuer,
|
||||
AccountID const& account);
|
||||
|
||||
TER
|
||||
fill(BasicTaker::Flow const& flow, Offer& offer);
|
||||
|
||||
TER
|
||||
fill(
|
||||
BasicTaker::Flow const& flow1,
|
||||
Offer& leg1,
|
||||
BasicTaker::Flow const& flow2,
|
||||
Offer& leg2);
|
||||
|
||||
TER
|
||||
transferXRP(
|
||||
AccountID const& from,
|
||||
AccountID const& to,
|
||||
STAmount const& amount);
|
||||
|
||||
TER
|
||||
redeemIOU(
|
||||
AccountID const& account,
|
||||
STAmount const& amount,
|
||||
Issue const& issue);
|
||||
|
||||
TER
|
||||
issueIOU(
|
||||
AccountID const& account,
|
||||
STAmount const& amount,
|
||||
Issue const& issue);
|
||||
|
||||
private:
|
||||
// The underlying ledger entry we are dealing with
|
||||
ApplyView& view_;
|
||||
|
||||
// The amount of XRP that flowed if we were autobridging
|
||||
STAmount xrp_flow_;
|
||||
|
||||
// The number direct crossings that we performed
|
||||
std::uint32_t direct_crossings_;
|
||||
|
||||
// The number autobridged crossings that we performed
|
||||
std::uint32_t bridge_crossings_;
|
||||
};
|
||||
|
||||
} // namespace ripple
|
||||
|
||||
#endif
|
||||
@@ -27,11 +27,16 @@
|
||||
|
||||
namespace ripple {
|
||||
|
||||
// These are the same flags defined as SF_PRIVATE1-4 in HashRouter.h
|
||||
#define SF_SIGBAD SF_PRIVATE1 // Signature is bad
|
||||
#define SF_SIGGOOD SF_PRIVATE2 // Signature is good
|
||||
#define SF_LOCALBAD SF_PRIVATE3 // Local checks failed
|
||||
#define SF_LOCALGOOD SF_PRIVATE4 // Local checks passed
|
||||
// These are the same flags defined as HashRouterFlags::PRIVATE1-4 in
|
||||
// HashRouter.h
|
||||
constexpr HashRouterFlags SF_SIGBAD =
|
||||
HashRouterFlags::PRIVATE1; // Signature is bad
|
||||
constexpr HashRouterFlags SF_SIGGOOD =
|
||||
HashRouterFlags::PRIVATE2; // Signature is good
|
||||
constexpr HashRouterFlags SF_LOCALBAD =
|
||||
HashRouterFlags::PRIVATE3; // Local checks failed
|
||||
constexpr HashRouterFlags SF_LOCALGOOD =
|
||||
HashRouterFlags::PRIVATE4; // Local checks passed
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
@@ -66,11 +71,11 @@ checkValidity(
|
||||
return {Validity::Valid, ""};
|
||||
}
|
||||
|
||||
if (flags & SF_SIGBAD)
|
||||
if (any(flags & SF_SIGBAD))
|
||||
// Signature is known bad
|
||||
return {Validity::SigBad, "Transaction has bad signature."};
|
||||
|
||||
if (!(flags & SF_SIGGOOD))
|
||||
if (!any(flags & SF_SIGGOOD))
|
||||
{
|
||||
// Don't know signature state. Check it.
|
||||
auto const requireCanonicalSig =
|
||||
@@ -88,12 +93,12 @@ checkValidity(
|
||||
}
|
||||
|
||||
// Signature is now known good
|
||||
if (flags & SF_LOCALBAD)
|
||||
if (any(flags & SF_LOCALBAD))
|
||||
// ...but the local checks
|
||||
// are known bad.
|
||||
return {Validity::SigGoodOnly, "Local checks failed."};
|
||||
|
||||
if (flags & SF_LOCALGOOD)
|
||||
if (any(flags & SF_LOCALGOOD))
|
||||
// ...and the local checks
|
||||
// are known good.
|
||||
return {Validity::Valid, ""};
|
||||
@@ -112,7 +117,7 @@ checkValidity(
|
||||
void
|
||||
forceValidity(HashRouter& router, uint256 const& txid, Validity validity)
|
||||
{
|
||||
int flags = 0;
|
||||
HashRouterFlags flags = HashRouterFlags::UNDEFINED;
|
||||
switch (validity)
|
||||
{
|
||||
case Validity::Valid:
|
||||
@@ -125,7 +130,7 @@ forceValidity(HashRouter& router, uint256 const& txid, Validity validity)
|
||||
// would be silly to call directly
|
||||
break;
|
||||
}
|
||||
if (flags)
|
||||
if (any(flags))
|
||||
router.setFlags(txid, flags);
|
||||
}
|
||||
|
||||
|
||||
@@ -1296,13 +1296,13 @@ PeerImp::handleTransaction(
|
||||
}
|
||||
// LCOV_EXCL_STOP
|
||||
|
||||
int flags;
|
||||
HashRouterFlags flags;
|
||||
constexpr std::chrono::seconds tx_interval = 10s;
|
||||
|
||||
if (!app_.getHashRouter().shouldProcess(txID, id_, flags, tx_interval))
|
||||
{
|
||||
// we have seen this transaction recently
|
||||
if (flags & SF_BAD)
|
||||
if (any(flags & HashRouterFlags::BAD))
|
||||
{
|
||||
fee_.update(Resource::feeUselessData, "known bad");
|
||||
JLOG(p_journal_.debug()) << "Ignoring known bad tx " << txID;
|
||||
@@ -1329,7 +1329,7 @@ PeerImp::handleTransaction(
|
||||
{
|
||||
// Skip local checks if a server we trust
|
||||
// put the transaction in its open ledger
|
||||
flags |= SF_TRUSTED;
|
||||
flags |= HashRouterFlags::TRUSTED;
|
||||
}
|
||||
|
||||
// for non-validator nodes only -- localPublicKey is set for
|
||||
@@ -2841,7 +2841,7 @@ PeerImp::doTransactions(
|
||||
|
||||
void
|
||||
PeerImp::checkTransaction(
|
||||
int flags,
|
||||
HashRouterFlags flags,
|
||||
bool checkSignature,
|
||||
std::shared_ptr<STTx const> const& stx,
|
||||
bool batch)
|
||||
@@ -2866,7 +2866,8 @@ PeerImp::checkTransaction(
|
||||
(stx->getFieldU32(sfLastLedgerSequence) <
|
||||
app_.getLedgerMaster().getValidLedgerIndex()))
|
||||
{
|
||||
app_.getHashRouter().setFlags(stx->getTransactionID(), SF_BAD);
|
||||
app_.getHashRouter().setFlags(
|
||||
stx->getTransactionID(), HashRouterFlags::BAD);
|
||||
charge(Resource::feeUselessData, "expired tx");
|
||||
return;
|
||||
}
|
||||
@@ -2925,8 +2926,10 @@ PeerImp::checkTransaction(
|
||||
<< "Exception checking transaction: " << validReason;
|
||||
}
|
||||
|
||||
// Probably not necessary to set SF_BAD, but doesn't hurt.
|
||||
app_.getHashRouter().setFlags(stx->getTransactionID(), SF_BAD);
|
||||
// Probably not necessary to set HashRouterFlags::BAD, but
|
||||
// doesn't hurt.
|
||||
app_.getHashRouter().setFlags(
|
||||
stx->getTransactionID(), HashRouterFlags::BAD);
|
||||
charge(
|
||||
Resource::feeInvalidSignature,
|
||||
"check transaction signature failure");
|
||||
@@ -2949,12 +2952,13 @@ PeerImp::checkTransaction(
|
||||
JLOG(p_journal_.trace())
|
||||
<< "Exception checking transaction: " << reason;
|
||||
}
|
||||
app_.getHashRouter().setFlags(stx->getTransactionID(), SF_BAD);
|
||||
app_.getHashRouter().setFlags(
|
||||
stx->getTransactionID(), HashRouterFlags::BAD);
|
||||
charge(Resource::feeInvalidSignature, "tx (impossible)");
|
||||
return;
|
||||
}
|
||||
|
||||
bool const trusted(flags & SF_TRUSTED);
|
||||
bool const trusted = any(flags & HashRouterFlags::TRUSTED);
|
||||
app_.getOPs().processTransaction(
|
||||
tx, trusted, false, NetworkOPs::FailHard::no);
|
||||
}
|
||||
@@ -2962,7 +2966,8 @@ PeerImp::checkTransaction(
|
||||
{
|
||||
JLOG(p_journal_.warn())
|
||||
<< "Exception in " << __func__ << ": " << ex.what();
|
||||
app_.getHashRouter().setFlags(stx->getTransactionID(), SF_BAD);
|
||||
app_.getHashRouter().setFlags(
|
||||
stx->getTransactionID(), HashRouterFlags::BAD);
|
||||
using namespace std::string_literals;
|
||||
charge(Resource::feeInvalidData, "tx "s + ex.what());
|
||||
}
|
||||
|
||||
@@ -22,6 +22,7 @@
|
||||
|
||||
#include <xrpld/app/consensus/RCLCxPeerPos.h>
|
||||
#include <xrpld/app/ledger/detail/LedgerReplayMsgHandler.h>
|
||||
#include <xrpld/app/misc/HashRouter.h>
|
||||
#include <xrpld/overlay/Squelch.h>
|
||||
#include <xrpld/overlay/detail/OverlayImpl.h>
|
||||
#include <xrpld/overlay/detail/ProtocolVersion.h>
|
||||
@@ -612,7 +613,7 @@ private:
|
||||
|
||||
void
|
||||
checkTransaction(
|
||||
int flags,
|
||||
HashRouterFlags flags,
|
||||
bool checkSignature,
|
||||
std::shared_ptr<STTx const> const& stx,
|
||||
bool batch);
|
||||
|
||||
Reference in New Issue
Block a user