mirror of
https://github.com/Xahau/xahaud.git
synced 2025-11-19 18:15:50 +00:00
Compare commits
5 Commits
nd-simplif
...
uritoken-r
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
80b8207b0d | ||
|
|
0d0adb4236 | ||
|
|
1de0493145 | ||
|
|
164b3a1c3d | ||
|
|
9b7103ea4d |
@@ -492,6 +492,7 @@ LedgerEntryTypesMatch::visitEntry(
|
||||
case ltURI_TOKEN:
|
||||
case ltIMPORT_VLSEQ:
|
||||
case ltUNL_REPORT:
|
||||
case ltURI_TOKEN_OFFER:
|
||||
break;
|
||||
default:
|
||||
invalidTypeAdded_ = true;
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
#include <ripple/app/ledger/Ledger.h>
|
||||
#include <ripple/app/tx/impl/URIToken.h>
|
||||
#include <ripple/basics/Log.h>
|
||||
#include <ripple/ledger/View.h>
|
||||
#include <ripple/protocol/Feature.h>
|
||||
#include <ripple/protocol/Indexes.h>
|
||||
#include <ripple/protocol/Quality.h>
|
||||
@@ -29,6 +30,319 @@
|
||||
|
||||
namespace ripple {
|
||||
|
||||
static TER
|
||||
transfer(
|
||||
Sandbox& sb,
|
||||
AccountID const& buyer,
|
||||
AccountID const& seller,
|
||||
Keylet const& kl,
|
||||
std::shared_ptr<SLE> const& sleU,
|
||||
std::shared_ptr<SLE> const& sleBuyer,
|
||||
std::shared_ptr<SLE> const& sleSeller,
|
||||
beast::Journal journal)
|
||||
{
|
||||
// add token to new owner dir
|
||||
auto const newPage = sb.dirInsert(
|
||||
keylet::ownerDir(buyer), kl, describeOwnerDir(buyer));
|
||||
|
||||
JLOG(journal.trace()) << "Adding URIToken to owner directory "
|
||||
<< to_string(kl.key) << ": "
|
||||
<< (newPage ? "success" : "failure");
|
||||
|
||||
if (!newPage)
|
||||
return tecDIR_FULL;
|
||||
|
||||
// remove from current owner directory
|
||||
if (!sb.dirRemove(
|
||||
keylet::ownerDir(seller),
|
||||
sleU->getFieldU64(sfOwnerNode),
|
||||
kl.key,
|
||||
true))
|
||||
{
|
||||
JLOG(journal.fatal())
|
||||
<< "Could not remove URIToken from owner directory";
|
||||
|
||||
return tefBAD_LEDGER;
|
||||
}
|
||||
|
||||
// adjust owner counts
|
||||
adjustOwnerCount(sb, sleSeller, -1, journal);
|
||||
adjustOwnerCount(sb, sleBuyer, 1, journal);
|
||||
|
||||
// clean the offer off the object
|
||||
sleU->makeFieldAbsent(sfAmount);
|
||||
if (sleU->isFieldPresent(sfDestination))
|
||||
sleU->makeFieldAbsent(sfDestination);
|
||||
|
||||
// set the new owner of the object
|
||||
sleU->setAccountID(sfOwner, buyer);
|
||||
|
||||
// tell the ledger where to find it
|
||||
sleU->setFieldU64(sfOwnerNode, *newPage);
|
||||
|
||||
// check each side has sufficient balance remaining to cover the
|
||||
// updated ownercounts
|
||||
auto hasSufficientReserve = [&](std::shared_ptr<SLE> const& sle) -> bool {
|
||||
std::uint32_t const uOwnerCount = sle->getFieldU32(sfOwnerCount);
|
||||
return sle->getFieldAmount(sfBalance) >=
|
||||
sb.fees().accountReserve(uOwnerCount);
|
||||
};
|
||||
|
||||
if (!hasSufficientReserve(sleBuyer))
|
||||
{
|
||||
JLOG(journal.trace()) << "URIToken: buyer " << buyer
|
||||
<< " has insufficient reserve to buy";
|
||||
return tecINSUFFICIENT_RESERVE;
|
||||
}
|
||||
|
||||
// This should only happen if the owner burned their reserves
|
||||
// below the needed amount via another transactor. If this
|
||||
// happens they should top up their account before selling!
|
||||
if (!hasSufficientReserve(sleSeller))
|
||||
{
|
||||
JLOG(journal.warn()) << "URIToken: seller " << seller
|
||||
<< " has insufficient reserve to allow purchase!";
|
||||
return tecINSUF_RESERVE_SELLER;
|
||||
}
|
||||
|
||||
sb.update(sleU);
|
||||
sb.update(sleBuyer);
|
||||
sb.update(sleSeller);
|
||||
return tesSUCCESS;
|
||||
}
|
||||
|
||||
static TER
|
||||
fullSwap(
|
||||
Sandbox& sb,
|
||||
STAmount const& priorBalance,
|
||||
STAmount const& purchaseAmount,
|
||||
STAmount const& saleAmount,
|
||||
STAmount const& fee,
|
||||
AccountID const& buyer,
|
||||
AccountID const& seller,
|
||||
AccountID const& issuer,
|
||||
Keylet const& kl,
|
||||
std::shared_ptr<SLE> const& sleU,
|
||||
std::shared_ptr<SLE> const& sleBuyer,
|
||||
std::shared_ptr<SLE> const& sleSeller,
|
||||
beast::Journal journal)
|
||||
{
|
||||
if (purchaseAmount < saleAmount)
|
||||
return tecINSUFFICIENT_PAYMENT;
|
||||
|
||||
// if it's an xrp sale/purchase then no trustline needed
|
||||
if (purchaseAmount.native())
|
||||
{
|
||||
STAmount needed{sb.fees().accountReserve(
|
||||
sleBuyer->getFieldU32(sfOwnerCount) + 1)};
|
||||
|
||||
// STAmount const fee = ctx_.tx.getFieldAmount(sfFee).xrp();
|
||||
|
||||
if (needed + fee < needed)
|
||||
return tecINTERNAL;
|
||||
|
||||
needed += fee;
|
||||
|
||||
if (needed + purchaseAmount < needed)
|
||||
return tecINTERNAL;
|
||||
|
||||
needed += purchaseAmount;
|
||||
|
||||
if (needed > priorBalance)
|
||||
return tecINSUFFICIENT_FUNDS;
|
||||
}
|
||||
else
|
||||
{
|
||||
// IOU sale
|
||||
if (TER result = trustTransferAllowed(
|
||||
sb, {buyer, seller}, purchaseAmount.issue(), journal);
|
||||
!isTesSuccess(result))
|
||||
{
|
||||
JLOG(journal.trace())
|
||||
<< "URIToken::doApply trustTransferAllowed result=" << result;
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
if (STAmount availableFunds{accountFunds(
|
||||
sb, buyer, purchaseAmount, fhZERO_IF_FROZEN, journal)};
|
||||
purchaseAmount > availableFunds)
|
||||
return tecINSUFFICIENT_FUNDS;
|
||||
}
|
||||
|
||||
// execute the funds transfer, we'll check reserves last
|
||||
if (TER result =
|
||||
accountSend(sb, buyer, seller, purchaseAmount, journal, false);
|
||||
!isTesSuccess(result))
|
||||
return result;
|
||||
|
||||
if (TER result = transfer(
|
||||
sb,
|
||||
buyer,
|
||||
seller,
|
||||
kl,
|
||||
sleU,
|
||||
sleBuyer,
|
||||
sleSeller,
|
||||
journal);
|
||||
!isTesSuccess(result))
|
||||
{
|
||||
JLOG(journal.fatal())
|
||||
<< "URIToken: transfer failed: " << result;
|
||||
return result;
|
||||
}
|
||||
return tesSUCCESS;
|
||||
}
|
||||
|
||||
static TER
|
||||
offerLock(
|
||||
Sandbox& sb,
|
||||
STAmount const& priorBalance,
|
||||
STAmount const& purchaseAmount,
|
||||
AccountID const& buyer,
|
||||
AccountID const& seller,
|
||||
AccountID const& issuer,
|
||||
Keylet const& kl,
|
||||
std::shared_ptr<SLE> const& sleU,
|
||||
std::shared_ptr<SLE> const& sleBuyer,
|
||||
std::shared_ptr<SLE> const& sleSeller,
|
||||
beast::Journal journal)
|
||||
{
|
||||
if (isXRP(purchaseAmount))
|
||||
(*sleBuyer)[sfBalance] = (*sleBuyer)[sfBalance] - purchaseAmount;
|
||||
else
|
||||
{
|
||||
// if (!sb.rules().enabled(featureRoyalties))
|
||||
// return temDISABLED;
|
||||
|
||||
TER const result = trustTransferAllowed(sb, {buyer, seller}, purchaseAmount.issue(), journal);
|
||||
if (!isTesSuccess(result))
|
||||
return result;
|
||||
|
||||
auto sleLine = sb.peek(keylet::line(buyer, purchaseAmount.getIssuer(), purchaseAmount.getCurrency()));
|
||||
if (purchaseAmount.getIssuer() != buyer)
|
||||
{
|
||||
if (!sleLine)
|
||||
return tecNO_LINE;
|
||||
|
||||
TER const result = trustAdjustLockedBalance(sb, sleLine, purchaseAmount, 1, journal, WetRun);
|
||||
if (!isTesSuccess(result))
|
||||
return result;
|
||||
}
|
||||
}
|
||||
return tesSUCCESS;
|
||||
}
|
||||
|
||||
static TER
|
||||
offerUnLock(
|
||||
Sandbox& sb,
|
||||
STAmount const& priorBalance,
|
||||
STAmount const& purchaseAmount,
|
||||
AccountID const& buyer,
|
||||
AccountID const& seller,
|
||||
AccountID const& issuer,
|
||||
Keylet const& kl,
|
||||
std::shared_ptr<SLE> const& sleU,
|
||||
std::shared_ptr<SLE> const& sleBuyer,
|
||||
std::shared_ptr<SLE> const& sleSeller,
|
||||
std::shared_ptr<SLE> const& sleIssuer,
|
||||
beast::Journal journal)
|
||||
{
|
||||
STAmount deliverAmount = purchaseAmount;
|
||||
STAmount royaltyAmount = STAmount{0};
|
||||
|
||||
// Amendment Guard
|
||||
if (sleU->isFieldPresent(sfRoyaltyRate))
|
||||
{
|
||||
// auto const royaltyRate = sleU->getFieldU32(sfRoyaltyRate);
|
||||
// royaltyAmount = multiplyRound(
|
||||
// deliverAmount, Rate{royaltyRate}, deliverAmount.issue(), true);
|
||||
// deliverAmount -= royaltyAmount;
|
||||
// STAmount deliverAmount = purchaseAmount;
|
||||
static Rate const parityRate(QUALITY_ONE);
|
||||
auto const royaltyRate = [&]() -> Rate {
|
||||
if (sleU->isFieldPresent(sfRoyaltyRate))
|
||||
return Rate{sleU->getFieldU32(sfRoyaltyRate)};
|
||||
return Rate{QUALITY_ONE};
|
||||
}();
|
||||
|
||||
if (royaltyRate != parityRate)
|
||||
deliverAmount = multiplyRound(purchaseAmount, royaltyRate, purchaseAmount.issue(), true);
|
||||
royaltyAmount = deliverAmount - purchaseAmount;
|
||||
deliverAmount = purchaseAmount - royaltyAmount;
|
||||
}
|
||||
|
||||
if (isXRP(purchaseAmount))
|
||||
{
|
||||
(*sleSeller)[sfBalance] = (*sleSeller)[sfBalance] + deliverAmount;
|
||||
(*sleIssuer)[sfBalance] = (*sleIssuer)[sfBalance] + royaltyAmount;
|
||||
}
|
||||
else
|
||||
{
|
||||
// if (!sb.rules().enabled(featureRoyalties))
|
||||
// return temDISABLED;
|
||||
|
||||
auto const xferRate = transferRate(sb, deliverAmount.getIssuer());
|
||||
if (TER const result = trustTransferLockedBalance(
|
||||
sb,
|
||||
seller,
|
||||
sleBuyer,
|
||||
sleSeller,
|
||||
deliverAmount,
|
||||
0,
|
||||
xferRate,
|
||||
journal,
|
||||
WetRun); !isTesSuccess(result))
|
||||
{
|
||||
JLOG(journal.fatal())
|
||||
<< "URIToken: trustTransferLockedBalance failed: " << result;
|
||||
return result;
|
||||
}
|
||||
|
||||
if (royaltyAmount != beast::zero)
|
||||
{
|
||||
if (TER const result = trustTransferLockedBalance(
|
||||
sb,
|
||||
seller,
|
||||
sleBuyer,
|
||||
sleIssuer,
|
||||
royaltyAmount,
|
||||
0,
|
||||
xferRate,
|
||||
journal,
|
||||
WetRun); !isTesSuccess(result))
|
||||
{
|
||||
JLOG(journal.fatal())
|
||||
<< "URIToken: trustTransferLockedBalance failed: " << result;
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// transfer the token to the new owner
|
||||
if (TER const result = transfer(
|
||||
sb,
|
||||
buyer,
|
||||
seller,
|
||||
kl,
|
||||
sleU,
|
||||
sleBuyer,
|
||||
sleSeller,
|
||||
journal);
|
||||
!isTesSuccess(result))
|
||||
{
|
||||
JLOG(journal.fatal())
|
||||
<< "URIToken: transfer failed: " << result;
|
||||
return result;
|
||||
}
|
||||
|
||||
sb.update(sleU);
|
||||
sb.update(sleBuyer);
|
||||
sb.update(sleSeller);
|
||||
sb.update(sleIssuer);
|
||||
return tesSUCCESS;
|
||||
}
|
||||
|
||||
NotTEC
|
||||
URIToken::preflight(PreflightContext const& ctx)
|
||||
{
|
||||
@@ -116,6 +430,32 @@ URIToken::preflight(PreflightContext const& ctx)
|
||||
case ttURITOKEN_MINT: {
|
||||
if (flags & tfURITokenMintMask)
|
||||
return temINVALID_FLAG;
|
||||
|
||||
// Amendment Guard
|
||||
if (ctx.tx.isFieldPresent(sfRoyaltyRate))
|
||||
{
|
||||
std::uint32_t uRate = ctx.tx.getFieldU32(sfRoyaltyRate);
|
||||
if (uRate == 0)
|
||||
{
|
||||
JLOG(ctx.j.error())
|
||||
<< "Malformed transaction: Royalty rate unset.";
|
||||
return temBAD_TRANSFER_RATE;
|
||||
}
|
||||
|
||||
if (uRate && (uRate < QUALITY_ONE))
|
||||
{
|
||||
JLOG(ctx.j.error())
|
||||
<< "Malformed transaction: Transfer rate too small.";
|
||||
return temBAD_TRANSFER_RATE;
|
||||
}
|
||||
|
||||
if (uRate > 2 * QUALITY_ONE)
|
||||
{
|
||||
JLOG(ctx.j.error())
|
||||
<< "Malformed transaction: Transfer rate too large.";
|
||||
return temBAD_TRANSFER_RATE;
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -125,7 +465,6 @@ URIToken::preflight(PreflightContext const& ctx)
|
||||
case ttURITOKEN_CREATE_SELL_OFFER: {
|
||||
if (flags & tfURITokenNonMintMask)
|
||||
return temINVALID_FLAG;
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -220,7 +559,11 @@ URIToken::preclaim(PreclaimContext const& ctx)
|
||||
|
||||
// check if the seller has listed it at all
|
||||
if (!saleAmount)
|
||||
return tecNO_PERMISSION;
|
||||
{
|
||||
// Amendment Guard
|
||||
// return tecNO_PERMISSION;
|
||||
return tesSUCCESS;
|
||||
}
|
||||
|
||||
// check if the seller has listed it for sale to a specific account
|
||||
if (dest && *dest != acc)
|
||||
@@ -434,6 +777,11 @@ URIToken::doApply()
|
||||
if (flags & tfBurnable)
|
||||
sleU->setFlag(tfBurnable);
|
||||
|
||||
// Amendment Guard
|
||||
if (ctx_.tx.isFieldPresent(sfRoyaltyRate))
|
||||
sleU->setFieldU32(
|
||||
sfRoyaltyRate, ctx_.tx.getFieldU32(sfRoyaltyRate));
|
||||
|
||||
auto const page = sb.dirInsert(
|
||||
keylet::ownerDir(account_), *kl, describeOwnerDir(account_));
|
||||
|
||||
@@ -472,9 +820,71 @@ URIToken::doApply()
|
||||
case ttURITOKEN_BUY: {
|
||||
STAmount const purchaseAmount = ctx_.tx.getFieldAmount(sfAmount);
|
||||
|
||||
// OLD Amendment Guard
|
||||
// check if the seller has listed it at all
|
||||
if (!saleAmount)
|
||||
return tecNO_PERMISSION;
|
||||
// if (!saleAmount)
|
||||
// return tecNO_PERMISSION;
|
||||
|
||||
auto const offerMatched = [&]() -> bool {
|
||||
if (purchaseAmount.issue() != saleAmount->issue())
|
||||
return false;
|
||||
if (purchaseAmount != *saleAmount)
|
||||
return false;
|
||||
return true;
|
||||
};
|
||||
|
||||
if (!saleAmount || !offerMatched())
|
||||
{
|
||||
// Amendment Guard
|
||||
auto uRate = getRate(purchaseAmount, STAmount{1});
|
||||
auto const buyOfferKey = keylet::uritoken_offer(
|
||||
*kl,
|
||||
uRate,
|
||||
purchaseAmount.getIssuer(),
|
||||
purchaseAmount.getCurrency());
|
||||
|
||||
// Add offer to owner's directory.
|
||||
auto const ownerNode = sb.dirInsert(
|
||||
keylet::ownerDir(account_),
|
||||
buyOfferKey,
|
||||
describeOwnerDir(account_));
|
||||
|
||||
if (!ownerNode)
|
||||
{
|
||||
JLOG(j_.debug()) << "final result: failed to add offer to "
|
||||
"owner's directory";
|
||||
return tecDIR_FULL;
|
||||
}
|
||||
|
||||
// Update owner count.
|
||||
adjustOwnerCount(sb, sle, 1, j);
|
||||
|
||||
// Create Offer
|
||||
auto sleOffer = std::make_shared<SLE>(buyOfferKey);
|
||||
sleOffer->setAccountID(sfOwner, account_);
|
||||
sleOffer->setFieldH256(sfURITokenID, kl->key);
|
||||
sleOffer->setFieldAmount(sfAmount, purchaseAmount);
|
||||
sleOffer->setFieldU64(sfOwnerNode, *ownerNode);
|
||||
sb.insert(sleOffer);
|
||||
|
||||
// Lock the amount (XRP or IOU) in the offer
|
||||
if (TER const ter = offerLock(
|
||||
sb,
|
||||
mPriorBalance,
|
||||
purchaseAmount,
|
||||
account_, // receiver
|
||||
*owner, // sender
|
||||
*issuer, // issuer
|
||||
*kl,
|
||||
sleU,
|
||||
sle, // receiver sle
|
||||
sleOwner, // sender sle
|
||||
j_); !isTesSuccess(ter))
|
||||
return ter;
|
||||
|
||||
sb.apply(ctx_.rawView());
|
||||
return tesSUCCESS;
|
||||
}
|
||||
|
||||
// check if the seller has listed it for sale to a specific account
|
||||
if (dest && *dest != account_)
|
||||
@@ -486,129 +896,23 @@ URIToken::doApply()
|
||||
if (fixV1)
|
||||
{
|
||||
// this is the reworked version of the buy routine
|
||||
|
||||
if (purchaseAmount < saleAmount)
|
||||
return tecINSUFFICIENT_PAYMENT;
|
||||
|
||||
// if it's an xrp sale/purchase then no trustline needed
|
||||
if (purchaseAmount.native())
|
||||
{
|
||||
STAmount needed{sb.fees().accountReserve(
|
||||
sle->getFieldU32(sfOwnerCount) + 1)};
|
||||
|
||||
STAmount const fee = ctx_.tx.getFieldAmount(sfFee).xrp();
|
||||
|
||||
if (needed + fee < needed)
|
||||
return tecINTERNAL;
|
||||
|
||||
needed += fee;
|
||||
|
||||
if (needed + purchaseAmount < needed)
|
||||
return tecINTERNAL;
|
||||
|
||||
needed += purchaseAmount;
|
||||
|
||||
if (needed > mPriorBalance)
|
||||
return tecINSUFFICIENT_FUNDS;
|
||||
}
|
||||
else
|
||||
{
|
||||
// IOU sale
|
||||
if (TER result = trustTransferAllowed(
|
||||
sb, {account_, *owner}, purchaseAmount.issue(), j);
|
||||
!isTesSuccess(result))
|
||||
{
|
||||
JLOG(j.trace())
|
||||
<< "URIToken::doApply trustTransferAllowed result="
|
||||
<< result;
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
if (STAmount availableFunds{accountFunds(
|
||||
sb, account_, purchaseAmount, fhZERO_IF_FROZEN, j)};
|
||||
purchaseAmount > availableFunds)
|
||||
return tecINSUFFICIENT_FUNDS;
|
||||
}
|
||||
|
||||
// execute the funds transfer, we'll check reserves last
|
||||
if (TER result = accountSend(
|
||||
sb, account_, *owner, purchaseAmount, j, false);
|
||||
!isTesSuccess(result))
|
||||
return result;
|
||||
|
||||
// add token to new owner dir
|
||||
auto const newPage = sb.dirInsert(
|
||||
keylet::ownerDir(account_),
|
||||
if (auto const ter = fullSwap(
|
||||
sb,
|
||||
mPriorBalance,
|
||||
purchaseAmount,
|
||||
*saleAmount,
|
||||
fee,
|
||||
account_, // receiver
|
||||
*owner, // sender
|
||||
*issuer, // issuer
|
||||
*kl,
|
||||
describeOwnerDir(account_));
|
||||
|
||||
JLOG(j_.trace()) << "Adding URIToken to owner directory "
|
||||
<< to_string(kl->key) << ": "
|
||||
<< (newPage ? "success" : "failure");
|
||||
|
||||
if (!newPage)
|
||||
return tecDIR_FULL;
|
||||
|
||||
// remove from current owner directory
|
||||
if (!sb.dirRemove(
|
||||
keylet::ownerDir(*owner),
|
||||
sleU->getFieldU64(sfOwnerNode),
|
||||
kl->key,
|
||||
true))
|
||||
{
|
||||
JLOG(j.fatal())
|
||||
<< "Could not remove URIToken from owner directory";
|
||||
|
||||
return tefBAD_LEDGER;
|
||||
}
|
||||
|
||||
// adjust owner counts
|
||||
adjustOwnerCount(sb, sleOwner, -1, j);
|
||||
adjustOwnerCount(sb, sle, 1, j);
|
||||
|
||||
// clean the offer off the object
|
||||
sleU->makeFieldAbsent(sfAmount);
|
||||
if (sleU->isFieldPresent(sfDestination))
|
||||
sleU->makeFieldAbsent(sfDestination);
|
||||
|
||||
// set the new owner of the object
|
||||
sleU->setAccountID(sfOwner, account_);
|
||||
|
||||
// tell the ledger where to find it
|
||||
sleU->setFieldU64(sfOwnerNode, *newPage);
|
||||
|
||||
// check each side has sufficient balance remaining to cover the
|
||||
// updated ownercounts
|
||||
auto hasSufficientReserve =
|
||||
[&](std::shared_ptr<SLE> const& sle) -> bool {
|
||||
std::uint32_t const uOwnerCount =
|
||||
sle->getFieldU32(sfOwnerCount);
|
||||
return sle->getFieldAmount(sfBalance) >=
|
||||
sb.fees().accountReserve(uOwnerCount);
|
||||
};
|
||||
|
||||
if (!hasSufficientReserve(sle))
|
||||
{
|
||||
JLOG(j.trace()) << "URIToken: buyer " << account_
|
||||
<< " has insufficient reserve to buy";
|
||||
return tecINSUFFICIENT_RESERVE;
|
||||
}
|
||||
|
||||
// This should only happen if the owner burned their reserves
|
||||
// below the needed amount via another transactor. If this
|
||||
// happens they should top up their account before selling!
|
||||
if (!hasSufficientReserve(sleOwner))
|
||||
{
|
||||
JLOG(j.warn())
|
||||
<< "URIToken: seller " << *owner
|
||||
<< " has insufficient reserve to allow purchase!";
|
||||
return tecINSUF_RESERVE_SELLER;
|
||||
}
|
||||
|
||||
sb.update(sle);
|
||||
sb.update(sleU);
|
||||
sb.update(sleOwner);
|
||||
sleU,
|
||||
sle, // receiver sle
|
||||
sleOwner, // sender sle
|
||||
j_);
|
||||
!isTesSuccess(ter))
|
||||
return ter;
|
||||
sb.apply(ctx_.rawView());
|
||||
return tesSUCCESS;
|
||||
}
|
||||
@@ -750,8 +1054,7 @@ URIToken::doApply()
|
||||
true);
|
||||
}
|
||||
|
||||
initSellerBal = !sleDstLine
|
||||
? purchaseAmount.zeroed()
|
||||
initSellerBal = !sleDstLine ? purchaseAmount.zeroed()
|
||||
: sellerLow ? ((*sleDstLine)[sfBalance])
|
||||
: -((*sleDstLine)[sfBalance]);
|
||||
|
||||
@@ -1016,6 +1319,54 @@ URIToken::doApply()
|
||||
if (account_ != *owner)
|
||||
return tecNO_PERMISSION;
|
||||
|
||||
// Amendment Guard
|
||||
auto const amount = ctx_.tx[sfAmount];
|
||||
auto uRate = getRate(amount, STAmount{1});
|
||||
auto const buyOfferKey = keylet::uritoken_offer(
|
||||
*kl, uRate, amount.getIssuer(), amount.getCurrency());
|
||||
if (sb.exists(buyOfferKey))
|
||||
{
|
||||
auto const buyOfferSle = sb.peek(buyOfferKey);
|
||||
owner = buyOfferSle->getAccountID(sfOwner);
|
||||
STAmount const offerAmount =
|
||||
buyOfferSle->getFieldAmount(sfAmount);
|
||||
sleOwner = sb.peek(keylet::account(*owner));
|
||||
STAmount const fee = ctx_.tx.getFieldAmount(sfFee).xrp();
|
||||
|
||||
auto sleIssuer = sb.peek(keylet::account(*issuer));
|
||||
// Unlock & Transfer the amount (XRP or IOU) in the offer
|
||||
if (auto const ter = offerUnLock(
|
||||
sb,
|
||||
mPriorBalance,
|
||||
offerAmount,
|
||||
*owner, // buyer
|
||||
account_, // seller
|
||||
*issuer, // issuer
|
||||
*kl,
|
||||
sleU,
|
||||
sleOwner, // buyer
|
||||
sle, // seller
|
||||
sleIssuer, // issuer
|
||||
j_);
|
||||
!isTesSuccess(ter))
|
||||
return ter;
|
||||
|
||||
if (!sb.dirRemove(
|
||||
keylet::ownerDir(*owner),
|
||||
buyOfferSle->getFieldU64(sfOwnerNode),
|
||||
buyOfferKey.key,
|
||||
true))
|
||||
{
|
||||
JLOG(j.fatal()) << "Could not remove URITokenOffer from "
|
||||
"owner directory";
|
||||
return tefBAD_LEDGER;
|
||||
}
|
||||
adjustOwnerCount(sb, sleOwner, -1, j);
|
||||
sb.erase(buyOfferSle);
|
||||
sb.apply(ctx_.rawView());
|
||||
return tesSUCCESS;
|
||||
}
|
||||
|
||||
auto const txDest = ctx_.tx[~sfDestination];
|
||||
|
||||
// update destination where applicable
|
||||
@@ -1024,7 +1375,7 @@ URIToken::doApply()
|
||||
else if (dest)
|
||||
sleU->makeFieldAbsent(sfDestination);
|
||||
|
||||
sleU->setFieldAmount(sfAmount, ctx_.tx[sfAmount]);
|
||||
sleU->setFieldAmount(sfAmount, amount);
|
||||
|
||||
sb.update(sleU);
|
||||
sb.apply(ctx_.rawView());
|
||||
|
||||
@@ -22,6 +22,7 @@
|
||||
|
||||
#include <ripple/app/ledger/Ledger.h>
|
||||
#include <ripple/app/tx/impl/Transactor.h>
|
||||
// #include <ripple/app/tx/impl/URITokenUtils.h>
|
||||
#include <ripple/basics/Log.h>
|
||||
#include <ripple/protocol/Indexes.h>
|
||||
|
||||
|
||||
260
src/ripple/app/tx/impl/URITokenUtils.cpp
Normal file
260
src/ripple/app/tx/impl/URITokenUtils.cpp
Normal file
@@ -0,0 +1,260 @@
|
||||
//------------------------------------------------------------------------------
|
||||
/*
|
||||
This file is part of rippled: https://github.com/ripple/rippled
|
||||
Copyright (c) 2017 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 <ripple/app/tx/impl/URITokenUtils.h>
|
||||
#include <ripple/basics/Log.h>
|
||||
#include <ripple/protocol/Feature.h>
|
||||
#include <ripple/protocol/Indexes.h>
|
||||
#include <ripple/protocol/Quality.h>
|
||||
#include <ripple/protocol/STAccount.h>
|
||||
#include <ripple/protocol/TER.h>
|
||||
#include <ripple/protocol/TxFlags.h>
|
||||
|
||||
namespace ripple {
|
||||
namespace uritoken {
|
||||
|
||||
TER
|
||||
transfer(
|
||||
Sandbox& sb,
|
||||
AccountID const& receiver,
|
||||
AccountID const& sender,
|
||||
Keylet const& kl,
|
||||
std::shared_ptr<SLE> const& sleU,
|
||||
std::shared_ptr<SLE> const& sleReciever,
|
||||
std::shared_ptr<SLE> const& sleSender,
|
||||
beast::Journal journal)
|
||||
{
|
||||
// add token to new owner dir
|
||||
auto const newPage = sb.dirInsert(
|
||||
keylet::ownerDir(receiver), kl, describeOwnerDir(receiver));
|
||||
|
||||
JLOG(journal.trace()) << "Adding URIToken to owner directory "
|
||||
<< to_string(kl.key) << ": "
|
||||
<< (newPage ? "success" : "failure");
|
||||
|
||||
if (!newPage)
|
||||
return tecDIR_FULL;
|
||||
|
||||
// remove from current owner directory
|
||||
if (!sb.dirRemove(
|
||||
keylet::ownerDir(sender),
|
||||
sleU->getFieldU64(sfOwnerNode),
|
||||
kl.key,
|
||||
true))
|
||||
{
|
||||
JLOG(journal.fatal())
|
||||
<< "Could not remove URIToken from owner directory";
|
||||
|
||||
return tefBAD_LEDGER;
|
||||
}
|
||||
|
||||
// adjust owner counts
|
||||
adjustOwnerCount(sb, sleSender, -1, journal);
|
||||
adjustOwnerCount(sb, sleReciever, 1, journal);
|
||||
|
||||
// clean the offer off the object
|
||||
sleU->makeFieldAbsent(sfAmount);
|
||||
if (sleU->isFieldPresent(sfDestination))
|
||||
sleU->makeFieldAbsent(sfDestination);
|
||||
|
||||
// set the new owner of the object
|
||||
sleU->setAccountID(sfOwner, receiver);
|
||||
|
||||
// tell the ledger where to find it
|
||||
sleU->setFieldU64(sfOwnerNode, *newPage);
|
||||
|
||||
// check each side has sufficient balance remaining to cover the
|
||||
// updated ownercounts
|
||||
auto hasSufficientReserve = [&](std::shared_ptr<SLE> const& sle) -> bool {
|
||||
std::uint32_t const uOwnerCount = sle->getFieldU32(sfOwnerCount);
|
||||
return sle->getFieldAmount(sfBalance) >=
|
||||
sb.fees().accountReserve(uOwnerCount);
|
||||
};
|
||||
|
||||
if (!hasSufficientReserve(sleReciever))
|
||||
{
|
||||
JLOG(journal.trace()) << "URIToken: buyer " << receiver
|
||||
<< " has insufficient reserve to buy";
|
||||
return tecINSUFFICIENT_RESERVE;
|
||||
}
|
||||
|
||||
// This should only happen if the owner burned their reserves
|
||||
// below the needed amount via another transactor. If this
|
||||
// happens they should top up their account before selling!
|
||||
if (!hasSufficientReserve(sleSender))
|
||||
{
|
||||
JLOG(journal.warn()) << "URIToken: seller " << sender
|
||||
<< " has insufficient reserve to allow purchase!";
|
||||
return tecINSUF_RESERVE_SELLER;
|
||||
}
|
||||
|
||||
sb.update(sleU);
|
||||
sb.update(sleReciever);
|
||||
sb.update(sleSender);
|
||||
return tesSUCCESS;
|
||||
}
|
||||
|
||||
TER
|
||||
fullSwap(
|
||||
Sandbox& sb,
|
||||
STAmount const& priorBalance,
|
||||
STAmount const& purchaseAmount,
|
||||
STAmount const& saleAmount,
|
||||
STAmount const& fee,
|
||||
AccountID const& receiver,
|
||||
AccountID const& sender,
|
||||
AccountID const& issuer,
|
||||
Keylet const& kl,
|
||||
std::shared_ptr<SLE> const& sleU,
|
||||
std::shared_ptr<SLE> const& sleReciever,
|
||||
std::shared_ptr<SLE> const& sleSender,
|
||||
beast::Journal journal)
|
||||
{
|
||||
if (purchaseAmount < saleAmount)
|
||||
return tecINSUFFICIENT_PAYMENT;
|
||||
|
||||
// if it's an xrp sale/purchase then no trustline needed
|
||||
if (purchaseAmount.native())
|
||||
{
|
||||
STAmount needed{sb.fees().accountReserve(
|
||||
sleReciever->getFieldU32(sfOwnerCount) + 1)};
|
||||
|
||||
// STAmount const fee = ctx_.tx.getFieldAmount(sfFee).xrp();
|
||||
|
||||
if (needed + fee < needed)
|
||||
return tecINTERNAL;
|
||||
|
||||
needed += fee;
|
||||
|
||||
if (needed + purchaseAmount < needed)
|
||||
return tecINTERNAL;
|
||||
|
||||
needed += purchaseAmount;
|
||||
|
||||
if (needed > priorBalance)
|
||||
return tecINSUFFICIENT_FUNDS;
|
||||
}
|
||||
else
|
||||
{
|
||||
// IOU sale
|
||||
if (TER result = trustTransferAllowed(
|
||||
sb, {receiver, sender}, purchaseAmount.issue(), journal);
|
||||
!isTesSuccess(result))
|
||||
{
|
||||
JLOG(journal.trace())
|
||||
<< "URIToken::doApply trustTransferAllowed result=" << result;
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
if (STAmount availableFunds{accountFunds(
|
||||
sb, receiver, purchaseAmount, fhZERO_IF_FROZEN, journal)};
|
||||
purchaseAmount > availableFunds)
|
||||
return tecINSUFFICIENT_FUNDS;
|
||||
}
|
||||
|
||||
// execute the funds transfer, we'll check reserves last
|
||||
if (TER result =
|
||||
accountSend(sb, receiver, sender, purchaseAmount, journal, false);
|
||||
!isTesSuccess(result))
|
||||
return result;
|
||||
|
||||
if (TER result = transfer(
|
||||
sb,
|
||||
receiver,
|
||||
sender,
|
||||
kl,
|
||||
sleU,
|
||||
sleReciever,
|
||||
sleSender,
|
||||
journal);
|
||||
!isTesSuccess(result))
|
||||
{
|
||||
JLOG(journal.fatal())
|
||||
<< "URIToken: transfer failed: " << result;
|
||||
return result;
|
||||
}
|
||||
return tesSUCCESS;
|
||||
}
|
||||
|
||||
TER
|
||||
offerLock(
|
||||
Sandbox& sb,
|
||||
STAmount const& priorBalance,
|
||||
STAmount const& purchaseAmount,
|
||||
AccountID const& receiver,
|
||||
AccountID const& sender,
|
||||
AccountID const& issuer,
|
||||
Keylet const& kl,
|
||||
std::shared_ptr<SLE> const& sleU,
|
||||
std::shared_ptr<SLE> const& sleReciever,
|
||||
std::shared_ptr<SLE> const& sleSender,
|
||||
beast::Journal journal)
|
||||
{
|
||||
STAmount deliverAmount = purchaseAmount;
|
||||
STAmount royaltyAmount = STAmount{0};
|
||||
|
||||
// Amendment Guard
|
||||
if (sleU->isFieldPresent(sfRoyaltyRate))
|
||||
{
|
||||
auto const royaltyRate = sleU->getFieldU32(sfRoyaltyRate);
|
||||
royaltyAmount = multiplyRound(
|
||||
deliverAmount, Rate{royaltyRate}, deliverAmount.issue(), true);
|
||||
deliverAmount -= royaltyAmount;
|
||||
STAmount deliverAmount = purchaseAmount;
|
||||
static Rate const parityRate(QUALITY_ONE);
|
||||
auto const royaltyRate = [&]() -> Rate {
|
||||
if (sleU->isFieldPresent(sfRoyaltyRate))
|
||||
return Rate{sleU->getFieldU32(sfRoyaltyRate)};
|
||||
return Rate{QUALITY_ONE};
|
||||
}();
|
||||
|
||||
if (royaltyRate != parityRate)
|
||||
deliverAmount = multiplyRound(
|
||||
deliverAmount, royaltyRate, deliverAmount.issue(), true);
|
||||
royaltyAmount = deliverAmount - purchaseAmount;
|
||||
}
|
||||
|
||||
if (isXRP(purchaseAmount))
|
||||
(*sleSender)[sfBalance] = (*sleSender)[sfBalance] - purchaseAmount;
|
||||
else
|
||||
{
|
||||
// if (!sb.rules().enabled(featureRoyalties))
|
||||
// return temDISABLED;
|
||||
|
||||
TER const result = trustTransferAllowed(sb, {sender, receiver}, purchaseAmount.issue(), journal);
|
||||
if (!isTesSuccess(result))
|
||||
return result;
|
||||
|
||||
auto sleLine = sb.peek(keylet::line(sender, purchaseAmount.getIssuer(), purchaseAmount.getCurrency()));
|
||||
if (issuer != sender)
|
||||
{
|
||||
if (!sleLine)
|
||||
return tecNO_LINE;
|
||||
|
||||
TER const result = trustAdjustLockedBalance(sb, sleLine, purchaseAmount, 1, journal, WetRun);
|
||||
if (!isTesSuccess(result))
|
||||
return result;
|
||||
}
|
||||
}
|
||||
return tesSUCCESS;
|
||||
}
|
||||
|
||||
} // namespace uritoken
|
||||
} // namespace ripple
|
||||
75
src/ripple/app/tx/impl/URITokenUtils.h
Normal file
75
src/ripple/app/tx/impl/URITokenUtils.h
Normal file
@@ -0,0 +1,75 @@
|
||||
//------------------------------------------------------------------------------
|
||||
/*
|
||||
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_TX_URITOKENUTILS_H_INCLUDED
|
||||
#define RIPPLE_TX_URITOKENUTILS_H_INCLUDED
|
||||
|
||||
#include <ripple/app/tx/impl/Transactor.h>
|
||||
#include <ripple/basics/Log.h>
|
||||
#include <ripple/ledger/Sandbox.h>
|
||||
#include <ripple/protocol/TER.h>
|
||||
|
||||
namespace ripple {
|
||||
namespace uritoken {
|
||||
|
||||
TER
|
||||
transfer(
|
||||
Sandbox& sb,
|
||||
AccountID const& receiver,
|
||||
AccountID const& sender,
|
||||
Keylet const& kl,
|
||||
std::shared_ptr<SLE> const& sleU,
|
||||
std::shared_ptr<SLE> const& sleReciever,
|
||||
std::shared_ptr<SLE> const& sleSender,
|
||||
beast::Journal journal);
|
||||
|
||||
TER
|
||||
fullSwap(
|
||||
Sandbox& sb,
|
||||
STAmount const& priorBalance,
|
||||
STAmount const& purchaseAmount,
|
||||
STAmount const& saleAmount,
|
||||
STAmount const& fee,
|
||||
AccountID const& receiver,
|
||||
AccountID const& sender,
|
||||
AccountID const& issuer,
|
||||
Keylet const& kl,
|
||||
std::shared_ptr<SLE> const& sleU,
|
||||
std::shared_ptr<SLE> const& sleReciever,
|
||||
std::shared_ptr<SLE> const& sleSender,
|
||||
beast::Journal journal);
|
||||
|
||||
TER
|
||||
offerLock(
|
||||
Sandbox& sb,
|
||||
STAmount const& priorBalance,
|
||||
STAmount const& purchaseAmount,
|
||||
AccountID const& receiver,
|
||||
AccountID const& sender,
|
||||
AccountID const& issuer,
|
||||
Keylet const& kl,
|
||||
std::shared_ptr<SLE> const& sleU,
|
||||
std::shared_ptr<SLE> const& sleReciever,
|
||||
std::shared_ptr<SLE> const& sleSender,
|
||||
beast::Journal journal);
|
||||
|
||||
} // namespace uritoken
|
||||
} // namespace ripple
|
||||
|
||||
#endif // RIPPLE_TX_URITOKENUTILS_H_INCLUDED
|
||||
@@ -443,7 +443,8 @@ struct RunType
|
||||
{
|
||||
// see:
|
||||
// http://alumni.media.mit.edu/~rahimi/compile-time-flags/
|
||||
constexpr operator T() const
|
||||
constexpr
|
||||
operator T() const
|
||||
{
|
||||
static_assert(std::is_same<bool, T>::value);
|
||||
return V;
|
||||
@@ -493,6 +494,8 @@ trustAdjustLockedBalance(
|
||||
(std::is_same<V, ReadView const>::value &&
|
||||
std::is_same<S, std::shared_ptr<SLE const>>::value) ||
|
||||
(std::is_same<V, ApplyView>::value &&
|
||||
std::is_same<S, std::shared_ptr<SLE>>::value) ||
|
||||
(std::is_same<V, Sandbox>::value &&
|
||||
std::is_same<S, std::shared_ptr<SLE>>::value));
|
||||
|
||||
// dry runs are explicit in code, but really the view type determines
|
||||
@@ -589,8 +592,10 @@ trustAdjustLockedBalance(
|
||||
return tesSUCCESS;
|
||||
|
||||
if constexpr (
|
||||
std::is_same<V, ApplyView>::value &&
|
||||
std::is_same<S, std::shared_ptr<SLE>>::value)
|
||||
(std::is_same<V, ApplyView>::value &&
|
||||
std::is_same<S, std::shared_ptr<SLE>>::value) ||
|
||||
(std::is_same<V, Sandbox>::value &&
|
||||
std::is_same<S, std::shared_ptr<SLE>>::value))
|
||||
{
|
||||
if (finalLockedBalance == beast::zero || finalLockCount == 0)
|
||||
{
|
||||
@@ -785,18 +790,25 @@ trustTransferLockedBalance(
|
||||
R dryRun)
|
||||
{
|
||||
typedef typename std::conditional<
|
||||
std::is_same<V, ApplyView>::value && !dryRun,
|
||||
(std::is_same<V, ApplyView>::value ||
|
||||
std::is_same<V, Sandbox>::value) &&
|
||||
!dryRun,
|
||||
std::shared_ptr<SLE>,
|
||||
std::shared_ptr<SLE const>>::type SLEPtr;
|
||||
|
||||
auto peek = [&](Keylet& k) {
|
||||
if constexpr (std::is_same<V, ApplyView>::value && !dryRun)
|
||||
return const_cast<ApplyView&>(view).peek(k);
|
||||
else if constexpr (std::is_same<V, Sandbox>::value && !dryRun)
|
||||
return const_cast<Sandbox&>(view).peek(k);
|
||||
else
|
||||
return view.read(k);
|
||||
};
|
||||
|
||||
static_assert(std::is_same<V, ApplyView>::value || dryRun);
|
||||
static_assert(
|
||||
std::is_same<V, ApplyView>::value || std::is_same<V, Sandbox>::value ||
|
||||
dryRun,
|
||||
"trustTransferLockedBalance requires ApplyView or Sandbox");
|
||||
|
||||
if (!view.rules().enabled(featurePaychanAndEscrowForTokens))
|
||||
return tefINTERNAL;
|
||||
@@ -1032,7 +1044,9 @@ trustTransferLockedBalance(
|
||||
|
||||
if constexpr (!dryRun)
|
||||
{
|
||||
static_assert(std::is_same<V, ApplyView>::value);
|
||||
static_assert(
|
||||
std::is_same<V, ApplyView>::value ||
|
||||
std::is_same<V, Sandbox>::value);
|
||||
|
||||
// if source account is not issuer
|
||||
if (!srcIssuer)
|
||||
|
||||
@@ -297,6 +297,10 @@ import_vlseq(PublicKey const& key) noexcept;
|
||||
Keylet
|
||||
uritoken(AccountID const& issuer, Blob const& uri);
|
||||
|
||||
/** The initial directory page for a specific quality */
|
||||
Keylet
|
||||
uritoken_offer(Keylet const& k, std::uint64_t q, AccountID const& issuer, Currency const& currency) noexcept;
|
||||
|
||||
} // namespace keylet
|
||||
|
||||
// Everything below is deprecated and should be removed in favor of keylets:
|
||||
|
||||
@@ -179,6 +179,13 @@ enum LedgerEntryType : std::uint16_t
|
||||
*/
|
||||
ltUNL_REPORT = 0x0052,
|
||||
|
||||
/** A ledger object that reports on the active dUNL validators
|
||||
* that were validating for more than 240 of the last 256 ledgers
|
||||
*
|
||||
* \sa keylet::uritoken_offer
|
||||
*/
|
||||
ltURI_TOKEN_OFFER = 0x0056,
|
||||
|
||||
//---------------------------------------------------------------------------
|
||||
/** A special type, matching any ledger entry type.
|
||||
|
||||
|
||||
@@ -370,6 +370,7 @@ extern SF_UINT32 const sfTransferRate;
|
||||
extern SF_UINT32 const sfWalletSize;
|
||||
extern SF_UINT32 const sfOwnerCount;
|
||||
extern SF_UINT32 const sfDestinationTag;
|
||||
extern SF_UINT32 const sfRoyaltyRate;
|
||||
|
||||
// 32-bit integers (uncommon)
|
||||
extern SF_UINT32 const sfHighQualityIn;
|
||||
|
||||
@@ -182,7 +182,11 @@ constexpr std::uint32_t const tfNFTokenCancelOfferMask = ~(tfUniversal);
|
||||
constexpr std::uint32_t const tfNFTokenAcceptOfferMask = ~tfUniversal;
|
||||
|
||||
// URIToken mask
|
||||
constexpr std::uint32_t const tfURITokenMintMask = ~(tfUniversal | tfBurnable);
|
||||
enum URITokenMintFlags : uint32_t {
|
||||
// tfBurnable = 0x00000001,
|
||||
tfRoyalty = 0x00000002,
|
||||
};
|
||||
constexpr std::uint32_t const tfURITokenMintMask = ~(tfUniversal | tfBurnable | tfRoyalty);
|
||||
constexpr std::uint32_t const tfURITokenNonMintMask = ~tfUniversal;
|
||||
|
||||
// ClaimReward flags:
|
||||
|
||||
@@ -72,6 +72,7 @@ enum class LedgerNameSpace : std::uint16_t {
|
||||
URI_TOKEN = 'U',
|
||||
IMPORT_VLSEQ = 'I',
|
||||
UNL_REPORT = 'R',
|
||||
URI_TOKEN_OFFER = 'z',
|
||||
|
||||
// No longer used or supported. Left here to reserve the space
|
||||
// to avoid accidental reuse.
|
||||
@@ -443,6 +444,24 @@ uritoken(AccountID const& issuer, Blob const& uri)
|
||||
LedgerNameSpace::URI_TOKEN, issuer, Slice{uri.data(), uri.size()})};
|
||||
}
|
||||
|
||||
Keylet
|
||||
uritoken_offer(Keylet const& k, std::uint64_t q, AccountID const& issuer, Currency const& currency) noexcept
|
||||
{
|
||||
assert(k.type == ltURI_TOKEN);
|
||||
|
||||
// Indexes are stored in big endian format: they print as hex as stored.
|
||||
// Most significant bytes are first and the least significant bytes
|
||||
// represent adjacent entries. We place the quality, in big endian format,
|
||||
// in the 8 right most bytes; this way, incrementing goes to the next entry
|
||||
// for indexes.
|
||||
uint256 x = k.key;
|
||||
|
||||
// FIXME This is ugly and we can and should do better...
|
||||
((std::uint64_t*)x.end())[-1] = boost::endian::native_to_big(q);
|
||||
|
||||
return {ltURI_TOKEN_OFFER, indexHash(LedgerNameSpace::URI_TOKEN_OFFER, x, issuer, currency)};
|
||||
}
|
||||
|
||||
} // namespace keylet
|
||||
|
||||
} // namespace ripple
|
||||
|
||||
@@ -359,6 +359,19 @@ LedgerFormats::LedgerFormats()
|
||||
{sfDigest, soeOPTIONAL},
|
||||
{sfAmount, soeOPTIONAL},
|
||||
{sfDestination, soeOPTIONAL},
|
||||
{sfRoyaltyRate, soeOPTIONAL},
|
||||
{sfPreviousTxnID, soeREQUIRED},
|
||||
{sfPreviousTxnLgrSeq, soeREQUIRED}
|
||||
},
|
||||
commonFields);
|
||||
|
||||
add(jss::URITokenOffer,
|
||||
ltURI_TOKEN_OFFER,
|
||||
{
|
||||
{sfOwner, soeREQUIRED},
|
||||
{sfOwnerNode, soeREQUIRED},
|
||||
{sfURITokenID, soeREQUIRED},
|
||||
{sfAmount, soeREQUIRED},
|
||||
{sfPreviousTxnID, soeREQUIRED},
|
||||
{sfPreviousTxnLgrSeq, soeREQUIRED}
|
||||
},
|
||||
|
||||
@@ -118,6 +118,7 @@ CONSTRUCT_TYPED_SFIELD(sfTransferRate, "TransferRate", UINT32,
|
||||
CONSTRUCT_TYPED_SFIELD(sfWalletSize, "WalletSize", UINT32, 12);
|
||||
CONSTRUCT_TYPED_SFIELD(sfOwnerCount, "OwnerCount", UINT32, 13);
|
||||
CONSTRUCT_TYPED_SFIELD(sfDestinationTag, "DestinationTag", UINT32, 14);
|
||||
CONSTRUCT_TYPED_SFIELD(sfRoyaltyRate, "RoyaltyRate", UINT32, 15);
|
||||
|
||||
// 32-bit integers (uncommon)
|
||||
CONSTRUCT_TYPED_SFIELD(sfHighQualityIn, "HighQualityIn", UINT32, 16);
|
||||
|
||||
@@ -419,6 +419,7 @@ TxFormats::TxFormats()
|
||||
{sfAmount, soeOPTIONAL},
|
||||
{sfDestination, soeOPTIONAL},
|
||||
{sfTicketSequence, soeOPTIONAL},
|
||||
{sfRoyaltyRate, soeOPTIONAL},
|
||||
},
|
||||
commonFields);
|
||||
|
||||
|
||||
@@ -147,6 +147,7 @@ JSS(TransactionType); // in: TransactionSign.
|
||||
JSS(TransferRate); // in: TransferRate.
|
||||
JSS(TrustSet); // transaction type.
|
||||
JSS(URIToken); // out: LedgerEntry
|
||||
JSS(URITokenOffer); // out: LedgerEntry
|
||||
JSS(URITokenMint); // tx type
|
||||
JSS(URITokenBurn); // tx type
|
||||
JSS(URITokenBuy); // tx type
|
||||
|
||||
@@ -483,10 +483,10 @@ struct URIToken_test : public beast::unit_test::suite
|
||||
// preclaim
|
||||
|
||||
// tecNO_PERMISSION - not for sale
|
||||
env(uritoken::buy(bob, hexid),
|
||||
uritoken::amt(USD(10)),
|
||||
ter(tecNO_PERMISSION));
|
||||
env.close();
|
||||
// env(uritoken::buy(bob, hexid),
|
||||
// uritoken::amt(USD(10)),
|
||||
// ter(tecNO_PERMISSION));
|
||||
// env.close();
|
||||
|
||||
// set sell
|
||||
env(uritoken::sell(alice, hexid),
|
||||
@@ -2544,31 +2544,138 @@ struct URIToken_test : public beast::unit_test::suite
|
||||
env(uritoken::mint(alice, uri), ter(temMALFORMED));
|
||||
}
|
||||
}
|
||||
|
||||
static STAmount
|
||||
lockedAmount(
|
||||
jtx::Env const& env,
|
||||
jtx::Account const& account,
|
||||
jtx::Account const& gw,
|
||||
jtx::IOU const& iou)
|
||||
{
|
||||
auto const sle = env.le(keylet::line(account, gw, iou.currency));
|
||||
if (sle->isFieldPresent(sfLockedBalance))
|
||||
return (*sle)[sfLockedBalance];
|
||||
return STAmount(iou, 0);
|
||||
}
|
||||
|
||||
void
|
||||
testRoyalty(FeatureBitset features)
|
||||
{
|
||||
testcase("royalty");
|
||||
using namespace jtx;
|
||||
using namespace std::literals::chrono_literals;
|
||||
|
||||
Env env{*this, features};
|
||||
|
||||
auto const alice = Account("alice");
|
||||
auto const bob = Account("bob");
|
||||
auto const minter = Account("minter");
|
||||
auto const gw = Account{"gateway"};
|
||||
auto const USD = gw["USD"];
|
||||
|
||||
// setup env
|
||||
env.fund(XRP(1000), alice, bob, minter, gw);
|
||||
env.trust(USD(100000), alice, bob, minter);
|
||||
env.close();
|
||||
env(pay(gw, alice, USD(1000)));
|
||||
env(pay(gw, bob, USD(1000)));
|
||||
env(pay(gw, minter, USD(1000)));
|
||||
env.close();
|
||||
|
||||
auto const feeDrops = env.current()->fees().base;
|
||||
std::string const uri(maxTokenURILength, '?');
|
||||
auto const tid = uritoken::tokenid(minter, uri);
|
||||
std::string const hexid{strHex(tid)};
|
||||
|
||||
// Buy Offer Before Sell Offer
|
||||
{
|
||||
// minter mints
|
||||
const auto delta = USD(10);
|
||||
env(uritoken::mint(minter, uri),
|
||||
uritoken::royalty(1.1),
|
||||
ter(tesSUCCESS));
|
||||
env.close();
|
||||
|
||||
BEAST_EXPECT(inOwnerDir(*env.current(), minter, tid));
|
||||
BEAST_EXPECT(!inOwnerDir(*env.current(), alice, tid));
|
||||
BEAST_EXPECT(!inOwnerDir(*env.current(), bob, tid));
|
||||
|
||||
// minter remits to alice
|
||||
env(remit::remit(minter, alice),
|
||||
remit::token_ids({strHex(tid)}),
|
||||
ter(tesSUCCESS));
|
||||
env.close();
|
||||
|
||||
BEAST_EXPECT(!inOwnerDir(*env.current(), minter, tid));
|
||||
BEAST_EXPECT(inOwnerDir(*env.current(), alice, tid));
|
||||
BEAST_EXPECT(!inOwnerDir(*env.current(), bob, tid));
|
||||
|
||||
auto preMinterUSD = env.balance(minter, USD.issue());
|
||||
auto preAliceUSD = env.balance(alice, USD.issue());
|
||||
auto preBobUSD = env.balance(bob, USD.issue());
|
||||
|
||||
// bob creates buy offer
|
||||
env(uritoken::buy(bob, hexid), uritoken::amt(delta));
|
||||
env.close();
|
||||
|
||||
BEAST_EXPECT(!inOwnerDir(*env.current(), minter, tid));
|
||||
BEAST_EXPECT(inOwnerDir(*env.current(), alice, tid));
|
||||
BEAST_EXPECT(!inOwnerDir(*env.current(), bob, tid));
|
||||
|
||||
BEAST_EXPECT(
|
||||
env.balance(minter, USD.issue()) == preMinterUSD);
|
||||
BEAST_EXPECT(
|
||||
env.balance(alice, USD.issue()) == preAliceUSD);
|
||||
BEAST_EXPECT(
|
||||
env.balance(bob, USD.issue()) == preBobUSD);
|
||||
BEAST_EXPECT(
|
||||
-lockedAmount(env, bob, gw, USD) == delta);
|
||||
|
||||
// alice sells to bobs buy offer
|
||||
env(uritoken::sell(alice, hexid), uritoken::amt(delta));
|
||||
env.close();
|
||||
|
||||
BEAST_EXPECT(!inOwnerDir(*env.current(), minter, tid));
|
||||
BEAST_EXPECT(!inOwnerDir(*env.current(), alice, tid));
|
||||
BEAST_EXPECT(inOwnerDir(*env.current(), bob, tid));
|
||||
|
||||
BEAST_EXPECT(
|
||||
env.balance(minter, USD.issue()) == preMinterUSD + USD(1));
|
||||
BEAST_EXPECT(
|
||||
env.balance(alice, USD.issue()) == preAliceUSD + (delta - USD(1)));
|
||||
BEAST_EXPECT(
|
||||
env.balance(bob, USD.issue()) == preBobUSD - delta);
|
||||
BEAST_EXPECT(
|
||||
lockedAmount(env, bob, gw, USD) == USD(0));
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
testWithFeats(FeatureBitset features)
|
||||
{
|
||||
testEnabled(features);
|
||||
testMintInvalid(features);
|
||||
testBurnInvalid(features);
|
||||
testSellInvalid(features);
|
||||
testBuyInvalid(features);
|
||||
testClearInvalid(features);
|
||||
testMintValid(features);
|
||||
testBurnValid(features);
|
||||
testBuyValid(features);
|
||||
testSellValid(features);
|
||||
testClearValid(features);
|
||||
testMetaAndOwnership(features);
|
||||
testAccountDelete(features);
|
||||
testTickets(features);
|
||||
testRippleState(features);
|
||||
testGateway(features);
|
||||
testRequireAuth(features);
|
||||
testFreeze(features);
|
||||
testTransferRate(features);
|
||||
testDisallowXRP(features);
|
||||
testLimitAmount(features);
|
||||
testURIUTF8(features);
|
||||
// testEnabled(features);
|
||||
// testMintInvalid(features);
|
||||
// testBurnInvalid(features);
|
||||
// testSellInvalid(features);
|
||||
// testBuyInvalid(features);
|
||||
// testClearInvalid(features);
|
||||
// testMintValid(features);
|
||||
// testBurnValid(features);
|
||||
// testBuyValid(features);
|
||||
// testSellValid(features);
|
||||
// testClearValid(features);
|
||||
// testMetaAndOwnership(features);
|
||||
// testAccountDelete(features);
|
||||
// testTickets(features);
|
||||
// testRippleState(features);
|
||||
// testGateway(features);
|
||||
// testRequireAuth(features);
|
||||
// testFreeze(features);
|
||||
// testTransferRate(features);
|
||||
// testDisallowXRP(features);
|
||||
// testLimitAmount(features);
|
||||
// testURIUTF8(features);
|
||||
testRoyalty(features);
|
||||
}
|
||||
|
||||
public:
|
||||
@@ -2578,7 +2685,7 @@ public:
|
||||
using namespace test::jtx;
|
||||
auto const sa = supported_amendments();
|
||||
testWithFeats(sa);
|
||||
testWithFeats(sa - fixXahauV1);
|
||||
// testWithFeats(sa - fixXahauV1);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -43,6 +43,12 @@ mint(jtx::Account const& account, std::string const& uri)
|
||||
return jv;
|
||||
}
|
||||
|
||||
void
|
||||
royalty::operator()(Env& env, JTx& jt) const
|
||||
{
|
||||
jt.jv[sfRoyaltyRate.jsonName] = std::uint32_t(1000000000 * royalty_);;
|
||||
}
|
||||
|
||||
void
|
||||
dest::operator()(Env& env, JTx& jt) const
|
||||
{
|
||||
|
||||
@@ -36,6 +36,21 @@ tokenid(jtx::Account const& account, std::string const& uri);
|
||||
Json::Value
|
||||
mint(jtx::Account const& account, std::string const& uri);
|
||||
|
||||
/** Sets the optional RoyaltyRate on a JTx. */
|
||||
class royalty
|
||||
{
|
||||
private:
|
||||
double royalty_;
|
||||
|
||||
public:
|
||||
explicit royalty(double const& royalty) : royalty_(royalty)
|
||||
{
|
||||
}
|
||||
|
||||
void
|
||||
operator()(Env&, JTx& jtx) const;
|
||||
};
|
||||
|
||||
/** Sets the optional Destination on a JTx. */
|
||||
class dest
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user