first version of URIToken amendment

This commit is contained in:
Richard Holland
2023-01-26 13:46:19 +00:00
parent 49207d007d
commit fbafb72262
16 changed files with 868 additions and 3 deletions

View File

@@ -447,6 +447,7 @@ target_sources (rippled PRIVATE
src/ripple/app/tx/impl/SignerEntries.cpp
src/ripple/app/tx/impl/Taker.cpp
src/ripple/app/tx/impl/Transactor.cpp
src/ripple/app/tx/impl/URIToken.cpp
src/ripple/app/tx/impl/apply.cpp
src/ripple/app/tx/impl/applySteps.cpp
src/ripple/app/hook/impl/applyHook.cpp

View File

@@ -392,6 +392,7 @@ LedgerEntryTypesMatch::visitEntry(
case ltEMITTED_TXN:
case ltNFTOKEN_PAGE:
case ltNFTOKEN_OFFER:
case ltURI_TOKEN:
break;
default:
invalidTypeAdded_ = true;

View File

@@ -0,0 +1,742 @@
//------------------------------------------------------------------------------
/*
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/ledger/Ledger.h>
#include <ripple/app/tx/impl/URIToken.h>
#include <ripple/basics/Log.h>
#include <ripple/protocol/Feature.h>
#include <ripple/protocol/Indexes.h>
#include <ripple/protocol/STAccount.h>
#include <ripple/protocol/TER.h>
#include <ripple/protocol/TxFlags.h>
namespace ripple {
/*
* Mint: URI populated only
* Burn: URITokenID populated, Blank but present URI
* Buy: URITokenID, Amount
* Sell: URITokenID, Amount, [Destination], flags=tfSell,
* Clear: URITokenID only (clear current sell offer)
*/
inline URIOperation inferOperation(STTx const& tx)
{
uint32_t const flags = tx.getFlags();
bool const hasURI = tx.isFieldPresent(sfURI);
bool const blankURI = hasURI && tx.getFieldVL(sfURI).empty();
bool const hasID = tx.isFieldPresent(sfURITokenID);
bool const hasAmt = tx.isFieldPresent(sfAmount);
bool const hasDst = tx.isFieldPresent(sfDestination);
bool const hasSellFlag = flags == tfSell;
bool const hasBurnFlag = flags == tfBurnable;
bool const blankFlags = flags == 0;
uint8_t combination =
(hasURI ? 0b10000000U : 0) +
(blankURI ? 0b01000000U : 0) +
(hasID ? 0b00100000U : 0) +
(hasAmt ? 0b00010000U : 0) +
(hasDst ? 0b00001000U : 0) +
(hasSellFlag ? 0b00000100U : 0) +
(hasBurnFlag ? 0b00000010U : 0) +
(blankFlags ? 0b00000001U : 0);
switch (combination)
{
case 0b10000001U:
case 0b10000010U:
return URIOperation::Mint;
case 0b11100001U:
return URIOperation::Burn;
case 0b00110001U:
return URIOperation::Buy;
case 0b00110100U:
case 0b00111100U:
return URIOperation::Sell;
case 0b00100001U:
return URIOperation::Clear;
default:
return URIOperation::Invalid;
}
}
NotTEC
URIToken::preflight(PreflightContext const& ctx)
{
if (!ctx.rules.enabled(featureURIToken))
return temDISABLED;
NotTEC const ret{preflight1(ctx)};
if (!isTesSuccess(ret))
return ret;
auto const op = inferOperation(ctx.tx);
if (op == URIOperation::Invalid)
{
JLOG(ctx.j.warn())
<< "Malformed transaction. Check flags/fields, "
<< "documentation for how to specify Mint/Burn/Buy/Sell operations.";
return temMALFORMED;
}
JLOG(ctx.j.trace())
<< "URIToken txnid=" << ctx.tx.getTransactionID() << " inferred operation="
<< (op == URIOperation::Invalid ? "Invalid" :
(op == URIOperation::Mint ? "Mint" :
(op == URIOperation::Burn ? "Burn" :
(op == URIOperation::Buy ? "Buy" :
(op == URIOperation::Sell ? "Sell" :
(op == URIOperation::Clear ? "Clear" : "Unknown")))))) << "\n";
if (op == URIOperation::Mint && ctx.tx.getFieldVL(sfURI).size() > 256)
{
JLOG(ctx.j.warn())
<< "Malformed transaction. URI may not exceed 256 bytes.";
return temMALFORMED;
}
if (ctx.tx.isFieldPresent(sfAmount))
{
STAmount const amt = ctx.tx.getFieldAmount(sfAmount);
if (!isLegalNet(amt) || amt.signum() <= 0)
{
JLOG(ctx.j.warn())
<< "Malformed transaction. Negative or invalid amount specified.";
return temBAD_AMOUNT;
}
if (badCurrency() == amt.getCurrency())
{
JLOG(ctx.j.warn())
<< "Malformed transaction. Bad currency.";
return temBAD_CURRENCY;
}
}
return preflight2(ctx);
}
TER
URIToken::preclaim(PreclaimContext const& ctx)
{
AccountID const acc = ctx.tx.getAccountID(sfAccount);
URIOperation op = inferOperation(ctx.tx);
std::shared_ptr<SLE const> sleU;
if (ctx.tx.isFieldPresent(sfURITokenID))
sleU = ctx.view.read(Keylet {ltURI_TOKEN, ctx.tx.getFieldH256(sfURITokenID)});
uint32_t leFlags = sleU ? sleU->getFieldU32(sfFlags) : 0;
std::optional<AccountID> issuer;
std::optional<AccountID> owner;
std::optional<STAmount> saleAmount;
std::optional<AccountID> dest;
std::shared_ptr<SLE const> sleOwner;
if (sleU)
{
if (sleU->getFieldU16(sfLedgerEntryType) != ltURI_TOKEN)
return tecNO_ENTRY;
owner = sleU->getAccountID(sfOwner);
issuer = sleU->getAccountID(sfIssuer);
if (sleU->isFieldPresent(sfAmount))
saleAmount = sleU->getFieldAmount(sfAmount);
if (sleU->isFieldPresent(sfDestination))
dest = sleU->getAccountID(sfDestination);
sleOwner = ctx.view.read(keylet::account(*owner));
if (!sleOwner)
{
JLOG(ctx.j.warn())
<< "Malformed transaction: owner of URIToken is not in the ledger.";
return tecNO_ENTRY;
}
}
else if (op != URIOperation::Mint)
return tecNO_ENTRY;
switch (op)
{
case URIOperation::Mint:
{
// check if this token has already been minted.
if (ctx.view.exists(keylet::uritoken(acc, ctx.tx.getFieldVL(sfURI))))
return tecDUPLICATE;
return tesSUCCESS;
}
case URIOperation::Burn:
{
if (leFlags == tfBurnable && acc == *issuer)
{
// pass, the issuer can burn the URIToken if they minted it with a burn flag
}
else
if (acc == *owner)
{
// pass, the owner can always destroy their own URI token
}
else
return tecNO_PERMISSION;
return tesSUCCESS;
}
case URIOperation::Buy:
{
// if the owner is the account then the buy operation is a clear operation
// and we won't bother to check anything else
if (acc == *owner)
return tesSUCCESS;
// check if the seller has listed it at all
if (!saleAmount)
return tecNO_PERMISSION;
// check if the seller has listed it for sale to a specific account
if (dest && *dest != acc)
return tecNO_PERMISSION;
// check if the buyer is paying enough
STAmount const purchaseAmount = ctx.tx[sfAmount];
if (purchaseAmount.issue() != saleAmount->issue())
return tecNFTOKEN_BUY_SELL_MISMATCH;
if (purchaseAmount < saleAmount)
return tecINSUFFICIENT_PAYMENT;
if (purchaseAmount.native() && saleAmount->native())
{
// if it's an xrp sale/purchase then no trustline needed
if (purchaseAmount > (sleOwner->getFieldAmount(sfBalance) - ctx.tx[sfFee]))
return tecINSUFFICIENT_FUNDS;
}
// execution to here means it's an IOU sale
// check if the buyer has the right trustline with an adequate balance
STAmount availableFunds{accountFunds(
ctx.view,
acc,
purchaseAmount,
fhZERO_IF_FROZEN,
ctx.j)};
if (purchaseAmount > availableFunds)
return tecINSUFFICIENT_FUNDS;
return tesSUCCESS;
}
case URIOperation::Clear:
{
if (acc != *owner)
return tecNO_PERMISSION;
return tesSUCCESS;
}
case URIOperation::Sell:
{
if (acc != *owner)
return tecNO_PERMISSION;
if (!isXRP(*saleAmount))
{
AccountID const iouIssuer = saleAmount->getIssuer();
if (!ctx.view.exists(keylet::account(iouIssuer)))
return tecNO_ISSUER;
}
return tesSUCCESS;
}
default:
{
JLOG(ctx.j.warn())
<< "URIToken txid=" << ctx.tx.getTransactionID() << " preclaim with URIOperation::Invalid\n";
return tecINTERNAL;
}
}
}
TER
URIToken::doApply()
{
auto j = ctx_.app.journal("View");
URIOperation op = inferOperation(ctx_.tx);
auto const sle = view().peek(keylet::account(account_));
if (!sle)
return tefINTERNAL;
if (op == URIOperation::Mint || op == URIOperation::Buy)
{
STAmount const reserve{view().fees().accountReserve(sle->getFieldU32(sfOwnerCount) + 1)};
if (mPriorBalance - ctx_.tx.getFieldAmount(sfFee).xrp() < reserve)
return tecINSUFFICIENT_RESERVE;
}
std::shared_ptr<SLE> sleU;
std::optional<AccountID> issuer;
std::optional<AccountID> owner;
std::optional<STAmount> saleAmount;
std::optional<AccountID> dest;
std::optional<Keylet> kl;
std::shared_ptr<SLE> sleOwner;
if (op != URIOperation::Mint)
{
kl = Keylet {ltURI_TOKEN, ctx_.tx.getFieldH256(sfURITokenID)};
sleU = view().peek(*kl);
if (!sleU)
return tecNO_ENTRY;
if (sleU->getFieldU16(sfLedgerEntryType) != ltURI_TOKEN)
return tecNO_ENTRY;
owner = (*sleU)[sfOwner];
issuer = (*sleU)[sfIssuer];
saleAmount = (*sleU)[~sfAmount];
dest = (*sleU)[~sfDestination];
if (*owner == account_)
sleOwner = sle;
else
sleOwner = view().peek(keylet::account(*owner));
if (!sleOwner)
{
JLOG(j.warn())
<< "Malformed transaction: owner of URIToken is not in the ledger.";
return tecNO_ENTRY;
}
}
switch (op)
{
case URIOperation::Mint:
{
kl = keylet::uritoken(account_, ctx_.tx.getFieldVL(sfURI));
if (view().exists(*kl))
return tecDUPLICATE;
sleU = std::make_shared<SLE>(*kl);
sleU->setAccountID(sfOwner, account_);
sleU->setAccountID(sfIssuer, account_);
sleU->setFieldVL(sfURI, ctx_.tx.getFieldVL(sfURI));
auto const page = view().dirInsert(
keylet::ownerDir(account_),
*kl,
describeOwnerDir(account_));
JLOG(j_.trace())
<< "Adding URIToken to owner directory "
<< to_string(kl->key) << ": "
<< (page ? "success" : "failure");
if (!page)
return tecDIR_FULL;
sleU->setFieldU64(sfOwnerNode, *page);
view().insert(sleU);
adjustOwnerCount(view(), sle, 1, j);
return tesSUCCESS;
}
case URIOperation::Clear:
{
sleU->makeFieldAbsent(sfAmount);
if (sleU->isFieldPresent(sfDestination))
sleU->makeFieldAbsent(sfDestination);
view().update(sleU);
return tesSUCCESS;
}
case URIOperation::Buy:
{
if (account_ == *owner)
{
// this is a clear operation
sleU->makeFieldAbsent(sfAmount);
if (sleU->isFieldPresent(sfDestination))
sleU->makeFieldAbsent(sfDestination);
view().update(sleU);
return tesSUCCESS;
}
STAmount const purchaseAmount = ctx_.tx.getFieldAmount(sfAmount);
bool const sellerLow = purchaseAmount.getIssuer() > *owner;
bool const buyerLow = purchaseAmount.getIssuer() > account_;
// check if the seller has listed it at all
if (!saleAmount)
return tecNO_PERMISSION;
// check if the seller has listed it for sale to a specific account
if (dest && *dest != account_)
return tecNO_PERMISSION;
if (purchaseAmount.issue() != saleAmount->issue())
return tecNFTOKEN_BUY_SELL_MISMATCH;
std::optional<STAmount> initBuyerBal;
std::optional<STAmount> initSellerBal;
std::optional<STAmount> finBuyerBal;
std::optional<STAmount> finSellerBal;
std::optional<STAmount> dstAmt;
std::optional<Keylet> tlSeller;
std::shared_ptr<SLE> sleDstLine;
std::shared_ptr<SLE> sleSrcLine;
// if it's an xrp sale/purchase then no trustline needed
if (purchaseAmount.native())
{
if (purchaseAmount < saleAmount)
return tecINSUFFICIENT_PAYMENT;
if (purchaseAmount > ((*sleOwner)[sfBalance] - ctx_.tx[sfFee]))
return tecINSUFFICIENT_FUNDS;
dstAmt = purchaseAmount;
initSellerBal = (*sleOwner)[sfBalance];
initBuyerBal = (*sle)[sfBalance];
finSellerBal = *initSellerBal + purchaseAmount;
finBuyerBal = *initBuyerBal - purchaseAmount;
}
else
{
// IOU sale
STAmount availableFunds{accountFunds(
view(),
account_,
purchaseAmount,
fhZERO_IF_FROZEN,
j)};
if (purchaseAmount > availableFunds)
return tecINSUFFICIENT_FUNDS;
// check if the seller has a line
tlSeller =
keylet::line(*owner, purchaseAmount.getIssuer(), purchaseAmount.getCurrency());
Keylet tlBuyer =
keylet::line(account_, purchaseAmount.getIssuer(), purchaseAmount.getCurrency());
sleDstLine = view().peek(*tlSeller);
sleSrcLine = view().peek(tlBuyer);
if (!sleDstLine)
{
// they do not, so we can create one if they have sufficient reserve
if (std::uint32_t const ownerCount = {sleOwner->at(sfOwnerCount)};
(*sleOwner)[sfBalance] < view().fees().accountReserve(ownerCount + 1))
{
JLOG(j_.trace()) << "Trust line does not exist. "
"Insufficent reserve to create line.";
return tecNO_LINE_INSUF_RESERVE;
}
}
// remove from buyer
initBuyerBal = buyerLow ? (*sleSrcLine)[sfBalance] : -(*sleSrcLine)[sfBalance];
finBuyerBal = *initBuyerBal - purchaseAmount;
// compute amount to deliver
static Rate const parityRate(QUALITY_ONE);
auto xferRate = transferRate(view(), saleAmount->getIssuer());
dstAmt =
xferRate == parityRate
? purchaseAmount
: multiplyRound(purchaseAmount, xferRate, purchaseAmount.issue(), true);
if (!sellerLow)
dstAmt->negate();
initSellerBal = !sleDstLine
? purchaseAmount.zeroed()
: (sellerLow ? (*sleDstLine)[sfBalance] : -(*sleDstLine)[sfBalance]);
finSellerBal = *initSellerBal + *dstAmt;
}
// sanity check balance mutations (xrp or iou, both are checked the same way now)
if (*finSellerBal < *initSellerBal)
{
JLOG(j.warn())
<< "URIToken txid=" << ctx_.tx.getTransactionID() << " "
<< "finSellerBal < initSellerBal";
return tecINTERNAL;
}
if (*finBuyerBal > *initBuyerBal)
{
JLOG(j.warn())
<< "URIToken txid=" << ctx_.tx.getTransactionID() << " "
<< "finBuyerBal > initBuyerBal";
return tecINTERNAL;
}
if (*finBuyerBal < beast::zero)
{
JLOG(j.warn())
<< "URIToken txid=" << ctx_.tx.getTransactionID() << " "
<< "finBuyerBal < 0";
return tecINTERNAL;
}
if (*finSellerBal < beast::zero)
{
JLOG(j.warn())
<< "URIToken txid=" << ctx_.tx.getTransactionID() << " "
<< "finSellerBal < 0";
return tecINTERNAL;
}
// to this point no ledger changes have been made
// make them in a sensible order such that failure doesn't require cleanup
// add to new owner's directory first, this can fail if they have too many objects
auto const newPage = view().dirInsert(
keylet::ownerDir(account_),
*kl,
describeOwnerDir(account_));
JLOG(j_.trace())
<< "Adding URIToken to owner directory "
<< to_string(kl->key) << ": "
<< (newPage ? "success" : "failure");
if (!newPage)
{
// nothing has happened at all and there is nothing to clean up
// we can just leave with DIR_FULL
return tecDIR_FULL;
}
// Next create destination trustline where applicable. This could fail for a variety of reasons.
// If it does fail we need to remove the dir entry we just added to the buyer before we leave.
bool lineCreated = false;
if (!isXRP(purchaseAmount) && !sleDstLine)
{
// clang-format off
if (TER const ter = trustCreate(
view(), // payment sandbox
sellerLow, // is dest low?
*issuer, // source
*owner, // destination
tlSeller->key, // ledger index
sleOwner, // Account to add to
false, // authorize account
(sleOwner->getFlags() & lsfDefaultRipple) == 0,
false, // freeze trust line
*dstAmt, // initial balance zero
Issue(purchaseAmount.getCurrency(), *owner), // limit of zero
0, // quality in
0, // quality out
j); // journal
!isTesSuccess(ter))
{
// remove the newly inserted directory entry before we leave
//
if (!view().dirRemove(keylet::ownerDir(account_), *newPage, kl->key, true))
{
JLOG(j.fatal())
<< "Could not remove URIToken from owner directory";
return tefBAD_LEDGER;
}
// leave
return ter;
}
// clang-format on
// add their trustline to their ownercount
lineCreated = true;
}
// execution to here means we added the URIToken to the buyer's directory
// and we definitely have a way to send the funds to the seller.
// remove from current owner directory
if (!view().dirRemove(keylet::ownerDir(*owner), sleU->getFieldU64(sfOwnerNode), kl->key, true))
{
JLOG(j.fatal())
<< "Could not remove URIToken from owner directory";
// remove the newly inserted directory entry before we leave
if (!view().dirRemove(keylet::ownerDir(account_), *newPage, kl->key, true))
{
JLOG(j.fatal())
<< "Could not remove URIToken from owner directory (2)";
}
// clean up any trustline we might have made
if (lineCreated)
{
auto line = view().peek(*tlSeller);
if (line)
view().erase(line);
}
return tefBAD_LEDGER;
}
// above is all the things that could fail. we now have swapped the ownership as far as the ownerdirs
// are concerned, and we have a place to pay to and from.
// if a trustline was created then the ownercount stays the same on the seller +1 TL -1 URIToken
if (!lineCreated)
adjustOwnerCount(view(), sleOwner, -1, j);
// the buyer gets a new object
adjustOwnerCount(view(), 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);
// update the buyer's balance
if (isXRP(purchaseAmount))
{
// the sale is for xrp, so set the balance
sle->setFieldAmount(sfBalance, *finBuyerBal);
}
else if (sleSrcLine)
{
// update the buyer's line to reflect the reduction of the purchase price
sleSrcLine->setFieldAmount(sfBalance, buyerLow ? *finBuyerBal : -(*finBuyerBal));
}
else
return tecINTERNAL;
// update the seller's balance
if (isXRP(purchaseAmount))
{
// the sale is for xrp, so set the balance
sleOwner->setFieldAmount(sfBalance, *finSellerBal);
}
else if (sleDstLine)
{
// the line already existed on the seller side so update it
sleDstLine->setFieldAmount(sfBalance, sellerLow ? *finSellerBal : -(*finSellerBal));
}
else if (lineCreated)
{
// pass, the TL already has this balance set on it at creation
}
else
return tecINTERNAL;
if (sleSrcLine)
view().update(sleSrcLine);
if (sleDstLine)
view().update(sleDstLine);
view().update(sleU);
view().update(sleOwner);
return tesSUCCESS;
}
case URIOperation::Burn:
{
if (sleU->getAccountID(sfOwner) == account_)
{
// pass, owner may always delete own object
}
else if (sleU->getAccountID(sfIssuer) == account_ && (sleU->getFlags() & tfBurnable))
{
// pass, issuer may burn if the tfBurnable flag was set during minting
}
else
return tecNO_PERMISSION;
// execution to here means there is permission to burn
auto const page = (*sleU)[sfOwnerNode];
if (!view().dirRemove(keylet::ownerDir(*owner), page, kl->key, true))
{
JLOG(j.fatal())
<< "Could not remove URIToken from owner directory";
return tefBAD_LEDGER;
}
view().erase(sleU);
adjustOwnerCount(view(), sle, -1, j);
return tesSUCCESS;
}
case URIOperation::Sell:
{
if (account_ != *owner)
return tecNO_PERMISSION;
auto const txDest = ctx_.tx[~sfDestination];
// update destination where applicable
if (txDest)
sleU->setAccountID(sfDestination, *txDest);
else if (dest)
sleU->makeFieldAbsent(sfDestination);
sleU->setFieldAmount(sfAmount, ctx_.tx[sfAmount]);
view().update(sleU);
std::cout << "sleU on sell: " << (*sleU) << "\n";
return tesSUCCESS;
}
default:
return tecINTERNAL;
}
}
} // namespace ripple

View File

@@ -0,0 +1,62 @@
//------------------------------------------------------------------------------
/*
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_URITOKEN_H_INCLUDED
#define RIPPLE_TX_URITOKEN_H_INCLUDED
#include <ripple/app/ledger/Ledger.h>
#include <ripple/app/tx/impl/Transactor.h>
#include <ripple/basics/Log.h>
#include <ripple/protocol/Indexes.h>
namespace ripple {
enum struct URIOperation : uint8_t
{
Invalid = 0,
Mint = 1,
Burn = 2,
Buy = 3,
Sell = 4,
Clear = 5
};
class URIToken : public Transactor
{
public:
static constexpr ConsequencesFactoryType ConsequencesFactory{Normal};
explicit URIToken(ApplyContext& ctx) : Transactor(ctx)
{
}
static NotTEC
preflight(PreflightContext const& ctx);
static TER
preclaim(PreclaimContext const& ctx);
TER
doApply() override;
};
} // namespace ripple
#endif

View File

@@ -156,6 +156,8 @@ invoke_preflight(PreflightContext const& ctx)
return invoke_preflight_helper<ClaimReward>(ctx);
case ttINVOKE:
return invoke_preflight_helper<Invoke>(ctx);
case ttURI_TOKEN:
return invoke_preflight_helper<URIToken>(ctx);
default:
assert(false);
return {temUNKNOWN, TxConsequences{temUNKNOWN}};
@@ -265,6 +267,8 @@ invoke_preclaim(PreclaimContext const& ctx)
return invoke_preclaim<ClaimReward>(ctx);
case ttINVOKE:
return invoke_preclaim<Invoke>(ctx);
case ttURI_TOKEN:
return invoke_preclaim<URIToken>(ctx);
default:
assert(false);
return temUNKNOWN;
@@ -335,6 +339,8 @@ invoke_calculateBaseFee(ReadView const& view, STTx const& tx)
return ClaimReward::calculateBaseFee(view, tx);
case ttINVOKE:
return Invoke::calculateBaseFee(view, tx);
case ttURI_TOKEN:
return URIToken::calculateBaseFee(view, tx);
default:
assert(false);
return FeeUnit64{0};
@@ -501,6 +507,10 @@ invoke_apply(ApplyContext& ctx)
Invoke p(ctx);
return p();
}
case ttURI_TOKEN: {
URIToken p(ctx);
return p();
}
default:
assert(false);
return {temUNKNOWN, false};

View File

@@ -74,7 +74,7 @@ namespace detail {
// Feature.cpp. Because it's only used to reserve storage, and determine how
// large to make the FeatureBitset, it MAY be larger. It MUST NOT be less than
// the actual number of amendments. A LogicError on startup will verify this.
static constexpr std::size_t numFeatures = 57;
static constexpr std::size_t numFeatures = 58;
/** Amendments that this server supports and the default voting behavior.
Whether they are enabled depends on the Rules defined in the validated
@@ -344,6 +344,7 @@ extern uint256 const featureDisallowIncoming;
extern uint256 const fixRemoveNFTokenAutoTrustLine;
extern uint256 const featureImmediateOfferKilled;
extern uint256 const featurePaychanAndEscrowForTokens;
extern uint256 const featureURIToken;
} // namespace ripple

View File

@@ -284,6 +284,9 @@ nft_buys(uint256 const& id) noexcept;
Keylet
nft_sells(uint256 const& id) noexcept;
Keylet
uritoken(AccountID const& issuer, Blob const& uri);
} // namespace keylet
// Everything below is deprecated and should be removed in favor of keylets:

View File

@@ -161,6 +161,12 @@ enum LedgerEntryType : std::uint16_t
*/
ltNFTOKEN_OFFER = 0x0037,
/** A unique ledger object which contains an up to 256 byte URI
\sa keylet::uritoken
*/
ltURI_TOKEN = 0x0055,
//---------------------------------------------------------------------------
/** A special type, matching any ledger entry type.

View File

@@ -472,7 +472,7 @@ extern SF_UINT256 const sfHookNamespace;
extern SF_UINT256 const sfHookSetTxnID;
extern SF_UINT256 const sfOfferID;
extern SF_UINT256 const sfEscrowID;
extern SF_UINT256 const sfURITokenID;
// currency amount (common)
extern SF_AMOUNT const sfAmount;

View File

@@ -139,6 +139,9 @@ enum TxType : std::uint16_t
/** This transaction accepts an existing offer to buy or sell an existing NFT. */
ttNFTOKEN_ACCEPT_OFFER = 29,
/** This transaction mints/burns/buys/sells a URI TOKEN */
ttURI_TOKEN = 45,
/** This transaction resets accumulator/counters and claims a reward for holding an average balance
* from a specified hook */
ttCLAIM_REWARD = 98,

View File

@@ -454,6 +454,7 @@ REGISTER_FIX (fixRemoveNFTokenAutoTrustLine, Supported::yes, DefaultVote::yes
REGISTER_FEATURE(ImmediateOfferKilled, Supported::yes, DefaultVote::no);
REGISTER_FEATURE(DisallowIncoming, Supported::yes, DefaultVote::no);
REGISTER_FEATURE(PaychanAndEscrowForTokens, Supported::yes, DefaultVote::yes);
REGISTER_FEATURE(URIToken, Supported::yes, DefaultVote::yes);
// The following amendments have been active for at least two years. Their
// pre-amendment code has been removed and the identifiers are deprecated.

View File

@@ -69,6 +69,7 @@ enum class LedgerNameSpace : std::uint16_t {
NFTOKEN_OFFER = 'q',
NFTOKEN_BUY_OFFERS = 'h',
NFTOKEN_SELL_OFFERS = 'i',
URI_TOKEN = 'U',
// No longer used or supported. Left here to reserve the space
// to avoid accidental reuse.
@@ -414,6 +415,13 @@ nft_sells(uint256 const& id) noexcept
return {ltDIR_NODE, indexHash(LedgerNameSpace::NFTOKEN_SELL_OFFERS, id)};
}
Keylet
uritoken(AccountID const& issuer, Blob const& uri)
{
return {
ltURI_TOKEN, indexHash(LedgerNameSpace::URI_TOKEN, issuer, uri)};
}
} // namespace keylet
} // namespace ripple

View File

@@ -314,6 +314,21 @@ LedgerFormats::LedgerFormats()
{sfPreviousTxnLgrSeq, soeREQUIRED}
},
commonFields);
add(jss::URIToken,
ltURI_TOKEN,
{
{sfOwner, soeREQUIRED},
{sfOwnerNode, soeREQUIRED},
{sfIssuer, soeREQUIRED},
{sfURI, soeREQUIRED},
{sfAmount, soeOPTIONAL},
{sfDestination, soeOPTIONAL},
{sfPreviousTxnID, soeREQUIRED},
{sfPreviousTxnLgrSeq, soeREQUIRED}
},
commonFields);
// clang-format on
}

View File

@@ -224,6 +224,7 @@ CONSTRUCT_TYPED_SFIELD(sfHookNamespace, "HookNamespace", UINT256,
CONSTRUCT_TYPED_SFIELD(sfHookSetTxnID, "HookSetTxnID", UINT256, 33);
CONSTRUCT_TYPED_SFIELD(sfOfferID, "OfferID", UINT256, 34);
CONSTRUCT_TYPED_SFIELD(sfEscrowID, "EscrowID", UINT256, 35);
CONSTRUCT_TYPED_SFIELD(sfURITokenID, "URITokenID", UINT256, 36);
// currency amount (common)
CONSTRUCT_TYPED_SFIELD(sfAmount, "Amount", AMOUNT, 1);

View File

@@ -361,6 +361,16 @@ TxFormats::TxFormats()
{sfInvoiceID, soeOPTIONAL},
},
commonFields);
add(jss::URIToken,
ttURI_TOKEN,
{
{sfURI, soeOPTIONAL},
{sfURITokenID, soeOPTIONAL},
{sfAmount, soeOPTIONAL},
{sfDestination, soeOPTIONAL}
},
commonFields);
}
TxFormats const&

View File

@@ -655,6 +655,7 @@ JSS(unl); // out: UnlList
JSS(unlimited); // out: Connection.h
JSS(uptime); // out: GetCounts
JSS(uri); // out: ValidatorSites
JSS(URIToken); // LedgerEntry
JSS(url); // in/out: Subscribe, Unsubscribe
JSS(url_password); // in: Subscribe
JSS(url_username); // in: Subscribe