XLS-52d: NFTokenMintOffer (#4845)

This commit is contained in:
tequ
2024-06-15 01:32:25 +02:00
committed by GitHub
parent 3f5e3212fe
commit 9f7c619e4f
11 changed files with 927 additions and 241 deletions

View File

@@ -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

View File

@@ -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)

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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.

View File

@@ -333,6 +333,9 @@ TxFormats::TxFormats()
{sfTransferFee, soeOPTIONAL},
{sfIssuer, soeOPTIONAL},
{sfURI, soeOPTIONAL},
{sfAmount, soeOPTIONAL},
{sfDestination, soeOPTIONAL},
{sfExpiration, soeOPTIONAL},
},
commonFields);