mirror of
https://github.com/XRPLF/rippled.git
synced 2025-12-06 17:27:55 +00:00
XLS-52d: NFTokenMintOffer (#4845)
This commit is contained in:
@@ -37,65 +37,24 @@ NFTokenCreateOffer::preflight(PreflightContext const& ctx)
|
||||
return ret;
|
||||
|
||||
auto const txFlags = ctx.tx.getFlags();
|
||||
bool const isSellOffer = txFlags & tfSellNFToken;
|
||||
|
||||
if (txFlags & tfNFTokenCreateOfferMask)
|
||||
return temINVALID_FLAG;
|
||||
|
||||
auto const account = ctx.tx[sfAccount];
|
||||
auto const nftFlags = nft::getFlags(ctx.tx[sfNFTokenID]);
|
||||
|
||||
{
|
||||
STAmount const amount = ctx.tx[sfAmount];
|
||||
|
||||
if (amount.negative() && ctx.rules.enabled(fixNFTokenNegOffer))
|
||||
// An offer for a negative amount makes no sense.
|
||||
return temBAD_AMOUNT;
|
||||
|
||||
if (!isXRP(amount))
|
||||
{
|
||||
if (nftFlags & nft::flagOnlyXRP)
|
||||
return temBAD_AMOUNT;
|
||||
|
||||
if (!amount)
|
||||
return temBAD_AMOUNT;
|
||||
}
|
||||
|
||||
// If this is an offer to buy, you must offer something; if it's an
|
||||
// offer to sell, you can ask for nothing.
|
||||
if (!isSellOffer && !amount)
|
||||
return temBAD_AMOUNT;
|
||||
}
|
||||
|
||||
if (auto exp = ctx.tx[~sfExpiration]; exp == 0)
|
||||
return temBAD_EXPIRATION;
|
||||
|
||||
auto const owner = ctx.tx[~sfOwner];
|
||||
|
||||
// The 'Owner' field must be present when offering to buy, but can't
|
||||
// be present when selling (it's implicit):
|
||||
if (owner.has_value() == isSellOffer)
|
||||
return temMALFORMED;
|
||||
|
||||
if (owner && owner == account)
|
||||
return temMALFORMED;
|
||||
|
||||
if (auto dest = ctx.tx[~sfDestination])
|
||||
{
|
||||
// Some folks think it makes sense for a buy offer to specify a
|
||||
// specific broker using the Destination field. This change doesn't
|
||||
// deserve it's own amendment, so we're piggy-backing on
|
||||
// fixNFTokenNegOffer.
|
||||
//
|
||||
// Prior to fixNFTokenNegOffer any use of the Destination field on
|
||||
// a buy offer was malformed.
|
||||
if (!isSellOffer && !ctx.rules.enabled(fixNFTokenNegOffer))
|
||||
return temMALFORMED;
|
||||
|
||||
// The destination can't be the account executing the transaction.
|
||||
if (dest == account)
|
||||
return temMALFORMED;
|
||||
}
|
||||
// Use implementation shared with NFTokenMint
|
||||
if (NotTEC notTec = nft::tokenOfferCreatePreflight(
|
||||
ctx.tx[sfAccount],
|
||||
ctx.tx[sfAmount],
|
||||
ctx.tx[~sfDestination],
|
||||
ctx.tx[~sfExpiration],
|
||||
nftFlags,
|
||||
ctx.rules,
|
||||
ctx.tx[~sfOwner],
|
||||
txFlags);
|
||||
!isTesSuccess(notTec))
|
||||
return notTec;
|
||||
|
||||
return preflight2(ctx);
|
||||
}
|
||||
@@ -106,182 +65,44 @@ NFTokenCreateOffer::preclaim(PreclaimContext const& ctx)
|
||||
if (hasExpired(ctx.view, ctx.tx[~sfExpiration]))
|
||||
return tecEXPIRED;
|
||||
|
||||
auto const nftokenID = ctx.tx[sfNFTokenID];
|
||||
bool const isSellOffer = ctx.tx.isFlag(tfSellNFToken);
|
||||
uint256 const nftokenID = ctx.tx[sfNFTokenID];
|
||||
std::uint32_t const txFlags = {ctx.tx.getFlags()};
|
||||
|
||||
if (!nft::findToken(
|
||||
ctx.view, ctx.tx[isSellOffer ? sfAccount : sfOwner], nftokenID))
|
||||
ctx.view,
|
||||
ctx.tx[(txFlags & tfSellNFToken) ? sfAccount : sfOwner],
|
||||
nftokenID))
|
||||
return tecNO_ENTRY;
|
||||
|
||||
auto const nftFlags = nft::getFlags(nftokenID);
|
||||
auto const issuer = nft::getIssuer(nftokenID);
|
||||
auto const amount = ctx.tx[sfAmount];
|
||||
|
||||
if (!(nftFlags & nft::flagCreateTrustLines) && !amount.native() &&
|
||||
nft::getTransferFee(nftokenID))
|
||||
{
|
||||
if (!ctx.view.exists(keylet::account(issuer)))
|
||||
return tecNO_ISSUER;
|
||||
|
||||
if (!ctx.view.exists(keylet::line(issuer, amount.issue())))
|
||||
return tecNO_LINE;
|
||||
|
||||
if (isFrozen(
|
||||
ctx.view, issuer, amount.getCurrency(), amount.getIssuer()))
|
||||
return tecFROZEN;
|
||||
}
|
||||
|
||||
if (issuer != ctx.tx[sfAccount] && !(nftFlags & nft::flagTransferable))
|
||||
{
|
||||
auto const root = ctx.view.read(keylet::account(issuer));
|
||||
assert(root);
|
||||
|
||||
if (auto minter = (*root)[~sfNFTokenMinter];
|
||||
minter != ctx.tx[sfAccount])
|
||||
return tefNFTOKEN_IS_NOT_TRANSFERABLE;
|
||||
}
|
||||
|
||||
if (isFrozen(
|
||||
ctx.view,
|
||||
ctx.tx[sfAccount],
|
||||
amount.getCurrency(),
|
||||
amount.getIssuer()))
|
||||
return tecFROZEN;
|
||||
|
||||
// If this is an offer to buy the token, the account must have the
|
||||
// needed funds at hand; but note that funds aren't reserved and the
|
||||
// offer may later become unfunded.
|
||||
if (!isSellOffer)
|
||||
{
|
||||
// After this amendment, we allow an IOU issuer to make a buy offer
|
||||
// using their own currency.
|
||||
if (ctx.view.rules().enabled(fixNonFungibleTokensV1_2))
|
||||
{
|
||||
if (accountFunds(
|
||||
ctx.view,
|
||||
ctx.tx[sfAccount],
|
||||
amount,
|
||||
FreezeHandling::fhZERO_IF_FROZEN,
|
||||
ctx.j)
|
||||
.signum() <= 0)
|
||||
return tecUNFUNDED_OFFER;
|
||||
}
|
||||
else if (
|
||||
accountHolds(
|
||||
ctx.view,
|
||||
ctx.tx[sfAccount],
|
||||
amount.getCurrency(),
|
||||
amount.getIssuer(),
|
||||
FreezeHandling::fhZERO_IF_FROZEN,
|
||||
ctx.j)
|
||||
.signum() <= 0)
|
||||
return tecUNFUNDED_OFFER;
|
||||
}
|
||||
|
||||
if (auto const destination = ctx.tx[~sfDestination])
|
||||
{
|
||||
// If a destination is specified, the destination must already be in
|
||||
// the ledger.
|
||||
auto const sleDst = ctx.view.read(keylet::account(*destination));
|
||||
|
||||
if (!sleDst)
|
||||
return tecNO_DST;
|
||||
|
||||
// check if the destination has disallowed incoming offers
|
||||
if (ctx.view.rules().enabled(featureDisallowIncoming))
|
||||
{
|
||||
// flag cannot be set unless amendment is enabled but
|
||||
// out of an abundance of caution check anyway
|
||||
|
||||
if (sleDst->getFlags() & lsfDisallowIncomingNFTokenOffer)
|
||||
return tecNO_PERMISSION;
|
||||
}
|
||||
}
|
||||
|
||||
if (auto const owner = ctx.tx[~sfOwner])
|
||||
{
|
||||
// Check if the owner (buy offer) has disallowed incoming offers
|
||||
if (ctx.view.rules().enabled(featureDisallowIncoming))
|
||||
{
|
||||
auto const sleOwner = ctx.view.read(keylet::account(*owner));
|
||||
|
||||
// defensively check
|
||||
// it should not be possible to specify owner that doesn't exist
|
||||
if (!sleOwner)
|
||||
return tecNO_TARGET;
|
||||
|
||||
if (sleOwner->getFlags() & lsfDisallowIncomingNFTokenOffer)
|
||||
return tecNO_PERMISSION;
|
||||
}
|
||||
}
|
||||
|
||||
return tesSUCCESS;
|
||||
// Use implementation shared with NFTokenMint
|
||||
return nft::tokenOfferCreatePreclaim(
|
||||
ctx.view,
|
||||
ctx.tx[sfAccount],
|
||||
nft::getIssuer(nftokenID),
|
||||
ctx.tx[sfAmount],
|
||||
ctx.tx[~sfDestination],
|
||||
nft::getFlags(nftokenID),
|
||||
nft::getTransferFee(nftokenID),
|
||||
ctx.j,
|
||||
ctx.tx[~sfOwner],
|
||||
txFlags);
|
||||
}
|
||||
|
||||
TER
|
||||
NFTokenCreateOffer::doApply()
|
||||
{
|
||||
if (auto const acct = view().read(keylet::account(ctx_.tx[sfAccount]));
|
||||
mPriorBalance < view().fees().accountReserve((*acct)[sfOwnerCount] + 1))
|
||||
return tecINSUFFICIENT_RESERVE;
|
||||
|
||||
auto const nftokenID = ctx_.tx[sfNFTokenID];
|
||||
|
||||
auto const offerID =
|
||||
keylet::nftoffer(account_, ctx_.tx.getSeqProxy().value());
|
||||
|
||||
// Create the offer:
|
||||
{
|
||||
// Token offers are always added to the owner's owner directory:
|
||||
auto const ownerNode = view().dirInsert(
|
||||
keylet::ownerDir(account_), offerID, describeOwnerDir(account_));
|
||||
|
||||
if (!ownerNode)
|
||||
return tecDIR_FULL;
|
||||
|
||||
bool const isSellOffer = ctx_.tx.isFlag(tfSellNFToken);
|
||||
|
||||
// Token offers are also added to the token's buy or sell offer
|
||||
// directory
|
||||
auto const offerNode = view().dirInsert(
|
||||
isSellOffer ? keylet::nft_sells(nftokenID)
|
||||
: keylet::nft_buys(nftokenID),
|
||||
offerID,
|
||||
[&nftokenID, isSellOffer](std::shared_ptr<SLE> const& sle) {
|
||||
(*sle)[sfFlags] =
|
||||
isSellOffer ? lsfNFTokenSellOffers : lsfNFTokenBuyOffers;
|
||||
(*sle)[sfNFTokenID] = nftokenID;
|
||||
});
|
||||
|
||||
if (!offerNode)
|
||||
return tecDIR_FULL;
|
||||
|
||||
std::uint32_t sleFlags = 0;
|
||||
|
||||
if (isSellOffer)
|
||||
sleFlags |= lsfSellNFToken;
|
||||
|
||||
auto offer = std::make_shared<SLE>(offerID);
|
||||
(*offer)[sfOwner] = account_;
|
||||
(*offer)[sfNFTokenID] = nftokenID;
|
||||
(*offer)[sfAmount] = ctx_.tx[sfAmount];
|
||||
(*offer)[sfFlags] = sleFlags;
|
||||
(*offer)[sfOwnerNode] = *ownerNode;
|
||||
(*offer)[sfNFTokenOfferNode] = *offerNode;
|
||||
|
||||
if (auto const expiration = ctx_.tx[~sfExpiration])
|
||||
(*offer)[sfExpiration] = *expiration;
|
||||
|
||||
if (auto const destination = ctx_.tx[~sfDestination])
|
||||
(*offer)[sfDestination] = *destination;
|
||||
|
||||
view().insert(offer);
|
||||
}
|
||||
|
||||
// Update owner count.
|
||||
adjustOwnerCount(view(), view().peek(keylet::account(account_)), 1, j_);
|
||||
|
||||
return tesSUCCESS;
|
||||
// Use implementation shared with NFTokenMint
|
||||
return nft::tokenOfferCreateApply(
|
||||
view(),
|
||||
ctx_.tx[sfAccount],
|
||||
ctx_.tx[sfAmount],
|
||||
ctx_.tx[~sfDestination],
|
||||
ctx_.tx[~sfExpiration],
|
||||
ctx_.tx.getSeqProxy(),
|
||||
ctx_.tx[sfNFTokenID],
|
||||
mPriorBalance,
|
||||
j_,
|
||||
ctx_.tx.getFlags());
|
||||
}
|
||||
|
||||
} // namespace ripple
|
||||
|
||||
@@ -31,12 +31,25 @@
|
||||
|
||||
namespace ripple {
|
||||
|
||||
static std::uint16_t
|
||||
extractNFTokenFlagsFromTxFlags(std::uint32_t txFlags)
|
||||
{
|
||||
return static_cast<std::uint16_t>(txFlags & 0x0000FFFF);
|
||||
}
|
||||
|
||||
NotTEC
|
||||
NFTokenMint::preflight(PreflightContext const& ctx)
|
||||
{
|
||||
if (!ctx.rules.enabled(featureNonFungibleTokensV1))
|
||||
return temDISABLED;
|
||||
|
||||
bool const hasOfferFields = ctx.tx.isFieldPresent(sfAmount) ||
|
||||
ctx.tx.isFieldPresent(sfDestination) ||
|
||||
ctx.tx.isFieldPresent(sfExpiration);
|
||||
|
||||
if (!ctx.rules.enabled(featureNFTokenMintOffer) && hasOfferFields)
|
||||
return temDISABLED;
|
||||
|
||||
if (auto const ret = preflight1(ctx); !isTesSuccess(ret))
|
||||
return ret;
|
||||
|
||||
@@ -80,6 +93,29 @@ NFTokenMint::preflight(PreflightContext const& ctx)
|
||||
return temMALFORMED;
|
||||
}
|
||||
|
||||
if (hasOfferFields)
|
||||
{
|
||||
// The Amount field must be present if either the Destination or
|
||||
// Expiration fields are present.
|
||||
if (!ctx.tx.isFieldPresent(sfAmount))
|
||||
return temMALFORMED;
|
||||
|
||||
// Rely on the common code shared with NFTokenCreateOffer to
|
||||
// do the validation. We pass tfSellNFToken as the transaction flags
|
||||
// because a Mint is only allowed to create a sell offer.
|
||||
if (NotTEC notTec = nft::tokenOfferCreatePreflight(
|
||||
ctx.tx[sfAccount],
|
||||
ctx.tx[sfAmount],
|
||||
ctx.tx[~sfDestination],
|
||||
ctx.tx[~sfExpiration],
|
||||
extractNFTokenFlagsFromTxFlags(ctx.tx.getFlags()),
|
||||
ctx.rules);
|
||||
!isTesSuccess(notTec))
|
||||
{
|
||||
return notTec;
|
||||
}
|
||||
}
|
||||
|
||||
return preflight2(ctx);
|
||||
}
|
||||
|
||||
@@ -146,6 +182,27 @@ NFTokenMint::preclaim(PreclaimContext const& ctx)
|
||||
return tecNO_PERMISSION;
|
||||
}
|
||||
|
||||
if (ctx.tx.isFieldPresent(sfAmount))
|
||||
{
|
||||
// The Amount field says create an offer for the minted token.
|
||||
if (hasExpired(ctx.view, ctx.tx[~sfExpiration]))
|
||||
return tecEXPIRED;
|
||||
|
||||
// Rely on the common code shared with NFTokenCreateOffer to
|
||||
// do the validation. We pass tfSellNFToken as the transaction flags
|
||||
// because a Mint is only allowed to create a sell offer.
|
||||
if (TER const ter = nft::tokenOfferCreatePreclaim(
|
||||
ctx.view,
|
||||
ctx.tx[sfAccount],
|
||||
ctx.tx[~sfIssuer].value_or(ctx.tx[sfAccount]),
|
||||
ctx.tx[sfAmount],
|
||||
ctx.tx[~sfDestination],
|
||||
extractNFTokenFlagsFromTxFlags(ctx.tx.getFlags()),
|
||||
ctx.tx[~sfTransferFee].value_or(0),
|
||||
ctx.j);
|
||||
!isTesSuccess(ter))
|
||||
return ter;
|
||||
}
|
||||
return tesSUCCESS;
|
||||
}
|
||||
|
||||
@@ -238,18 +295,16 @@ NFTokenMint::doApply()
|
||||
// Should never happen.
|
||||
return tecINTERNAL;
|
||||
|
||||
auto const nftokenID = createNFTokenID(
|
||||
extractNFTokenFlagsFromTxFlags(ctx_.tx.getFlags()),
|
||||
ctx_.tx[~sfTransferFee].value_or(0),
|
||||
issuer,
|
||||
nft::toTaxon(ctx_.tx[sfNFTokenTaxon]),
|
||||
tokenSeq.value());
|
||||
|
||||
STObject newToken(
|
||||
*nfTokenTemplate,
|
||||
sfNFToken,
|
||||
[this, &issuer, &tokenSeq](STObject& object) {
|
||||
object.setFieldH256(
|
||||
sfNFTokenID,
|
||||
createNFTokenID(
|
||||
static_cast<std::uint16_t>(ctx_.tx.getFlags() & 0x0000FFFF),
|
||||
ctx_.tx[~sfTransferFee].value_or(0),
|
||||
issuer,
|
||||
nft::toTaxon(ctx_.tx[sfNFTokenTaxon]),
|
||||
tokenSeq.value()));
|
||||
*nfTokenTemplate, sfNFToken, [this, &nftokenID](STObject& object) {
|
||||
object.setFieldH256(sfNFTokenID, nftokenID);
|
||||
|
||||
if (auto const uri = ctx_.tx[~sfURI])
|
||||
object.setFieldVL(sfURI, *uri);
|
||||
@@ -260,10 +315,29 @@ NFTokenMint::doApply()
|
||||
ret != tesSUCCESS)
|
||||
return ret;
|
||||
|
||||
if (ctx_.tx.isFieldPresent(sfAmount))
|
||||
{
|
||||
// Rely on the common code shared with NFTokenCreateOffer to create
|
||||
// the offer. We pass tfSellNFToken as the transaction flags
|
||||
// because a Mint is only allowed to create a sell offer.
|
||||
if (TER const ter = nft::tokenOfferCreateApply(
|
||||
view(),
|
||||
ctx_.tx[sfAccount],
|
||||
ctx_.tx[sfAmount],
|
||||
ctx_.tx[~sfDestination],
|
||||
ctx_.tx[~sfExpiration],
|
||||
ctx_.tx.getSeqProxy(),
|
||||
nftokenID,
|
||||
mPriorBalance,
|
||||
j_);
|
||||
!isTesSuccess(ter))
|
||||
return ter;
|
||||
}
|
||||
|
||||
// Only check the reserve if the owner count actually changed. This
|
||||
// allows NFTs to be added to the page (and burn fees) without
|
||||
// requiring the reserve to be met each time. The reserve is
|
||||
// only managed when a new NFT page is added.
|
||||
// only managed when a new NFT page or sell offer is added.
|
||||
if (auto const ownerCountAfter =
|
||||
view().read(keylet::account(account_))->getFieldU32(sfOwnerCount);
|
||||
ownerCountAfter > ownerCountBefore)
|
||||
|
||||
@@ -636,5 +636,252 @@ deleteTokenOffer(ApplyView& view, std::shared_ptr<SLE> const& offer)
|
||||
return true;
|
||||
}
|
||||
|
||||
NotTEC
|
||||
tokenOfferCreatePreflight(
|
||||
AccountID const& acctID,
|
||||
STAmount const& amount,
|
||||
std::optional<AccountID> const& dest,
|
||||
std::optional<std::uint32_t> const& expiration,
|
||||
std::uint16_t nftFlags,
|
||||
Rules const& rules,
|
||||
std::optional<AccountID> const& owner,
|
||||
std::uint32_t txFlags)
|
||||
{
|
||||
if (amount.negative() && rules.enabled(fixNFTokenNegOffer))
|
||||
// An offer for a negative amount makes no sense.
|
||||
return temBAD_AMOUNT;
|
||||
|
||||
if (!isXRP(amount))
|
||||
{
|
||||
if (nftFlags & nft::flagOnlyXRP)
|
||||
return temBAD_AMOUNT;
|
||||
|
||||
if (!amount)
|
||||
return temBAD_AMOUNT;
|
||||
}
|
||||
|
||||
// If this is an offer to buy, you must offer something; if it's an
|
||||
// offer to sell, you can ask for nothing.
|
||||
bool const isSellOffer = txFlags & tfSellNFToken;
|
||||
if (!isSellOffer && !amount)
|
||||
return temBAD_AMOUNT;
|
||||
|
||||
if (expiration.has_value() && expiration.value() == 0)
|
||||
return temBAD_EXPIRATION;
|
||||
|
||||
// The 'Owner' field must be present when offering to buy, but can't
|
||||
// be present when selling (it's implicit):
|
||||
if (owner.has_value() == isSellOffer)
|
||||
return temMALFORMED;
|
||||
|
||||
if (owner && owner == acctID)
|
||||
return temMALFORMED;
|
||||
|
||||
if (dest)
|
||||
{
|
||||
// Some folks think it makes sense for a buy offer to specify a
|
||||
// specific broker using the Destination field. This change doesn't
|
||||
// deserve it's own amendment, so we're piggy-backing on
|
||||
// fixNFTokenNegOffer.
|
||||
//
|
||||
// Prior to fixNFTokenNegOffer any use of the Destination field on
|
||||
// a buy offer was malformed.
|
||||
if (!isSellOffer && !rules.enabled(fixNFTokenNegOffer))
|
||||
return temMALFORMED;
|
||||
|
||||
// The destination can't be the account executing the transaction.
|
||||
if (dest == acctID)
|
||||
return temMALFORMED;
|
||||
}
|
||||
return tesSUCCESS;
|
||||
}
|
||||
|
||||
TER
|
||||
tokenOfferCreatePreclaim(
|
||||
ReadView const& view,
|
||||
AccountID const& acctID,
|
||||
AccountID const& nftIssuer,
|
||||
STAmount const& amount,
|
||||
std::optional<AccountID> const& dest,
|
||||
std::uint16_t nftFlags,
|
||||
std::uint16_t xferFee,
|
||||
beast::Journal j,
|
||||
std::optional<AccountID> const& owner,
|
||||
std::uint32_t txFlags)
|
||||
{
|
||||
if (!(nftFlags & nft::flagCreateTrustLines) && !amount.native() && xferFee)
|
||||
{
|
||||
if (!view.exists(keylet::account(nftIssuer)))
|
||||
return tecNO_ISSUER;
|
||||
|
||||
// If the IOU issuer and the NFToken issuer are the same, then that
|
||||
// issuer does not need a trust line to accept their fee.
|
||||
if (view.rules().enabled(featureNFTokenMintOffer))
|
||||
{
|
||||
if (nftIssuer != amount.getIssuer() &&
|
||||
!view.read(keylet::line(nftIssuer, amount.issue())))
|
||||
return tecNO_LINE;
|
||||
}
|
||||
else if (!view.exists(keylet::line(nftIssuer, amount.issue())))
|
||||
{
|
||||
return tecNO_LINE;
|
||||
}
|
||||
|
||||
if (isFrozen(view, nftIssuer, amount.getCurrency(), amount.getIssuer()))
|
||||
return tecFROZEN;
|
||||
}
|
||||
|
||||
if (nftIssuer != acctID && !(nftFlags & nft::flagTransferable))
|
||||
{
|
||||
auto const root = view.read(keylet::account(nftIssuer));
|
||||
assert(root);
|
||||
|
||||
if (auto minter = (*root)[~sfNFTokenMinter]; minter != acctID)
|
||||
return tefNFTOKEN_IS_NOT_TRANSFERABLE;
|
||||
}
|
||||
|
||||
if (isFrozen(view, acctID, amount.getCurrency(), amount.getIssuer()))
|
||||
return tecFROZEN;
|
||||
|
||||
// If this is an offer to buy the token, the account must have the
|
||||
// needed funds at hand; but note that funds aren't reserved and the
|
||||
// offer may later become unfunded.
|
||||
if ((txFlags & tfSellNFToken) == 0)
|
||||
{
|
||||
// After this amendment, we allow an IOU issuer to make a buy offer
|
||||
// using their own currency.
|
||||
if (view.rules().enabled(fixNonFungibleTokensV1_2))
|
||||
{
|
||||
if (accountFunds(
|
||||
view, acctID, amount, FreezeHandling::fhZERO_IF_FROZEN, j)
|
||||
.signum() <= 0)
|
||||
return tecUNFUNDED_OFFER;
|
||||
}
|
||||
else if (
|
||||
accountHolds(
|
||||
view,
|
||||
acctID,
|
||||
amount.getCurrency(),
|
||||
amount.getIssuer(),
|
||||
FreezeHandling::fhZERO_IF_FROZEN,
|
||||
j)
|
||||
.signum() <= 0)
|
||||
return tecUNFUNDED_OFFER;
|
||||
}
|
||||
|
||||
if (dest)
|
||||
{
|
||||
// If a destination is specified, the destination must already be in
|
||||
// the ledger.
|
||||
auto const sleDst = view.read(keylet::account(*dest));
|
||||
|
||||
if (!sleDst)
|
||||
return tecNO_DST;
|
||||
|
||||
// check if the destination has disallowed incoming offers
|
||||
if (view.rules().enabled(featureDisallowIncoming))
|
||||
{
|
||||
// flag cannot be set unless amendment is enabled but
|
||||
// out of an abundance of caution check anyway
|
||||
|
||||
if (sleDst->getFlags() & lsfDisallowIncomingNFTokenOffer)
|
||||
return tecNO_PERMISSION;
|
||||
}
|
||||
}
|
||||
|
||||
if (owner)
|
||||
{
|
||||
// Check if the owner (buy offer) has disallowed incoming offers
|
||||
if (view.rules().enabled(featureDisallowIncoming))
|
||||
{
|
||||
auto const sleOwner = view.read(keylet::account(*owner));
|
||||
|
||||
// defensively check
|
||||
// it should not be possible to specify owner that doesn't exist
|
||||
if (!sleOwner)
|
||||
return tecNO_TARGET;
|
||||
|
||||
if (sleOwner->getFlags() & lsfDisallowIncomingNFTokenOffer)
|
||||
return tecNO_PERMISSION;
|
||||
}
|
||||
}
|
||||
|
||||
return tesSUCCESS;
|
||||
}
|
||||
|
||||
TER
|
||||
tokenOfferCreateApply(
|
||||
ApplyView& view,
|
||||
AccountID const& acctID,
|
||||
STAmount const& amount,
|
||||
std::optional<AccountID> const& dest,
|
||||
std::optional<std::uint32_t> const& expiration,
|
||||
SeqProxy seqProxy,
|
||||
uint256 const& nftokenID,
|
||||
XRPAmount const& priorBalance,
|
||||
beast::Journal j,
|
||||
std::uint32_t txFlags)
|
||||
{
|
||||
Keylet const acctKeylet = keylet::account(acctID);
|
||||
if (auto const acct = view.read(acctKeylet);
|
||||
priorBalance < view.fees().accountReserve((*acct)[sfOwnerCount] + 1))
|
||||
return tecINSUFFICIENT_RESERVE;
|
||||
|
||||
auto const offerID = keylet::nftoffer(acctID, seqProxy.value());
|
||||
|
||||
// Create the offer:
|
||||
{
|
||||
// Token offers are always added to the owner's owner directory:
|
||||
auto const ownerNode = view.dirInsert(
|
||||
keylet::ownerDir(acctID), offerID, describeOwnerDir(acctID));
|
||||
|
||||
if (!ownerNode)
|
||||
return tecDIR_FULL;
|
||||
|
||||
bool const isSellOffer = txFlags & tfSellNFToken;
|
||||
|
||||
// Token offers are also added to the token's buy or sell offer
|
||||
// directory
|
||||
auto const offerNode = view.dirInsert(
|
||||
isSellOffer ? keylet::nft_sells(nftokenID)
|
||||
: keylet::nft_buys(nftokenID),
|
||||
offerID,
|
||||
[&nftokenID, isSellOffer](std::shared_ptr<SLE> const& sle) {
|
||||
(*sle)[sfFlags] =
|
||||
isSellOffer ? lsfNFTokenSellOffers : lsfNFTokenBuyOffers;
|
||||
(*sle)[sfNFTokenID] = nftokenID;
|
||||
});
|
||||
|
||||
if (!offerNode)
|
||||
return tecDIR_FULL;
|
||||
|
||||
std::uint32_t sleFlags = 0;
|
||||
|
||||
if (isSellOffer)
|
||||
sleFlags |= lsfSellNFToken;
|
||||
|
||||
auto offer = std::make_shared<SLE>(offerID);
|
||||
(*offer)[sfOwner] = acctID;
|
||||
(*offer)[sfNFTokenID] = nftokenID;
|
||||
(*offer)[sfAmount] = amount;
|
||||
(*offer)[sfFlags] = sleFlags;
|
||||
(*offer)[sfOwnerNode] = *ownerNode;
|
||||
(*offer)[sfNFTokenOfferNode] = *offerNode;
|
||||
|
||||
if (expiration)
|
||||
(*offer)[sfExpiration] = *expiration;
|
||||
|
||||
if (dest)
|
||||
(*offer)[sfDestination] = *dest;
|
||||
|
||||
view.insert(offer);
|
||||
}
|
||||
|
||||
// Update owner count.
|
||||
adjustOwnerCount(view, view.peek(acctKeylet), 1, j);
|
||||
|
||||
return tesSUCCESS;
|
||||
}
|
||||
|
||||
} // namespace nft
|
||||
} // namespace ripple
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
#ifndef RIPPLE_TX_IMPL_DETAILS_NFTOKENUTILS_H_INCLUDED
|
||||
#define RIPPLE_TX_IMPL_DETAILS_NFTOKENUTILS_H_INCLUDED
|
||||
|
||||
#include <ripple/app/tx/impl/Transactor.h>
|
||||
#include <ripple/basics/base_uint.h>
|
||||
#include <ripple/basics/tagged_integer.h>
|
||||
#include <ripple/ledger/ApplyView.h>
|
||||
@@ -97,6 +98,46 @@ deleteTokenOffer(ApplyView& view, std::shared_ptr<SLE> const& offer);
|
||||
bool
|
||||
compareTokens(uint256 const& a, uint256 const& b);
|
||||
|
||||
/** Preflight checks shared by NFTokenCreateOffer and NFTokenMint */
|
||||
NotTEC
|
||||
tokenOfferCreatePreflight(
|
||||
AccountID const& acctID,
|
||||
STAmount const& amount,
|
||||
std::optional<AccountID> const& dest,
|
||||
std::optional<std::uint32_t> const& expiration,
|
||||
std::uint16_t nftFlags,
|
||||
Rules const& rules,
|
||||
std::optional<AccountID> const& owner = std::nullopt,
|
||||
std::uint32_t txFlags = lsfSellNFToken);
|
||||
|
||||
/** Preclaim checks shared by NFTokenCreateOffer and NFTokenMint */
|
||||
TER
|
||||
tokenOfferCreatePreclaim(
|
||||
ReadView const& view,
|
||||
AccountID const& acctID,
|
||||
AccountID const& nftIssuer,
|
||||
STAmount const& amount,
|
||||
std::optional<AccountID> const& dest,
|
||||
std::uint16_t nftFlags,
|
||||
std::uint16_t xferFee,
|
||||
beast::Journal j,
|
||||
std::optional<AccountID> const& owner = std::nullopt,
|
||||
std::uint32_t txFlags = lsfSellNFToken);
|
||||
|
||||
/** doApply implementation shared by NFTokenCreateOffer and NFTokenMint */
|
||||
TER
|
||||
tokenOfferCreateApply(
|
||||
ApplyView& view,
|
||||
AccountID const& acctID,
|
||||
STAmount const& amount,
|
||||
std::optional<AccountID> const& dest,
|
||||
std::optional<std::uint32_t> const& expiration,
|
||||
SeqProxy seqProxy,
|
||||
uint256 const& nftokenID,
|
||||
XRPAmount const& priorBalance,
|
||||
beast::Journal j,
|
||||
std::uint32_t txFlags = lsfSellNFToken);
|
||||
|
||||
} // namespace nft
|
||||
|
||||
} // namespace ripple
|
||||
|
||||
@@ -80,7 +80,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 = 74;
|
||||
static constexpr std::size_t numFeatures = 75;
|
||||
|
||||
/** Amendments that this server supports and the default voting behavior.
|
||||
Whether they are enabled depends on the Rules defined in the validated
|
||||
@@ -366,6 +366,7 @@ extern uint256 const fixEmptyDID;
|
||||
extern uint256 const fixXChainRewardRounding;
|
||||
extern uint256 const fixPreviousTxnID;
|
||||
extern uint256 const fixAMMv1_1;
|
||||
extern uint256 const featureNFTokenMintOffer;
|
||||
extern uint256 const fixReducedOffersV2;
|
||||
|
||||
} // namespace ripple
|
||||
|
||||
@@ -493,6 +493,7 @@ REGISTER_FIX (fixEmptyDID, Supported::yes, VoteBehavior::De
|
||||
REGISTER_FIX (fixXChainRewardRounding, Supported::yes, VoteBehavior::DefaultNo);
|
||||
REGISTER_FIX (fixPreviousTxnID, Supported::yes, VoteBehavior::DefaultNo);
|
||||
REGISTER_FIX (fixAMMv1_1, Supported::yes, VoteBehavior::DefaultNo);
|
||||
REGISTER_FEATURE(NFTokenMintOffer, Supported::yes, VoteBehavior::DefaultNo);
|
||||
REGISTER_FIX (fixReducedOffersV2, Supported::yes, VoteBehavior::DefaultNo);
|
||||
|
||||
// The following amendments are obsolete, but must remain supported
|
||||
|
||||
@@ -31,7 +31,8 @@ canHaveNFTokenOfferID(
|
||||
return false;
|
||||
|
||||
TxType const tt = serializedTx->getTxnType();
|
||||
if (tt != ttNFTOKEN_CREATE_OFFER)
|
||||
if (!(tt == ttNFTOKEN_MINT && serializedTx->isFieldPresent(sfAmount)) &&
|
||||
tt != ttNFTOKEN_CREATE_OFFER)
|
||||
return false;
|
||||
|
||||
// if the transaction failed nothing could have been delivered.
|
||||
|
||||
@@ -333,6 +333,9 @@ TxFormats::TxFormats()
|
||||
{sfTransferFee, soeOPTIONAL},
|
||||
{sfIssuer, soeOPTIONAL},
|
||||
{sfURI, soeOPTIONAL},
|
||||
{sfAmount, soeOPTIONAL},
|
||||
{sfDestination, soeOPTIONAL},
|
||||
{sfExpiration, soeOPTIONAL},
|
||||
},
|
||||
commonFields);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user