mirror of
https://github.com/Xahau/xahaud.git
synced 2026-08-28 02:30:55 +00:00
Compare commits
3 Commits
fixhookmap
...
feature/ur
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
043dcb2a38 | ||
|
|
bb244ef772 | ||
|
|
639ea34377 |
@@ -95,8 +95,16 @@ if [[ "$4" == "" ]]; then
|
||||
echo "Non GH, local building, no Action runner magic"
|
||||
else
|
||||
# GH Action, runner
|
||||
cp /io/release-build/xahaud /data/builds/$(date +%Y).$(date +%-m).$(date +%-d)-$(git rev-parse --abbrev-ref HEAD)+$4
|
||||
cp /io/release-build/release.info /data/builds/$(date +%Y).$(date +%-m).$(date +%-d)-$(git rev-parse --abbrev-ref HEAD)+$4.releaseinfo
|
||||
if [[ "$(git rev-parse --abbrev-ref HEAD)" == "release" ]]; then
|
||||
echo "building on the release branch... placing it in builds/candidate"
|
||||
mkdir /data/builds/candidate
|
||||
cp /io/release-build/xahaud /data/builds/candidate/$(date +%Y).$(date +%-m).$(date +%-d)-$(git rev-parse --abbrev-ref HEAD)+$4
|
||||
cp /io/release-build/release.info /data/builds/candidate/$(date +%Y).$(date +%-m).$(date +%-d)-$(git rev-parse --abbrev-ref HEAD)+$4.releaseinfo
|
||||
else
|
||||
echo "building non-release branch, placing it in builds root"
|
||||
cp /io/release-build/xahaud /data/builds/$(date +%Y).$(date +%-m).$(date +%-d)-$(git rev-parse --abbrev-ref HEAD)+$4
|
||||
cp /io/release-build/release.info /data/builds/$(date +%Y).$(date +%-m).$(date +%-d)-$(git rev-parse --abbrev-ref HEAD)+$4.releaseinfo
|
||||
fi
|
||||
echo "Published build to: http://build.xahau.tech/"
|
||||
echo $(date +%Y).$(date +%-m).$(date +%-d)-$(git rev-parse --abbrev-ref HEAD)+$4
|
||||
fi
|
||||
|
||||
@@ -242,6 +242,7 @@
|
||||
#define sfLockingChainDoor ((8U << 16U) + 22U)
|
||||
#define sfIssuingChainDoor ((8U << 16U) + 23U)
|
||||
#define sfSubject ((8U << 16U) + 24U)
|
||||
#define sfBrokerAccount ((8U << 16U) + 98U)
|
||||
#define sfInform ((8U << 16U) + 99U)
|
||||
#define sfIndexes ((19U << 16U) + 1U)
|
||||
#define sfHashes ((19U << 16U) + 2U)
|
||||
|
||||
@@ -34,6 +34,8 @@
|
||||
// If you add an amendment here, then do not forget to increment `numFeatures`
|
||||
// in include/xrpl/protocol/Feature.h.
|
||||
|
||||
XRPL_FEATURE(URITokenBroker, Supported::yes, VoteBehavior::DefaultNo)
|
||||
XRPL_FIX (HookMap, Supported::yes, VoteBehavior::DefaultYes)
|
||||
XRPL_FIX (GuardDepth32, Supported::yes, VoteBehavior::DefaultNo)
|
||||
XRPL_FEATURE(NamedHooks, Supported::yes, VoteBehavior::DefaultNo)
|
||||
XRPL_FEATURE(IOURewardClaim, Supported::yes, VoteBehavior::DefaultNo)
|
||||
|
||||
@@ -319,6 +319,7 @@ TYPED_SFIELD(sfAttestationRewardAccount, ACCOUNT, 21)
|
||||
TYPED_SFIELD(sfLockingChainDoor, ACCOUNT, 22)
|
||||
TYPED_SFIELD(sfIssuingChainDoor, ACCOUNT, 23)
|
||||
TYPED_SFIELD(sfSubject, ACCOUNT, 24)
|
||||
TYPED_SFIELD(sfBrokerAccount, ACCOUNT, 98)
|
||||
TYPED_SFIELD(sfInform, ACCOUNT, 99)
|
||||
|
||||
// vector of 256-bit
|
||||
|
||||
@@ -314,6 +314,7 @@ TRANSACTION(ttURITOKEN_BURN, 46, URITokenBurn, ({
|
||||
TRANSACTION(ttURITOKEN_BUY, 47, URITokenBuy, ({
|
||||
{sfURITokenID, soeREQUIRED},
|
||||
{sfAmount, soeREQUIRED},
|
||||
{sfBrokerAccount, soeOPTIONAL},
|
||||
}))
|
||||
|
||||
TRANSACTION(ttURITOKEN_CREATE_SELL_OFFER, 48, URITokenCreateSellOffer, ({
|
||||
|
||||
@@ -3632,8 +3632,9 @@ public:
|
||||
|
||||
auto const alice = Account{"alice"};
|
||||
auto const bob = Account{"bob"};
|
||||
auto const claire = Account{"claire"};
|
||||
Env env{*this, features};
|
||||
env.fund(XRP(100000), alice, bob);
|
||||
env.fund(XRP(100000), alice, bob, claire);
|
||||
env.close();
|
||||
|
||||
// Compute hook hash for the accept hook
|
||||
@@ -3773,6 +3774,84 @@ public:
|
||||
BEAST_EXPECT(result2.has_value());
|
||||
BEAST_EXPECT(result2.value() == newData.size());
|
||||
}
|
||||
|
||||
{
|
||||
// fixHookMap: foreign state set without grant after state previous
|
||||
// modified
|
||||
|
||||
HookStateMap stateMap;
|
||||
auto hookCtx = makeStubHookContext(
|
||||
applyCtx, alice.id(), bob.id(), {}, stateMap);
|
||||
|
||||
AccountID const aliceid = alice.id();
|
||||
|
||||
// Pre-populate stateMap
|
||||
stateMap[alice.id()] = {
|
||||
100, // availableForReserves
|
||||
1, // namespaceCount
|
||||
1, // hookStateScale
|
||||
{}};
|
||||
|
||||
auto& api = hookCtx.api();
|
||||
|
||||
// setup a hook on alice, and on claire, no grants
|
||||
env(hook(alice, {{hso(genesis::AcceptHook)}}, 0), fee(XRP(1)));
|
||||
env(hook(claire, {{hso(genesis::AcceptHook)}}, 0), fee(XRP(1)));
|
||||
env.close();
|
||||
|
||||
// First modification
|
||||
auto result1 =
|
||||
api.state_foreign_set(testKey, testNs, aliceid, testData);
|
||||
BEAST_EXPECT(result1.has_value());
|
||||
|
||||
// Second modification this time using bob as hookacc (should hit
|
||||
// cache)
|
||||
auto hookCtx2 = makeStubHookContext(
|
||||
applyCtx, claire.id(), bob.id(), {}, stateMap);
|
||||
|
||||
// check the state entry is carried into the second context
|
||||
|
||||
// does the map contain the account?
|
||||
BEAST_EXPECT(
|
||||
hookCtx2.result.stateMap.find(aliceid) !=
|
||||
hookCtx2.result.stateMap.end());
|
||||
|
||||
// the name space?
|
||||
BEAST_EXPECT(
|
||||
std::get<3>(hookCtx2.result.stateMap[aliceid]).find(testNs) !=
|
||||
std::get<3>(hookCtx2.result.stateMap[aliceid]).end());
|
||||
|
||||
// the key entry?
|
||||
BEAST_EXPECT(
|
||||
std::get<3>(hookCtx2.result.stateMap[aliceid])[testNs].find(
|
||||
testKey) !=
|
||||
std::get<3>(hookCtx2.result.stateMap[aliceid])[testNs].end());
|
||||
|
||||
// is the entry marked as modified?
|
||||
BEAST_EXPECT(
|
||||
std::get<3>(hookCtx2.result.stateMap[aliceid])[testNs][testKey]
|
||||
.first);
|
||||
|
||||
auto& api2 = hookCtx2.api();
|
||||
Bytes newData{0x04, 0x05};
|
||||
auto result2 =
|
||||
api2.state_foreign_set(testKey, testNs, aliceid, newData);
|
||||
|
||||
if (features[fixHookMap])
|
||||
{
|
||||
// new behaviour: grant is missing, cannot write
|
||||
BEAST_EXPECT(!result2.has_value());
|
||||
BEAST_EXPECT(result2.error() == NOT_AUTHORIZED);
|
||||
BEAST_EXPECT(hookCtx2.result.foreignStateSetDisabled);
|
||||
}
|
||||
else
|
||||
{
|
||||
// old behaviour: allow this illegal write due to the entry
|
||||
// being modified previously in the map
|
||||
BEAST_EXPECT(result2.has_value());
|
||||
BEAST_EXPECT(result2.value() == newData.size());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
@@ -4832,6 +4911,7 @@ public:
|
||||
|
||||
test_state(features);
|
||||
test_state_foreign(features);
|
||||
test_state_foreign_set(features - fixHookMap);
|
||||
test_state_foreign_set(features);
|
||||
test_state_foreign_set_max(features);
|
||||
test_state_set(features);
|
||||
|
||||
@@ -7033,6 +7033,130 @@ private:
|
||||
testTSHStrongWeak(env, tshNONE, __LINE__);
|
||||
}
|
||||
}
|
||||
|
||||
// A broker is an explicitly discovered weak TSH. This is required for
|
||||
// XAH sales because native balance changes are not found by the generic
|
||||
// balance-based weak-TSH discovery.
|
||||
if (features[featureURITokenBroker] && features[fixXahauV1])
|
||||
{
|
||||
for (bool const collectEnabled : {false, true})
|
||||
{
|
||||
test::jtx::Env env{
|
||||
*this,
|
||||
network::makeNetworkConfig(
|
||||
21337, "10", "1000000", "200000"),
|
||||
features};
|
||||
|
||||
auto const issuer = Account("alice");
|
||||
auto const owner = Account("bob");
|
||||
auto const buyer = Account("carol");
|
||||
auto const broker = Account("dave");
|
||||
env.fund(XRP(1000), issuer, owner, buyer, broker);
|
||||
env.close();
|
||||
|
||||
std::string const uri =
|
||||
collectEnabled ? "broker-collect" : "broker-no-collect";
|
||||
auto const tid = uritoken::tokenid(issuer, uri);
|
||||
std::string const hexid{strHex(tid)};
|
||||
|
||||
env(uritoken::mint(issuer, uri),
|
||||
uritoken::dest(owner),
|
||||
uritoken::amt(XRP(1)),
|
||||
ter(tesSUCCESS));
|
||||
env.close();
|
||||
env(uritoken::buy(owner, hexid),
|
||||
uritoken::amt(XRP(1)),
|
||||
fee(XRP(1)),
|
||||
ter(tesSUCCESS));
|
||||
env.close();
|
||||
env(uritoken::sell(owner, hexid),
|
||||
uritoken::dest(buyer),
|
||||
uritoken::amt(XRP(1)),
|
||||
ter(tesSUCCESS));
|
||||
env.close();
|
||||
|
||||
if (collectEnabled)
|
||||
addWeakTSH(env, broker);
|
||||
setTSHHook(env, broker, false);
|
||||
|
||||
auto buy = uritoken::buy(buyer, hexid);
|
||||
buy[sfAmount.jsonName] =
|
||||
XRP(2).value().getJson(JsonOptions::none);
|
||||
buy[sfBrokerAccount.jsonName] = broker.human();
|
||||
env(buy, fee(XRP(1)), ter(tesSUCCESS));
|
||||
env.close();
|
||||
|
||||
testTSHStrongWeak(
|
||||
env, collectEnabled ? tshWEAK : tshNONE, __LINE__);
|
||||
}
|
||||
}
|
||||
|
||||
// If the token issuer is also the broker, TSH strengths are ORed and
|
||||
// the account executes exactly once: weak for a non-burnable token and
|
||||
// strong for a burnable token.
|
||||
if (features[featureURITokenBroker] && features[fixXahauV1])
|
||||
{
|
||||
for (bool const burnable : {false, true})
|
||||
{
|
||||
test::jtx::Env env{
|
||||
*this,
|
||||
network::makeNetworkConfig(
|
||||
21337, "10", "1000000", "200000"),
|
||||
features};
|
||||
|
||||
auto const issuer = Account("alice");
|
||||
auto const owner = Account("bob");
|
||||
auto const buyer = Account("carol");
|
||||
env.fund(XRP(1000), issuer, owner, buyer);
|
||||
env.close();
|
||||
|
||||
std::string const uri =
|
||||
burnable ? "broker-issuer-burnable" : "broker-issuer";
|
||||
auto const tid = uritoken::tokenid(issuer, uri);
|
||||
std::string const hexid{strHex(tid)};
|
||||
|
||||
env(uritoken::mint(issuer, uri),
|
||||
uritoken::dest(owner),
|
||||
uritoken::amt(XRP(1)),
|
||||
txflags(burnable ? tfBurnable : 0),
|
||||
ter(tesSUCCESS));
|
||||
env.close();
|
||||
|
||||
env(uritoken::buy(owner, hexid),
|
||||
uritoken::amt(XRP(1)),
|
||||
fee(XRP(1)),
|
||||
ter(tesSUCCESS));
|
||||
env.close();
|
||||
|
||||
env(uritoken::sell(owner, hexid),
|
||||
uritoken::dest(buyer),
|
||||
uritoken::amt(XRP(1)),
|
||||
ter(tesSUCCESS));
|
||||
env.close();
|
||||
|
||||
if (!burnable)
|
||||
addWeakTSH(env, issuer);
|
||||
setTSHHook(env, issuer, burnable);
|
||||
|
||||
auto buy = uritoken::buy(buyer, hexid);
|
||||
buy[sfAmount.jsonName] =
|
||||
XRP(2).value().getJson(JsonOptions::none);
|
||||
buy[sfBrokerAccount.jsonName] = issuer.human();
|
||||
env(buy, fee(XRP(1)), ter(tesSUCCESS));
|
||||
env.close();
|
||||
|
||||
testTSHStrongWeak(
|
||||
env, burnable ? tshSTRONG : tshWEAK, __LINE__);
|
||||
|
||||
Json::Value params;
|
||||
params[jss::transaction] =
|
||||
env.tx()->getJson(JsonOptions::none)[jss::hash];
|
||||
auto const result = env.rpc("json", "tx", to_string(params));
|
||||
auto const executions =
|
||||
result[jss::result][jss::meta][sfHookExecutions.jsonName];
|
||||
BEAST_EXPECT(executions.size() == 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
|
||||
465
src/test/app/URITokenBroker_test.cpp
Normal file
465
src/test/app/URITokenBroker_test.cpp
Normal file
@@ -0,0 +1,465 @@
|
||||
//------------------------------------------------------------------------------
|
||||
/*
|
||||
This file is part of rippled: https://github.com/ripple/rippled
|
||||
Copyright (c) 2026 XRPL-Labs
|
||||
|
||||
Permission to use, copy, modify, and/or distribute this software for any
|
||||
purpose with or without fee is hereby granted, provided that the above
|
||||
copyright notice and this permission notice appear in all copies.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY
|
||||
SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
*/
|
||||
//==============================================================================
|
||||
|
||||
#include <test/jtx.h>
|
||||
#include <test/jtx/AMM.h>
|
||||
#include <xrpl/protocol/Feature.h>
|
||||
#include <xrpl/protocol/Indexes.h>
|
||||
#include <xrpl/protocol/TxFlags.h>
|
||||
|
||||
namespace ripple {
|
||||
namespace test {
|
||||
|
||||
struct URITokenBroker_test : public beast::unit_test::suite
|
||||
{
|
||||
static Json::Value
|
||||
brokeredBuy(
|
||||
jtx::Account const& buyer,
|
||||
std::string const& tokenID,
|
||||
STAmount const& amount,
|
||||
jtx::Account const& broker)
|
||||
{
|
||||
auto tx = jtx::uritoken::buy(buyer, tokenID);
|
||||
tx[sfAmount.jsonName] = amount.getJson(JsonOptions::none);
|
||||
tx[sfBrokerAccount.jsonName] = broker.human();
|
||||
return tx;
|
||||
}
|
||||
|
||||
static std::string
|
||||
mintAndSell(
|
||||
jtx::Env& env,
|
||||
jtx::Account const& seller,
|
||||
std::string const& uri,
|
||||
STAmount const& amount,
|
||||
std::optional<jtx::Account> const& destination = std::nullopt)
|
||||
{
|
||||
auto const id = strHex(jtx::uritoken::tokenid(seller, uri));
|
||||
env(jtx::uritoken::mint(seller, uri));
|
||||
if (destination)
|
||||
env(jtx::uritoken::sell(seller, id),
|
||||
jtx::uritoken::amt(amount),
|
||||
jtx::uritoken::dest(*destination));
|
||||
else
|
||||
env(jtx::uritoken::sell(seller, id), jtx::uritoken::amt(amount));
|
||||
env.close();
|
||||
return id;
|
||||
}
|
||||
|
||||
static std::shared_ptr<SLE const>
|
||||
token(jtx::Env const& env, uint256 const& id)
|
||||
{
|
||||
return env.le(Keylet{ltURI_TOKEN, id});
|
||||
}
|
||||
|
||||
static std::shared_ptr<SLE const>
|
||||
token(jtx::Env const& env, std::string const& id)
|
||||
{
|
||||
uint256 tokenID;
|
||||
if (!tokenID.parseHex(id))
|
||||
return nullptr;
|
||||
return token(env, tokenID);
|
||||
}
|
||||
|
||||
void
|
||||
testAmendment(FeatureBitset const& features)
|
||||
{
|
||||
testcase("amendment");
|
||||
using namespace jtx;
|
||||
|
||||
Account const seller{"seller"};
|
||||
Account const buyer{"buyer"};
|
||||
Account const broker{"broker"};
|
||||
|
||||
// A broker-less buy remains valid when the broker amendment is off.
|
||||
{
|
||||
Env env{*this, features - featureURITokenBroker};
|
||||
env.fund(XRP(1000), seller, buyer, broker);
|
||||
auto const id = mintAndSell(env, seller, "no-broker", XRP(10));
|
||||
env(uritoken::buy(buyer, id), uritoken::amt(XRP(10)));
|
||||
}
|
||||
|
||||
// BrokerAccount requires both URITokenBroker and fixXahauV1.
|
||||
for (auto const disabled : {featureURITokenBroker, fixXahauV1})
|
||||
{
|
||||
Env env{*this, features - disabled};
|
||||
env.fund(XRP(1000), seller, buyer, broker);
|
||||
auto const id =
|
||||
mintAndSell(env, seller, to_string(disabled), XRP(10));
|
||||
env(brokeredBuy(buyer, id, XRP(11), broker), ter(temDISABLED));
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
testMalformed(FeatureBitset const& features)
|
||||
{
|
||||
testcase("preflight");
|
||||
using namespace jtx;
|
||||
|
||||
Env env{*this, features};
|
||||
Account const seller{"seller"};
|
||||
Account const buyer{"buyer"};
|
||||
env.fund(XRP(1000), seller, buyer);
|
||||
auto const id = mintAndSell(env, seller, "preflight", XRP(10));
|
||||
|
||||
env(brokeredBuy(buyer, id, XRP(11), buyer), ter(temREDUNDANT));
|
||||
|
||||
auto zero = uritoken::buy(buyer, id);
|
||||
zero[sfAmount.jsonName] = XRP(11).value().getJson(JsonOptions::none);
|
||||
zero[sfBrokerAccount.jsonName] = "rrrrrrrrrrrrrrrrrrrrrhoLvTp";
|
||||
env(zero, ter(temMALFORMED));
|
||||
|
||||
// BrokerAccount is only part of the URITokenBuy template.
|
||||
auto mint = uritoken::mint(seller, "broker-on-mint");
|
||||
mint[sfBrokerAccount.jsonName] = buyer.human();
|
||||
env(mint, ter(temMALFORMED));
|
||||
|
||||
// BrokerAccount belongs only to URITokenBuy's serialization template.
|
||||
auto payment = pay(buyer, seller, XRP(1));
|
||||
payment[sfBrokerAccount.jsonName] = seller.human();
|
||||
env(payment, ter(temMALFORMED));
|
||||
}
|
||||
|
||||
void
|
||||
testPreclaim(FeatureBitset const& features)
|
||||
{
|
||||
testcase("preclaim");
|
||||
using namespace jtx;
|
||||
|
||||
Account const seller{"seller"};
|
||||
Account const buyer{"buyer"};
|
||||
Account const broker{"broker"};
|
||||
Account const missing{"missing"};
|
||||
|
||||
// Missing broker account.
|
||||
{
|
||||
Env env{*this, features};
|
||||
env.fund(XRP(1000), seller, buyer);
|
||||
env.memoize(missing);
|
||||
auto const id = mintAndSell(env, seller, "missing", XRP(10));
|
||||
env(brokeredBuy(buyer, id, XRP(11), missing), ter(tecNO_TARGET));
|
||||
}
|
||||
|
||||
// The seller cannot also receive the broker spread.
|
||||
{
|
||||
Env env{*this, features};
|
||||
env.fund(XRP(1000), seller, buyer);
|
||||
auto const id = mintAndSell(env, seller, "seller", XRP(10));
|
||||
env(brokeredBuy(buyer, id, XRP(11), seller), ter(tecNO_PERMISSION));
|
||||
}
|
||||
|
||||
// A brokered sale requires both a positive price and positive spread.
|
||||
{
|
||||
Env env{*this, features};
|
||||
env.fund(XRP(1000), seller, buyer, broker);
|
||||
auto const zeroID = mintAndSell(env, seller, "zero", XRP(0), buyer);
|
||||
env(brokeredBuy(buyer, zeroID, XRP(1), broker),
|
||||
ter(tecNO_PERMISSION));
|
||||
// The legacy destination-restricted zero-price buy is unchanged.
|
||||
env(uritoken::buy(buyer, zeroID), uritoken::amt(XRP(0)));
|
||||
|
||||
auto const id = mintAndSell(env, seller, "spread", XRP(10));
|
||||
env(brokeredBuy(buyer, id, XRP(9), broker),
|
||||
ter(tecINSUFFICIENT_PAYMENT));
|
||||
env(brokeredBuy(buyer, id, XRP(10), broker), ter(tecNO_PERMISSION));
|
||||
}
|
||||
|
||||
// An IOU broker must already have a trust line.
|
||||
{
|
||||
Env env{*this, features};
|
||||
Account const gw{"gateway"};
|
||||
auto const USD = gw["USD"];
|
||||
env.fund(XRP(1000), seller, buyer, broker, gw);
|
||||
env.trust(USD(1000), seller, buyer);
|
||||
env.close();
|
||||
env(pay(gw, buyer, USD(100)));
|
||||
auto const id = mintAndSell(env, seller, "no-line", USD(10));
|
||||
env(brokeredBuy(buyer, id, USD(11), broker), ter(tecNO_LINE));
|
||||
}
|
||||
|
||||
// AMM pseudo-accounts cannot be named as brokers.
|
||||
{
|
||||
Env env{*this, features | featureAMM};
|
||||
Account const gw{"gateway"};
|
||||
auto const USD = gw["USD"];
|
||||
env.fund(XRP(30'000), seller, buyer, gw);
|
||||
env.trust(USD(30'000), seller, buyer);
|
||||
env.close();
|
||||
env(pay(gw, seller, USD(20'000)));
|
||||
AMM amm{env, seller, XRP(10'000), USD(10'000)};
|
||||
|
||||
auto const id = mintAndSell(env, seller, "amm", XRP(10));
|
||||
auto tx = uritoken::buy(buyer, id);
|
||||
tx[sfAmount.jsonName] = XRP(11).value().getJson(JsonOptions::none);
|
||||
tx[sfBrokerAccount.jsonName] = toBase58(amm.ammAccount());
|
||||
env(tx, ter(tecNO_PERMISSION));
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
testXAH(FeatureBitset const& features)
|
||||
{
|
||||
testcase("XAH settlement");
|
||||
using namespace jtx;
|
||||
|
||||
Env env{*this, features};
|
||||
Account const seller{"seller"};
|
||||
Account const buyer{"buyer"};
|
||||
Account const broker{"broker"};
|
||||
env.fund(XRP(1000), seller, buyer, broker);
|
||||
env(fset(seller, asfDepositAuth));
|
||||
env(fset(broker, asfDepositAuth));
|
||||
env.close();
|
||||
|
||||
auto const id = mintAndSell(env, seller, "xah", XRP(100));
|
||||
auto const sellerBefore = env.balance(seller);
|
||||
auto const buyerBefore = env.balance(buyer);
|
||||
auto const brokerBefore = env.balance(broker);
|
||||
auto const buyerOwners = env.ownerCount(buyer);
|
||||
auto const brokerOwners = env.ownerCount(broker);
|
||||
auto const fee = env.current()->fees().base;
|
||||
|
||||
env(brokeredBuy(buyer, id, XRP(110), broker));
|
||||
env.close();
|
||||
|
||||
BEAST_EXPECT(env.balance(seller) == sellerBefore + XRP(100));
|
||||
BEAST_EXPECT(env.balance(broker) == brokerBefore + XRP(10));
|
||||
BEAST_EXPECT(env.balance(buyer) == buyerBefore - XRP(110) - fee);
|
||||
BEAST_EXPECT(env.ownerCount(buyer) == buyerOwners + 1);
|
||||
BEAST_EXPECT(env.ownerCount(broker) == brokerOwners);
|
||||
|
||||
auto const sle = token(env, id);
|
||||
if (!BEAST_EXPECT(sle))
|
||||
return;
|
||||
BEAST_EXPECT((*sle)[sfOwner] == buyer.id());
|
||||
BEAST_EXPECT(!sle->isFieldPresent(sfAmount));
|
||||
BEAST_EXPECT(!sle->isFieldPresent(sfDestination));
|
||||
}
|
||||
|
||||
void
|
||||
testXAHReserve(FeatureBitset const& features)
|
||||
{
|
||||
testcase("XAH reserve boundary");
|
||||
using namespace jtx;
|
||||
|
||||
Env env{*this, features};
|
||||
Account const seller{"seller"};
|
||||
Account const buyer{"buyer"};
|
||||
Account const broker{"broker"};
|
||||
env.fund(XRP(1000), seller, broker);
|
||||
env.fund(XRP(300), buyer);
|
||||
env.close();
|
||||
|
||||
auto const id = mintAndSell(env, seller, "reserve", XRP(1));
|
||||
auto const sellerBefore = env.balance(seller);
|
||||
auto const brokerBefore = env.balance(broker);
|
||||
|
||||
// The buyer needs the next owner reserve plus the full signed amount,
|
||||
// not merely the seller's one-XAH listing amount.
|
||||
env(brokeredBuy(buyer, id, XRP(51), broker),
|
||||
ter(tecINSUFFICIENT_FUNDS));
|
||||
|
||||
BEAST_EXPECT(env.balance(seller) == sellerBefore);
|
||||
BEAST_EXPECT(env.balance(broker) == brokerBefore);
|
||||
auto const sle = token(env, id);
|
||||
BEAST_EXPECT(sle && (*sle)[sfOwner] == seller.id());
|
||||
}
|
||||
|
||||
void
|
||||
testIOU(FeatureBitset const& features)
|
||||
{
|
||||
testcase("IOU settlement");
|
||||
using namespace jtx;
|
||||
|
||||
Account const seller{"seller"};
|
||||
Account const buyer{"buyer"};
|
||||
Account const broker{"broker"};
|
||||
Account const gw{"gateway"};
|
||||
auto const USD = gw["USD"];
|
||||
|
||||
// Both recipients bear the transfer rate; buyer debit remains exact.
|
||||
{
|
||||
Env env{*this, features};
|
||||
env.fund(XRP(1000), seller, buyer, broker, gw);
|
||||
env(rate(gw, 1.25));
|
||||
env.trust(USD(1000), seller, buyer, broker);
|
||||
env.close();
|
||||
env(trust(gw, seller["USD"](1000), tfClearNoRipple));
|
||||
env(trust(gw, buyer["USD"](1000), tfClearNoRipple));
|
||||
env(trust(gw, broker["USD"](1000), tfClearNoRipple));
|
||||
env.close();
|
||||
env(pay(gw, buyer, USD(1000)));
|
||||
auto const id = mintAndSell(env, seller, "rate", USD(100));
|
||||
auto const buyerBefore = env.balance(buyer, USD.issue());
|
||||
auto const brokerOwners = env.ownerCount(broker);
|
||||
|
||||
env(brokeredBuy(buyer, id, USD(110), broker));
|
||||
env.close();
|
||||
|
||||
BEAST_EXPECT(env.balance(seller, USD.issue()) == USD(80));
|
||||
BEAST_EXPECT(env.balance(broker, USD.issue()) == USD(8));
|
||||
BEAST_EXPECT(
|
||||
env.balance(buyer, USD.issue()) == buyerBefore - USD(110));
|
||||
BEAST_EXPECT(env.ownerCount(broker) == brokerOwners);
|
||||
}
|
||||
|
||||
// The issuer may be the broker and needs no trust line.
|
||||
{
|
||||
Env env{*this, features};
|
||||
env.fund(XRP(1000), seller, buyer, gw);
|
||||
env(rate(gw, 1.25));
|
||||
env.trust(USD(1000), seller, buyer);
|
||||
env.close();
|
||||
env(trust(gw, seller["USD"](1000), tfClearNoRipple));
|
||||
env(trust(gw, buyer["USD"](1000), tfClearNoRipple));
|
||||
env.close();
|
||||
env(pay(gw, buyer, USD(1000)));
|
||||
auto const id = mintAndSell(env, seller, "issuer", USD(100));
|
||||
auto const buyerBefore = env.balance(buyer, USD.issue());
|
||||
|
||||
env(brokeredBuy(buyer, id, USD(110), gw));
|
||||
env.close();
|
||||
|
||||
BEAST_EXPECT(env.balance(seller, USD.issue()) == USD(80));
|
||||
BEAST_EXPECT(
|
||||
env.balance(buyer, USD.issue()) == buyerBefore - USD(110));
|
||||
}
|
||||
|
||||
// Existing line limits are not consulted and no owner object is added.
|
||||
{
|
||||
Env env{*this, features};
|
||||
env.fund(XRP(1000), seller, buyer, broker, gw);
|
||||
env.trust(USD(1000), seller, buyer);
|
||||
env.trust(USD(1), broker);
|
||||
env.close();
|
||||
env(trust(gw, seller["USD"](1000), tfClearNoRipple));
|
||||
env(trust(gw, buyer["USD"](1000), tfClearNoRipple));
|
||||
env(trust(gw, broker["USD"](1), tfClearNoRipple));
|
||||
env.close();
|
||||
env(pay(gw, buyer, USD(100)));
|
||||
env(pay(gw, broker, USD(1)));
|
||||
auto const id = mintAndSell(env, seller, "limit", USD(10));
|
||||
auto const brokerOwners = env.ownerCount(broker);
|
||||
|
||||
env(brokeredBuy(buyer, id, USD(20), broker));
|
||||
env.close();
|
||||
|
||||
BEAST_EXPECT(env.balance(broker, USD.issue()) == USD(11));
|
||||
BEAST_EXPECT(env.ownerCount(broker) == brokerOwners);
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
testIOUPermissions(FeatureBitset const& features)
|
||||
{
|
||||
testcase("IOU permissions");
|
||||
using namespace jtx;
|
||||
|
||||
Account const seller{"seller"};
|
||||
Account const buyer{"buyer"};
|
||||
Account const broker{"broker"};
|
||||
Account const gw{"gateway"};
|
||||
auto const USD = gw["USD"];
|
||||
|
||||
// A frozen broker line rejects the broker leg atomically.
|
||||
{
|
||||
Env env{*this, features};
|
||||
env.fund(XRP(1000), seller, buyer, broker, gw);
|
||||
env.trust(USD(1000), seller, buyer, broker);
|
||||
env.close();
|
||||
env(trust(gw, seller["USD"](1000), tfClearNoRipple));
|
||||
env(trust(gw, buyer["USD"](1000), tfClearNoRipple));
|
||||
env(pay(gw, buyer, USD(100)));
|
||||
env(trust(gw, broker["USD"](1000), tfSetFreeze | tfClearNoRipple));
|
||||
env.close();
|
||||
auto const id = mintAndSell(env, seller, "freeze", USD(10));
|
||||
auto const buyerBefore = env.balance(buyer, USD.issue());
|
||||
|
||||
env(brokeredBuy(buyer, id, USD(11), broker), ter(tecFROZEN));
|
||||
BEAST_EXPECT(env.balance(seller, USD.issue()) == USD(0));
|
||||
BEAST_EXPECT(env.balance(broker, USD.issue()) == USD(0));
|
||||
BEAST_EXPECT(env.balance(buyer, USD.issue()) == buyerBefore);
|
||||
}
|
||||
|
||||
// Under RequireAuth, an existing but unauthorized broker line fails.
|
||||
{
|
||||
Env env{*this, features};
|
||||
auto const sellerUSD = seller["USD"];
|
||||
auto const buyerUSD = buyer["USD"];
|
||||
env.fund(XRP(1000), seller, buyer, broker, gw);
|
||||
env(fset(gw, asfRequireAuth));
|
||||
env.close();
|
||||
env.trust(USD(1000), seller, buyer, broker);
|
||||
env(trust(gw, sellerUSD(1000)),
|
||||
txflags(tfSetfAuth | tfClearNoRipple));
|
||||
env(trust(gw, buyerUSD(1000)),
|
||||
txflags(tfSetfAuth | tfClearNoRipple));
|
||||
env(trust(gw, broker["USD"](1000)), txflags(tfClearNoRipple));
|
||||
env.close();
|
||||
env(pay(gw, buyer, USD(100)));
|
||||
auto const id = mintAndSell(env, seller, "auth", USD(10));
|
||||
|
||||
env(brokeredBuy(buyer, id, USD(11), broker), ter(tecNO_AUTH));
|
||||
}
|
||||
|
||||
// NoRipple on the broker leg rejects the whole transaction; the
|
||||
// already-computed seller leg must also be rolled back.
|
||||
{
|
||||
Env env{*this, features};
|
||||
env.fund(XRP(1000), seller, buyer, broker, gw);
|
||||
env.trust(USD(1000), seller, buyer, broker);
|
||||
env.close();
|
||||
env(trust(gw, seller["USD"](1000), tfClearNoRipple));
|
||||
env(trust(gw, buyer["USD"](1000), tfClearNoRipple));
|
||||
env(pay(gw, buyer, USD(100)));
|
||||
env(trust(gw, broker["USD"](1000), tfSetNoRipple));
|
||||
env.close();
|
||||
auto const id = mintAndSell(env, seller, "no-ripple", USD(10));
|
||||
auto const sellerBefore = env.balance(seller, USD.issue());
|
||||
auto const buyerBefore = env.balance(buyer, USD.issue());
|
||||
auto const brokerBefore = env.balance(broker, USD.issue());
|
||||
|
||||
env(brokeredBuy(buyer, id, USD(11), broker), ter(tecPATH_DRY));
|
||||
|
||||
BEAST_EXPECT(env.balance(seller, USD.issue()) == sellerBefore);
|
||||
BEAST_EXPECT(env.balance(buyer, USD.issue()) == buyerBefore);
|
||||
BEAST_EXPECT(env.balance(broker, USD.issue()) == brokerBefore);
|
||||
auto const sle = token(env, id);
|
||||
BEAST_EXPECT(sle && (*sle)[sfOwner] == seller.id());
|
||||
}
|
||||
}
|
||||
|
||||
public:
|
||||
void
|
||||
run() override
|
||||
{
|
||||
auto const features = jtx::supported_amendments();
|
||||
testAmendment(features);
|
||||
testMalformed(features);
|
||||
testPreclaim(features);
|
||||
testXAH(features);
|
||||
testXAHReserve(features);
|
||||
testIOU(features);
|
||||
testIOUPermissions(features);
|
||||
}
|
||||
};
|
||||
|
||||
BEAST_DEFINE_TESTSUITE(URITokenBroker, app, ripple);
|
||||
|
||||
} // namespace test
|
||||
} // namespace ripple
|
||||
@@ -15,6 +15,7 @@
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
#include <queue>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
#include <wasmedge/wasmedge.h>
|
||||
|
||||
@@ -174,6 +175,8 @@ struct HookResult
|
||||
false; // hook_again allows strong pre-apply to nominate
|
||||
// additional weak post-apply execution
|
||||
std::shared_ptr<STObject const> provisionalMeta;
|
||||
std::set<std::pair<AccountID, uint256 /* namespace */>>
|
||||
foreignStateGrantCache; // add found grants here to avoid rechecking
|
||||
};
|
||||
|
||||
class HookExecutor;
|
||||
|
||||
@@ -1920,88 +1920,114 @@ HookAPI::state_foreign_set(
|
||||
if (hookCtx.result.foreignStateSetDisabled)
|
||||
return Unexpected(PREVIOUS_FAILURE_PREVENTS_RETRY);
|
||||
|
||||
// first check if we've already modified this state
|
||||
auto cacheEntry = lookup_state_cache(account, ns, key);
|
||||
if (cacheEntry && cacheEntry->get().first)
|
||||
{
|
||||
// if a cache entry already exists and it has already been modified
|
||||
// don't check grants again
|
||||
if (auto ret = set_state_cache(account, ns, key, data, true);
|
||||
!ret.has_value())
|
||||
return Unexpected(ret.error());
|
||||
bool const hasFix = hookCtx.applyCtx.view().rules().enabled(fixHookMap);
|
||||
|
||||
return data.size();
|
||||
if (!hasFix)
|
||||
{
|
||||
// first check if we've already modified this state
|
||||
auto cacheEntry = lookup_state_cache(account, ns, key);
|
||||
if (cacheEntry && cacheEntry->get().first)
|
||||
{
|
||||
// if a cache entry already exists and it has already been modified
|
||||
// don't check grants again
|
||||
if (auto ret = set_state_cache(account, ns, key, data, true);
|
||||
!ret.has_value())
|
||||
return Unexpected(ret.error());
|
||||
|
||||
return data.size();
|
||||
}
|
||||
}
|
||||
|
||||
// cache miss or cache was present but entry was not marked as previously
|
||||
// modified therefore before continuing we need to check grants
|
||||
auto const sle =
|
||||
hookCtx.applyCtx.view().read(ripple::keylet::hook(account));
|
||||
if (!sle)
|
||||
return Unexpected(INTERNAL_ERROR);
|
||||
|
||||
bool found_auth = false;
|
||||
|
||||
// we do this by iterating the hooks installed on the foreign account and in
|
||||
// turn their grants and namespaces
|
||||
auto const& hooks = sle->getFieldArray(sfHooks);
|
||||
for (auto const& hookObj : hooks)
|
||||
// check if we've used a grant to modify this state entry before, if not
|
||||
// look up possible grants
|
||||
if (!hasFix ||
|
||||
hookCtx.result.foreignStateGrantCache.find({account, ns}) ==
|
||||
hookCtx.result.foreignStateGrantCache.end())
|
||||
{
|
||||
// skip blank entries
|
||||
if (!hookObj.isFieldPresent(sfHookHash))
|
||||
continue;
|
||||
auto const sle =
|
||||
hookCtx.applyCtx.view().read(ripple::keylet::hook(account));
|
||||
|
||||
if (!hookObj.isFieldPresent(sfHookGrants))
|
||||
continue;
|
||||
|
||||
auto const& hookGrants = hookObj.getFieldArray(sfHookGrants);
|
||||
|
||||
if (hookGrants.size() < 1)
|
||||
continue;
|
||||
|
||||
// the grant allows the hook to modify the granter's namespace only
|
||||
if (hookObj.isFieldPresent(sfHookNamespace))
|
||||
if (!sle)
|
||||
{
|
||||
if (hookObj.getFieldH256(sfHookNamespace) != ns)
|
||||
continue;
|
||||
}
|
||||
else
|
||||
{
|
||||
// fetch the hook definition
|
||||
auto const def =
|
||||
hookCtx.applyCtx.view().read(ripple::keylet::hookDefinition(
|
||||
hookObj.getFieldH256(sfHookHash)));
|
||||
if (!def) // should never happen except in a rare race condition
|
||||
continue;
|
||||
if (def->getFieldH256(sfHookNamespace) != ns)
|
||||
continue;
|
||||
}
|
||||
|
||||
// this is expensive search so we'll disallow after one failed attempt
|
||||
for (auto const& hookGrantObj : hookGrants)
|
||||
{
|
||||
bool hasAuthorizedField = hookGrantObj.isFieldPresent(sfAuthorize);
|
||||
|
||||
if (hookGrantObj.getFieldH256(sfHookHash) ==
|
||||
hookCtx.result.hookHash &&
|
||||
(!hasAuthorizedField ||
|
||||
hookGrantObj.getAccountID(sfAuthorize) ==
|
||||
hookCtx.result.account))
|
||||
if (hasFix)
|
||||
{
|
||||
found_auth = true;
|
||||
break;
|
||||
hookCtx.result.foreignStateSetDisabled = true;
|
||||
return Unexpected(NOT_AUTHORIZED);
|
||||
}
|
||||
|
||||
return Unexpected(INTERNAL_ERROR);
|
||||
}
|
||||
|
||||
if (found_auth)
|
||||
break;
|
||||
}
|
||||
// RH TODO: test this code path more completely
|
||||
|
||||
if (!found_auth)
|
||||
{
|
||||
// hook only gets one attempt
|
||||
hookCtx.result.foreignStateSetDisabled = true;
|
||||
return Unexpected(NOT_AUTHORIZED);
|
||||
bool found_auth = false;
|
||||
|
||||
// we do this by iterating the hooks installed on the foreign account
|
||||
// and in turn their grants and namespaces
|
||||
auto const& hooks = sle->getFieldArray(sfHooks);
|
||||
for (auto const& hookObj : hooks)
|
||||
{
|
||||
// skip blank entries
|
||||
if (!hookObj.isFieldPresent(sfHookHash))
|
||||
continue;
|
||||
|
||||
if (!hookObj.isFieldPresent(sfHookGrants))
|
||||
continue;
|
||||
|
||||
auto const& hookGrants = hookObj.getFieldArray(sfHookGrants);
|
||||
|
||||
if (hookGrants.size() < 1)
|
||||
continue;
|
||||
|
||||
// the grant allows the hook to modify the granter's namespace only
|
||||
if (hookObj.isFieldPresent(sfHookNamespace))
|
||||
{
|
||||
if (hookObj.getFieldH256(sfHookNamespace) != ns)
|
||||
continue;
|
||||
}
|
||||
else
|
||||
{
|
||||
// fetch the hook definition
|
||||
auto const def =
|
||||
hookCtx.applyCtx.view().read(ripple::keylet::hookDefinition(
|
||||
hookObj.getFieldH256(sfHookHash)));
|
||||
if (!def) // should never happen except in a rare race
|
||||
// condition
|
||||
continue;
|
||||
if (def->getFieldH256(sfHookNamespace) != ns)
|
||||
continue;
|
||||
}
|
||||
|
||||
// this is expensive search so we'll disallow after one failed
|
||||
// attempt
|
||||
for (auto const& hookGrantObj : hookGrants)
|
||||
{
|
||||
bool hasAuthorizedField =
|
||||
hookGrantObj.isFieldPresent(sfAuthorize);
|
||||
|
||||
if (hookGrantObj.getFieldH256(sfHookHash) ==
|
||||
hookCtx.result.hookHash &&
|
||||
(!hasAuthorizedField ||
|
||||
hookGrantObj.getAccountID(sfAuthorize) ==
|
||||
hookCtx.result.account))
|
||||
{
|
||||
found_auth = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (found_auth)
|
||||
break;
|
||||
}
|
||||
|
||||
if (!found_auth)
|
||||
{
|
||||
// hook only gets one attempt
|
||||
hookCtx.result.foreignStateSetDisabled = true;
|
||||
return Unexpected(NOT_AUTHORIZED);
|
||||
}
|
||||
|
||||
// add the grant to the cache
|
||||
hookCtx.result.foreignStateGrantCache.emplace(account, ns);
|
||||
}
|
||||
|
||||
if (auto ret = set_state_cache(account, ns, key, data, true);
|
||||
|
||||
@@ -200,6 +200,10 @@ getTransactionalStakeHolders(STTx const& tx, ReadView const& rv)
|
||||
issuer,
|
||||
(ut->getFlags() & lsfBurnable) ? tshSTRONG : tshWEAK);
|
||||
|
||||
if (rv.rules().enabled(featureURITokenBroker) &&
|
||||
tx.isFieldPresent(sfBrokerAccount))
|
||||
ADD_TSH(tx.getAccountID(sfBrokerAccount), tshWEAK);
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
|
||||
@@ -29,6 +29,33 @@
|
||||
|
||||
namespace ripple {
|
||||
|
||||
TER
|
||||
checkBroker(
|
||||
ReadView const& view,
|
||||
AccountID const& broker,
|
||||
AccountID const& owner,
|
||||
STAmount const& saleAmount,
|
||||
STAmount const& purchaseAmount)
|
||||
{
|
||||
auto const sleBroker = view.read(keylet::account(broker));
|
||||
if (!sleBroker)
|
||||
return tecNO_TARGET;
|
||||
|
||||
if (sleBroker->isFieldPresent(sfAMMID))
|
||||
return tecNO_PERMISSION;
|
||||
|
||||
if (broker == owner || saleAmount == beast::zero ||
|
||||
purchaseAmount == saleAmount)
|
||||
return tecNO_PERMISSION;
|
||||
|
||||
if (!purchaseAmount.native() && broker != purchaseAmount.getIssuer() &&
|
||||
!view.exists(keylet::line(
|
||||
broker, purchaseAmount.getIssuer(), purchaseAmount.getCurrency())))
|
||||
return tecNO_LINE;
|
||||
|
||||
return tesSUCCESS;
|
||||
}
|
||||
|
||||
NotTEC
|
||||
URIToken::preflight(PreflightContext const& ctx)
|
||||
{
|
||||
@@ -138,6 +165,23 @@ URIToken::preflight(PreflightContext const& ctx)
|
||||
ctx.tx.getAccountID(sfAccount) == ctx.tx.getAccountID(sfDestination))
|
||||
return temREDUNDANT;
|
||||
|
||||
if (ctx.tx.isFieldPresent(sfBrokerAccount))
|
||||
{
|
||||
if (tt != ttURITOKEN_BUY)
|
||||
return temMALFORMED;
|
||||
|
||||
if (!ctx.rules.enabled(featureURITokenBroker) ||
|
||||
!ctx.rules.enabled(fixXahauV1))
|
||||
return temDISABLED;
|
||||
|
||||
auto const broker = ctx.tx.getAccountID(sfBrokerAccount);
|
||||
if (broker == beast::zero)
|
||||
return temMALFORMED;
|
||||
|
||||
if (broker == ctx.tx.getAccountID(sfAccount))
|
||||
return temREDUNDANT;
|
||||
}
|
||||
|
||||
return preflight2(ctx);
|
||||
}
|
||||
|
||||
@@ -235,6 +279,21 @@ URIToken::preclaim(PreclaimContext const& ctx)
|
||||
if (purchaseAmount < saleAmount)
|
||||
return tecINSUFFICIENT_PAYMENT;
|
||||
|
||||
if (ctx.tx.isFieldPresent(sfBrokerAccount))
|
||||
{
|
||||
if (!ctx.view.rules().enabled(featureURITokenBroker) || !fixV1)
|
||||
return temDISABLED;
|
||||
|
||||
if (TER const result = checkBroker(
|
||||
ctx.view,
|
||||
ctx.tx.getAccountID(sfBrokerAccount),
|
||||
*owner,
|
||||
*saleAmount,
|
||||
purchaseAmount);
|
||||
!isTesSuccess(result))
|
||||
return result;
|
||||
}
|
||||
|
||||
if (fixV1)
|
||||
{
|
||||
if (purchaseAmount.native() && saleAmount->native())
|
||||
@@ -483,6 +542,11 @@ URIToken::doApply()
|
||||
if (purchaseAmount.issue() != saleAmount->issue())
|
||||
return temBAD_CURRENCY;
|
||||
|
||||
std::optional<AccountID> const broker = ctx_.tx[~sfBrokerAccount];
|
||||
if (broker &&
|
||||
(!sb.rules().enabled(featureURITokenBroker) || !fixV1))
|
||||
return temDISABLED;
|
||||
|
||||
if (fixV1)
|
||||
{
|
||||
// this is the reworked version of the buy routine
|
||||
@@ -490,6 +554,14 @@ URIToken::doApply()
|
||||
if (purchaseAmount < saleAmount)
|
||||
return tecINSUFFICIENT_PAYMENT;
|
||||
|
||||
if (broker)
|
||||
{
|
||||
if (TER const result = checkBroker(
|
||||
sb, *broker, *owner, *saleAmount, purchaseAmount);
|
||||
!isTesSuccess(result))
|
||||
return result;
|
||||
}
|
||||
|
||||
// if it's an xrp sale/purchase then no trustline needed
|
||||
if (purchaseAmount.native())
|
||||
{
|
||||
@@ -525,6 +597,23 @@ URIToken::doApply()
|
||||
return result;
|
||||
}
|
||||
|
||||
if (broker)
|
||||
{
|
||||
if (TER result = trustTransferAllowed(
|
||||
sb,
|
||||
{account_, *broker},
|
||||
purchaseAmount.issue(),
|
||||
j);
|
||||
!isTesSuccess(result))
|
||||
{
|
||||
JLOG(j.trace()) << "URIToken::doApply broker "
|
||||
"trustTransferAllowed result="
|
||||
<< result;
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
if (STAmount availableFunds{accountFunds(
|
||||
sb, account_, purchaseAmount, fhZERO_IF_FROZEN, j)};
|
||||
purchaseAmount > availableFunds)
|
||||
@@ -536,13 +625,31 @@ URIToken::doApply()
|
||||
sb,
|
||||
account_,
|
||||
*owner,
|
||||
purchaseAmount,
|
||||
broker ? *saleAmount : purchaseAmount,
|
||||
j,
|
||||
WaiveTransferFee::No,
|
||||
false);
|
||||
!isTesSuccess(result))
|
||||
return result;
|
||||
|
||||
if (broker)
|
||||
{
|
||||
STAmount const spread = purchaseAmount - *saleAmount;
|
||||
if (spread <= beast::zero)
|
||||
return tecINTERNAL; // LCOV_EXCL_LINE
|
||||
|
||||
if (TER result = accountSend(
|
||||
sb,
|
||||
account_,
|
||||
*broker,
|
||||
spread,
|
||||
j,
|
||||
WaiveTransferFee::No,
|
||||
false);
|
||||
!isTesSuccess(result))
|
||||
return result;
|
||||
}
|
||||
|
||||
// add token to new owner dir
|
||||
auto const newPage = sb.dirInsert(
|
||||
keylet::ownerDir(account_),
|
||||
|
||||
Reference in New Issue
Block a user