Compare commits

...

5 Commits

Author SHA1 Message Date
Denis Angell
80b8207b0d lock tokens 2025-04-29 13:25:19 +02:00
Denis Angell
0d0adb4236 fix rate 2025-04-16 13:38:48 +02:00
Denis Angell
1de0493145 add royalty 2025-04-16 12:37:37 +02:00
Denis Angell
164b3a1c3d Add URIToken Offers 2025-04-15 14:25:42 +02:00
Denis Angell
9b7103ea4d rename workflow 2025-04-10 11:33:08 +02:00
19 changed files with 1047 additions and 166 deletions

View File

@@ -492,6 +492,7 @@ LedgerEntryTypesMatch::visitEntry(
case ltURI_TOKEN: case ltURI_TOKEN:
case ltIMPORT_VLSEQ: case ltIMPORT_VLSEQ:
case ltUNL_REPORT: case ltUNL_REPORT:
case ltURI_TOKEN_OFFER:
break; break;
default: default:
invalidTypeAdded_ = true; invalidTypeAdded_ = true;

View File

@@ -20,6 +20,7 @@
#include <ripple/app/ledger/Ledger.h> #include <ripple/app/ledger/Ledger.h>
#include <ripple/app/tx/impl/URIToken.h> #include <ripple/app/tx/impl/URIToken.h>
#include <ripple/basics/Log.h> #include <ripple/basics/Log.h>
#include <ripple/ledger/View.h>
#include <ripple/protocol/Feature.h> #include <ripple/protocol/Feature.h>
#include <ripple/protocol/Indexes.h> #include <ripple/protocol/Indexes.h>
#include <ripple/protocol/Quality.h> #include <ripple/protocol/Quality.h>
@@ -29,6 +30,319 @@
namespace ripple { 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 NotTEC
URIToken::preflight(PreflightContext const& ctx) URIToken::preflight(PreflightContext const& ctx)
{ {
@@ -116,6 +430,32 @@ URIToken::preflight(PreflightContext const& ctx)
case ttURITOKEN_MINT: { case ttURITOKEN_MINT: {
if (flags & tfURITokenMintMask) if (flags & tfURITokenMintMask)
return temINVALID_FLAG; 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; break;
} }
@@ -125,7 +465,6 @@ URIToken::preflight(PreflightContext const& ctx)
case ttURITOKEN_CREATE_SELL_OFFER: { case ttURITOKEN_CREATE_SELL_OFFER: {
if (flags & tfURITokenNonMintMask) if (flags & tfURITokenNonMintMask)
return temINVALID_FLAG; return temINVALID_FLAG;
break; break;
} }
@@ -220,7 +559,11 @@ URIToken::preclaim(PreclaimContext const& ctx)
// check if the seller has listed it at all // check if the seller has listed it at all
if (!saleAmount) 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 // check if the seller has listed it for sale to a specific account
if (dest && *dest != acc) if (dest && *dest != acc)
@@ -434,6 +777,11 @@ URIToken::doApply()
if (flags & tfBurnable) if (flags & tfBurnable)
sleU->setFlag(tfBurnable); sleU->setFlag(tfBurnable);
// Amendment Guard
if (ctx_.tx.isFieldPresent(sfRoyaltyRate))
sleU->setFieldU32(
sfRoyaltyRate, ctx_.tx.getFieldU32(sfRoyaltyRate));
auto const page = sb.dirInsert( auto const page = sb.dirInsert(
keylet::ownerDir(account_), *kl, describeOwnerDir(account_)); keylet::ownerDir(account_), *kl, describeOwnerDir(account_));
@@ -472,9 +820,71 @@ URIToken::doApply()
case ttURITOKEN_BUY: { case ttURITOKEN_BUY: {
STAmount const purchaseAmount = ctx_.tx.getFieldAmount(sfAmount); STAmount const purchaseAmount = ctx_.tx.getFieldAmount(sfAmount);
// OLD Amendment Guard
// check if the seller has listed it at all // check if the seller has listed it at all
if (!saleAmount) // if (!saleAmount)
return tecNO_PERMISSION; // 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 // check if the seller has listed it for sale to a specific account
if (dest && *dest != account_) if (dest && *dest != account_)
@@ -486,129 +896,23 @@ URIToken::doApply()
if (fixV1) if (fixV1)
{ {
// this is the reworked version of the buy routine // this is the reworked version of the buy routine
STAmount const fee = ctx_.tx.getFieldAmount(sfFee).xrp();
if (purchaseAmount < saleAmount) if (auto const ter = fullSwap(
return tecINSUFFICIENT_PAYMENT; sb,
mPriorBalance,
// if it's an xrp sale/purchase then no trustline needed purchaseAmount,
if (purchaseAmount.native()) *saleAmount,
{ fee,
STAmount needed{sb.fees().accountReserve( account_, // receiver
sle->getFieldU32(sfOwnerCount) + 1)}; *owner, // sender
*issuer, // issuer
STAmount const fee = ctx_.tx.getFieldAmount(sfFee).xrp(); *kl,
sleU,
if (needed + fee < needed) sle, // receiver sle
return tecINTERNAL; sleOwner, // sender sle
j_);
needed += fee; !isTesSuccess(ter))
return ter;
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_),
*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);
sb.apply(ctx_.rawView()); sb.apply(ctx_.rawView());
return tesSUCCESS; return tesSUCCESS;
} }
@@ -750,10 +1054,9 @@ URIToken::doApply()
true); true);
} }
initSellerBal = !sleDstLine initSellerBal = !sleDstLine ? purchaseAmount.zeroed()
? purchaseAmount.zeroed() : sellerLow ? ((*sleDstLine)[sfBalance])
: sellerLow ? ((*sleDstLine)[sfBalance]) : -((*sleDstLine)[sfBalance]);
: -((*sleDstLine)[sfBalance]);
finSellerBal = *initSellerBal + *dstAmt; finSellerBal = *initSellerBal + *dstAmt;
} }
@@ -1016,6 +1319,54 @@ URIToken::doApply()
if (account_ != *owner) if (account_ != *owner)
return tecNO_PERMISSION; 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]; auto const txDest = ctx_.tx[~sfDestination];
// update destination where applicable // update destination where applicable
@@ -1024,7 +1375,7 @@ URIToken::doApply()
else if (dest) else if (dest)
sleU->makeFieldAbsent(sfDestination); sleU->makeFieldAbsent(sfDestination);
sleU->setFieldAmount(sfAmount, ctx_.tx[sfAmount]); sleU->setFieldAmount(sfAmount, amount);
sb.update(sleU); sb.update(sleU);
sb.apply(ctx_.rawView()); sb.apply(ctx_.rawView());

View File

@@ -22,6 +22,7 @@
#include <ripple/app/ledger/Ledger.h> #include <ripple/app/ledger/Ledger.h>
#include <ripple/app/tx/impl/Transactor.h> #include <ripple/app/tx/impl/Transactor.h>
// #include <ripple/app/tx/impl/URITokenUtils.h>
#include <ripple/basics/Log.h> #include <ripple/basics/Log.h>
#include <ripple/protocol/Indexes.h> #include <ripple/protocol/Indexes.h>

View 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

View 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

View File

@@ -443,7 +443,8 @@ struct RunType
{ {
// see: // see:
// http://alumni.media.mit.edu/~rahimi/compile-time-flags/ // 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); static_assert(std::is_same<bool, T>::value);
return V; return V;
@@ -493,6 +494,8 @@ trustAdjustLockedBalance(
(std::is_same<V, ReadView const>::value && (std::is_same<V, ReadView const>::value &&
std::is_same<S, std::shared_ptr<SLE const>>::value) || std::is_same<S, std::shared_ptr<SLE const>>::value) ||
(std::is_same<V, ApplyView>::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)); std::is_same<S, std::shared_ptr<SLE>>::value));
// dry runs are explicit in code, but really the view type determines // dry runs are explicit in code, but really the view type determines
@@ -589,8 +592,10 @@ trustAdjustLockedBalance(
return tesSUCCESS; return tesSUCCESS;
if constexpr ( if constexpr (
std::is_same<V, ApplyView>::value && (std::is_same<V, ApplyView>::value &&
std::is_same<S, std::shared_ptr<SLE>>::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) if (finalLockedBalance == beast::zero || finalLockCount == 0)
{ {
@@ -785,18 +790,25 @@ trustTransferLockedBalance(
R dryRun) R dryRun)
{ {
typedef typename std::conditional< 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>,
std::shared_ptr<SLE const>>::type SLEPtr; std::shared_ptr<SLE const>>::type SLEPtr;
auto peek = [&](Keylet& k) { auto peek = [&](Keylet& k) {
if constexpr (std::is_same<V, ApplyView>::value && !dryRun) if constexpr (std::is_same<V, ApplyView>::value && !dryRun)
return const_cast<ApplyView&>(view).peek(k); 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 else
return view.read(k); 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)) if (!view.rules().enabled(featurePaychanAndEscrowForTokens))
return tefINTERNAL; return tefINTERNAL;
@@ -1032,7 +1044,9 @@ trustTransferLockedBalance(
if constexpr (!dryRun) 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 source account is not issuer
if (!srcIssuer) if (!srcIssuer)

View File

@@ -297,6 +297,10 @@ import_vlseq(PublicKey const& key) noexcept;
Keylet Keylet
uritoken(AccountID const& issuer, Blob const& uri); 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 } // namespace keylet
// Everything below is deprecated and should be removed in favor of keylets: // Everything below is deprecated and should be removed in favor of keylets:

View File

@@ -179,6 +179,13 @@ enum LedgerEntryType : std::uint16_t
*/ */
ltUNL_REPORT = 0x0052, 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. /** A special type, matching any ledger entry type.

View File

@@ -370,6 +370,7 @@ extern SF_UINT32 const sfTransferRate;
extern SF_UINT32 const sfWalletSize; extern SF_UINT32 const sfWalletSize;
extern SF_UINT32 const sfOwnerCount; extern SF_UINT32 const sfOwnerCount;
extern SF_UINT32 const sfDestinationTag; extern SF_UINT32 const sfDestinationTag;
extern SF_UINT32 const sfRoyaltyRate;
// 32-bit integers (uncommon) // 32-bit integers (uncommon)
extern SF_UINT32 const sfHighQualityIn; extern SF_UINT32 const sfHighQualityIn;

View File

@@ -182,7 +182,11 @@ constexpr std::uint32_t const tfNFTokenCancelOfferMask = ~(tfUniversal);
constexpr std::uint32_t const tfNFTokenAcceptOfferMask = ~tfUniversal; constexpr std::uint32_t const tfNFTokenAcceptOfferMask = ~tfUniversal;
// URIToken mask // 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; constexpr std::uint32_t const tfURITokenNonMintMask = ~tfUniversal;
// ClaimReward flags: // ClaimReward flags:

View File

@@ -72,6 +72,7 @@ enum class LedgerNameSpace : std::uint16_t {
URI_TOKEN = 'U', URI_TOKEN = 'U',
IMPORT_VLSEQ = 'I', IMPORT_VLSEQ = 'I',
UNL_REPORT = 'R', UNL_REPORT = 'R',
URI_TOKEN_OFFER = 'z',
// No longer used or supported. Left here to reserve the space // No longer used or supported. Left here to reserve the space
// to avoid accidental reuse. // to avoid accidental reuse.
@@ -443,6 +444,24 @@ uritoken(AccountID const& issuer, Blob const& uri)
LedgerNameSpace::URI_TOKEN, issuer, Slice{uri.data(), uri.size()})}; 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 keylet
} // namespace ripple } // namespace ripple

View File

@@ -359,6 +359,19 @@ LedgerFormats::LedgerFormats()
{sfDigest, soeOPTIONAL}, {sfDigest, soeOPTIONAL},
{sfAmount, soeOPTIONAL}, {sfAmount, soeOPTIONAL},
{sfDestination, 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}, {sfPreviousTxnID, soeREQUIRED},
{sfPreviousTxnLgrSeq, soeREQUIRED} {sfPreviousTxnLgrSeq, soeREQUIRED}
}, },

View File

@@ -118,6 +118,7 @@ CONSTRUCT_TYPED_SFIELD(sfTransferRate, "TransferRate", UINT32,
CONSTRUCT_TYPED_SFIELD(sfWalletSize, "WalletSize", UINT32, 12); CONSTRUCT_TYPED_SFIELD(sfWalletSize, "WalletSize", UINT32, 12);
CONSTRUCT_TYPED_SFIELD(sfOwnerCount, "OwnerCount", UINT32, 13); CONSTRUCT_TYPED_SFIELD(sfOwnerCount, "OwnerCount", UINT32, 13);
CONSTRUCT_TYPED_SFIELD(sfDestinationTag, "DestinationTag", UINT32, 14); CONSTRUCT_TYPED_SFIELD(sfDestinationTag, "DestinationTag", UINT32, 14);
CONSTRUCT_TYPED_SFIELD(sfRoyaltyRate, "RoyaltyRate", UINT32, 15);
// 32-bit integers (uncommon) // 32-bit integers (uncommon)
CONSTRUCT_TYPED_SFIELD(sfHighQualityIn, "HighQualityIn", UINT32, 16); CONSTRUCT_TYPED_SFIELD(sfHighQualityIn, "HighQualityIn", UINT32, 16);

View File

@@ -419,6 +419,7 @@ TxFormats::TxFormats()
{sfAmount, soeOPTIONAL}, {sfAmount, soeOPTIONAL},
{sfDestination, soeOPTIONAL}, {sfDestination, soeOPTIONAL},
{sfTicketSequence, soeOPTIONAL}, {sfTicketSequence, soeOPTIONAL},
{sfRoyaltyRate, soeOPTIONAL},
}, },
commonFields); commonFields);

View File

@@ -147,6 +147,7 @@ JSS(TransactionType); // in: TransactionSign.
JSS(TransferRate); // in: TransferRate. JSS(TransferRate); // in: TransferRate.
JSS(TrustSet); // transaction type. JSS(TrustSet); // transaction type.
JSS(URIToken); // out: LedgerEntry JSS(URIToken); // out: LedgerEntry
JSS(URITokenOffer); // out: LedgerEntry
JSS(URITokenMint); // tx type JSS(URITokenMint); // tx type
JSS(URITokenBurn); // tx type JSS(URITokenBurn); // tx type
JSS(URITokenBuy); // tx type JSS(URITokenBuy); // tx type

View File

@@ -483,10 +483,10 @@ struct URIToken_test : public beast::unit_test::suite
// preclaim // preclaim
// tecNO_PERMISSION - not for sale // tecNO_PERMISSION - not for sale
env(uritoken::buy(bob, hexid), // env(uritoken::buy(bob, hexid),
uritoken::amt(USD(10)), // uritoken::amt(USD(10)),
ter(tecNO_PERMISSION)); // ter(tecNO_PERMISSION));
env.close(); // env.close();
// set sell // set sell
env(uritoken::sell(alice, hexid), env(uritoken::sell(alice, hexid),
@@ -2544,31 +2544,138 @@ struct URIToken_test : public beast::unit_test::suite
env(uritoken::mint(alice, uri), ter(temMALFORMED)); 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 void
testWithFeats(FeatureBitset features) testWithFeats(FeatureBitset features)
{ {
testEnabled(features); // testEnabled(features);
testMintInvalid(features); // testMintInvalid(features);
testBurnInvalid(features); // testBurnInvalid(features);
testSellInvalid(features); // testSellInvalid(features);
testBuyInvalid(features); // testBuyInvalid(features);
testClearInvalid(features); // testClearInvalid(features);
testMintValid(features); // testMintValid(features);
testBurnValid(features); // testBurnValid(features);
testBuyValid(features); // testBuyValid(features);
testSellValid(features); // testSellValid(features);
testClearValid(features); // testClearValid(features);
testMetaAndOwnership(features); // testMetaAndOwnership(features);
testAccountDelete(features); // testAccountDelete(features);
testTickets(features); // testTickets(features);
testRippleState(features); // testRippleState(features);
testGateway(features); // testGateway(features);
testRequireAuth(features); // testRequireAuth(features);
testFreeze(features); // testFreeze(features);
testTransferRate(features); // testTransferRate(features);
testDisallowXRP(features); // testDisallowXRP(features);
testLimitAmount(features); // testLimitAmount(features);
testURIUTF8(features); // testURIUTF8(features);
testRoyalty(features);
} }
public: public:
@@ -2578,7 +2685,7 @@ public:
using namespace test::jtx; using namespace test::jtx;
auto const sa = supported_amendments(); auto const sa = supported_amendments();
testWithFeats(sa); testWithFeats(sa);
testWithFeats(sa - fixXahauV1); // testWithFeats(sa - fixXahauV1);
} }
}; };

View File

@@ -43,6 +43,12 @@ mint(jtx::Account const& account, std::string const& uri)
return jv; return jv;
} }
void
royalty::operator()(Env& env, JTx& jt) const
{
jt.jv[sfRoyaltyRate.jsonName] = std::uint32_t(1000000000 * royalty_);;
}
void void
dest::operator()(Env& env, JTx& jt) const dest::operator()(Env& env, JTx& jt) const
{ {

View File

@@ -36,6 +36,21 @@ tokenid(jtx::Account const& account, std::string const& uri);
Json::Value Json::Value
mint(jtx::Account const& account, std::string const& uri); 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. */ /** Sets the optional Destination on a JTx. */
class dest class dest
{ {