mirror of
https://github.com/Xahau/xahaud.git
synced 2025-11-19 18:15:50 +00:00
4959 lines
193 KiB
C++
4959 lines
193 KiB
C++
//------------------------------------------------------------------------------
|
|
/*
|
|
This file is part of rippled: https://github.com/ripple/rippled
|
|
Copyright (c) 2021 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/details/NFTokenUtils.h>
|
|
#include <ripple/basics/random.h>
|
|
#include <ripple/protocol/Feature.h>
|
|
#include <ripple/protocol/jss.h>
|
|
#include <test/jtx.h>
|
|
|
|
#include <initializer_list>
|
|
|
|
namespace ripple {
|
|
|
|
class NFToken_test : public beast::unit_test::suite
|
|
{
|
|
// Helper function that returns the owner count of an account root.
|
|
static std::uint32_t
|
|
ownerCount(test::jtx::Env const& env, test::jtx::Account const& acct)
|
|
{
|
|
std::uint32_t ret{0};
|
|
if (auto const sleAcct = env.le(acct))
|
|
ret = sleAcct->at(sfOwnerCount);
|
|
return ret;
|
|
}
|
|
|
|
// Helper function that returns the number of NFTs minted by an issuer.
|
|
static std::uint32_t
|
|
mintedCount(test::jtx::Env const& env, test::jtx::Account const& issuer)
|
|
{
|
|
std::uint32_t ret{0};
|
|
if (auto const sleIssuer = env.le(issuer))
|
|
ret = sleIssuer->at(~sfMintedNFTokens).value_or(0);
|
|
return ret;
|
|
}
|
|
|
|
// Helper function that returns the number of an issuer's burned NFTs.
|
|
static std::uint32_t
|
|
burnedCount(test::jtx::Env const& env, test::jtx::Account const& issuer)
|
|
{
|
|
std::uint32_t ret{0};
|
|
if (auto const sleIssuer = env.le(issuer))
|
|
ret = sleIssuer->at(~sfBurnedNFTokens).value_or(0);
|
|
return ret;
|
|
}
|
|
|
|
// Helper function that returns the number of nfts owned by an account.
|
|
static std::uint32_t
|
|
nftCount(test::jtx::Env& env, test::jtx::Account const& acct)
|
|
{
|
|
Json::Value params;
|
|
params[jss::account] = acct.human();
|
|
params[jss::type] = "state";
|
|
Json::Value nfts = env.rpc("json", "account_nfts", to_string(params));
|
|
return nfts[jss::result][jss::account_nfts].size();
|
|
};
|
|
|
|
// Helper function that returns the number of tickets held by an account.
|
|
static std::uint32_t
|
|
ticketCount(test::jtx::Env const& env, test::jtx::Account const& acct)
|
|
{
|
|
std::uint32_t ret{0};
|
|
if (auto const sleAcct = env.le(acct))
|
|
ret = sleAcct->at(~sfTicketCount).value_or(0);
|
|
return ret;
|
|
}
|
|
|
|
// Helper function returns the close time of the parent ledger.
|
|
std::uint32_t
|
|
lastClose(test::jtx::Env& env)
|
|
{
|
|
return env.current()->info().parentCloseTime.time_since_epoch().count();
|
|
}
|
|
|
|
void
|
|
testEnabled(FeatureBitset features)
|
|
{
|
|
testcase("Enabled");
|
|
|
|
using namespace test::jtx;
|
|
{
|
|
// If the NFT amendment is not enabled, you should not be able
|
|
// to create or burn NFTs.
|
|
Env env{
|
|
*this,
|
|
features - featureNonFungibleTokensV1 -
|
|
featureNonFungibleTokensV1_1};
|
|
Account const& master = env.master;
|
|
|
|
BEAST_EXPECT(ownerCount(env, master) == 0);
|
|
BEAST_EXPECT(mintedCount(env, master) == 0);
|
|
BEAST_EXPECT(burnedCount(env, master) == 0);
|
|
|
|
uint256 const nftId{token::getNextID(env, master, 0u)};
|
|
env(token::mint(master, 0u), ter(temDISABLED));
|
|
env.close();
|
|
BEAST_EXPECT(ownerCount(env, master) == 0);
|
|
BEAST_EXPECT(mintedCount(env, master) == 0);
|
|
BEAST_EXPECT(burnedCount(env, master) == 0);
|
|
|
|
env(token::burn(master, nftId), ter(temDISABLED));
|
|
env.close();
|
|
BEAST_EXPECT(ownerCount(env, master) == 0);
|
|
BEAST_EXPECT(mintedCount(env, master) == 0);
|
|
BEAST_EXPECT(burnedCount(env, master) == 0);
|
|
|
|
uint256 const offerIndex =
|
|
keylet::nftoffer(master, env.seq(master)).key;
|
|
env(token::createOffer(master, nftId, XRP(10)), ter(temDISABLED));
|
|
env.close();
|
|
BEAST_EXPECT(ownerCount(env, master) == 0);
|
|
BEAST_EXPECT(mintedCount(env, master) == 0);
|
|
BEAST_EXPECT(burnedCount(env, master) == 0);
|
|
|
|
env(token::cancelOffer(master, {offerIndex}), ter(temDISABLED));
|
|
env.close();
|
|
BEAST_EXPECT(ownerCount(env, master) == 0);
|
|
BEAST_EXPECT(mintedCount(env, master) == 0);
|
|
BEAST_EXPECT(burnedCount(env, master) == 0);
|
|
|
|
env(token::acceptBuyOffer(master, offerIndex), ter(temDISABLED));
|
|
env.close();
|
|
BEAST_EXPECT(ownerCount(env, master) == 0);
|
|
BEAST_EXPECT(mintedCount(env, master) == 0);
|
|
BEAST_EXPECT(burnedCount(env, master) == 0);
|
|
}
|
|
{
|
|
// If the NFT amendment is enabled all NFT-related
|
|
// facilities should be available.
|
|
Env env{*this, features};
|
|
Account const& master = env.master;
|
|
|
|
BEAST_EXPECT(ownerCount(env, master) == 0);
|
|
BEAST_EXPECT(mintedCount(env, master) == 0);
|
|
BEAST_EXPECT(burnedCount(env, master) == 0);
|
|
|
|
uint256 const nftId0{token::getNextID(env, env.master, 0u)};
|
|
env(token::mint(env.master, 0u));
|
|
env.close();
|
|
BEAST_EXPECT(ownerCount(env, master) == 1);
|
|
BEAST_EXPECT(mintedCount(env, master) == 1);
|
|
BEAST_EXPECT(burnedCount(env, master) == 0);
|
|
|
|
env(token::burn(env.master, nftId0));
|
|
env.close();
|
|
BEAST_EXPECT(ownerCount(env, master) == 0);
|
|
BEAST_EXPECT(mintedCount(env, master) == 1);
|
|
BEAST_EXPECT(burnedCount(env, master) == 1);
|
|
|
|
uint256 const nftId1{
|
|
token::getNextID(env, env.master, 0u, tfTransferable)};
|
|
env(token::mint(env.master, 0u), txflags(tfTransferable));
|
|
env.close();
|
|
BEAST_EXPECT(ownerCount(env, master) == 1);
|
|
BEAST_EXPECT(mintedCount(env, master) == 2);
|
|
BEAST_EXPECT(burnedCount(env, master) == 1);
|
|
|
|
Account const alice{"alice"};
|
|
env.fund(XRP(10000), alice);
|
|
env.close();
|
|
uint256 const aliceOfferIndex =
|
|
keylet::nftoffer(alice, env.seq(alice)).key;
|
|
env(token::createOffer(alice, nftId1, XRP(1000)),
|
|
token::owner(master));
|
|
env.close();
|
|
|
|
BEAST_EXPECT(ownerCount(env, master) == 1);
|
|
BEAST_EXPECT(mintedCount(env, master) == 2);
|
|
BEAST_EXPECT(burnedCount(env, master) == 1);
|
|
|
|
BEAST_EXPECT(ownerCount(env, alice) == 1);
|
|
BEAST_EXPECT(mintedCount(env, alice) == 0);
|
|
BEAST_EXPECT(burnedCount(env, alice) == 0);
|
|
|
|
env(token::acceptBuyOffer(master, aliceOfferIndex));
|
|
env.close();
|
|
|
|
BEAST_EXPECT(ownerCount(env, master) == 0);
|
|
BEAST_EXPECT(mintedCount(env, master) == 2);
|
|
BEAST_EXPECT(burnedCount(env, master) == 1);
|
|
|
|
BEAST_EXPECT(ownerCount(env, alice) == 1);
|
|
BEAST_EXPECT(mintedCount(env, alice) == 0);
|
|
BEAST_EXPECT(burnedCount(env, alice) == 0);
|
|
}
|
|
}
|
|
|
|
void
|
|
testMintReserve(FeatureBitset features)
|
|
{
|
|
// Verify that the reserve behaves as expected for minting.
|
|
testcase("Mint reserve");
|
|
|
|
using namespace test::jtx;
|
|
|
|
Env env{*this, features};
|
|
Account const alice{"alice"};
|
|
Account const minter{"minter"};
|
|
|
|
// Fund alice and minter enough to exist, but not enough to meet
|
|
// the reserve for creating their first NFT. Account reserve for unit
|
|
// tests is 200 XRP, not 20.
|
|
env.fund(XRP(200), alice, minter);
|
|
env.close();
|
|
BEAST_EXPECT(env.balance(alice) == XRP(200));
|
|
BEAST_EXPECT(env.balance(minter) == XRP(200));
|
|
BEAST_EXPECT(ownerCount(env, alice) == 0);
|
|
BEAST_EXPECT(ownerCount(env, minter) == 0);
|
|
|
|
// alice does not have enough XRP to cover the reserve for an NFT page.
|
|
env(token::mint(alice, 0u), ter(tecINSUFFICIENT_RESERVE));
|
|
env.close();
|
|
BEAST_EXPECT(ownerCount(env, alice) == 0);
|
|
BEAST_EXPECT(mintedCount(env, alice) == 0);
|
|
BEAST_EXPECT(burnedCount(env, alice) == 0);
|
|
|
|
// Pay alice almost enough to make the reserve for an NFT page.
|
|
env(pay(env.master, alice, XRP(50) + drops(9)));
|
|
env.close();
|
|
|
|
// A lambda that checks alice's ownerCount, mintedCount, and
|
|
// burnedCount all in one fell swoop.
|
|
auto checkAliceOwnerMintedBurned = [&env, this, &alice](
|
|
std::uint32_t owners,
|
|
std::uint32_t minted,
|
|
std::uint32_t burned,
|
|
int line) {
|
|
auto oneCheck =
|
|
[line, this](
|
|
char const* type, std::uint32_t found, std::uint32_t exp) {
|
|
if (found == exp)
|
|
pass();
|
|
else
|
|
{
|
|
std::stringstream ss;
|
|
ss << "Wrong " << type << " count. Found: " << found
|
|
<< "; Expected: " << exp;
|
|
fail(ss.str(), __FILE__, line);
|
|
}
|
|
};
|
|
oneCheck("owner", ownerCount(env, alice), owners);
|
|
oneCheck("minted", mintedCount(env, alice), minted);
|
|
oneCheck("burned", burnedCount(env, alice), burned);
|
|
};
|
|
|
|
// alice still does not have enough XRP for the reserve of an NFT page.
|
|
env(token::mint(alice, 0u), ter(tecINSUFFICIENT_RESERVE));
|
|
env.close();
|
|
checkAliceOwnerMintedBurned(0, 0, 0, __LINE__);
|
|
|
|
// Pay alice enough to make the reserve for an NFT page.
|
|
env(pay(env.master, alice, drops(11)));
|
|
env.close();
|
|
|
|
// Now alice can mint an NFT.
|
|
env(token::mint(alice));
|
|
env.close();
|
|
checkAliceOwnerMintedBurned(1, 1, 0, __LINE__);
|
|
|
|
// Alice should be able to mint an additional 31 NFTs without
|
|
// any additional reserve requirements.
|
|
for (int i = 1; i < 32; ++i)
|
|
{
|
|
env(token::mint(alice));
|
|
checkAliceOwnerMintedBurned(1, i + 1, 0, __LINE__);
|
|
}
|
|
|
|
// That NFT page is full. Creating an additional NFT page requires
|
|
// additional reserve.
|
|
env(token::mint(alice), ter(tecINSUFFICIENT_RESERVE));
|
|
env.close();
|
|
checkAliceOwnerMintedBurned(1, 32, 0, __LINE__);
|
|
|
|
// Pay alice almost enough to make the reserve for an NFT page.
|
|
env(pay(env.master, alice, XRP(50) + drops(329)));
|
|
env.close();
|
|
|
|
// alice still does not have enough XRP for the reserve of an NFT page.
|
|
env(token::mint(alice), ter(tecINSUFFICIENT_RESERVE));
|
|
env.close();
|
|
checkAliceOwnerMintedBurned(1, 32, 0, __LINE__);
|
|
|
|
// Pay alice enough to make the reserve for an NFT page.
|
|
env(pay(env.master, alice, drops(11)));
|
|
env.close();
|
|
|
|
// Now alice can mint an NFT.
|
|
env(token::mint(alice));
|
|
env.close();
|
|
checkAliceOwnerMintedBurned(2, 33, 0, __LINE__);
|
|
|
|
// alice burns the NFTs she created: check that pages consolidate
|
|
std::uint32_t seq = 0;
|
|
|
|
while (seq < 33)
|
|
{
|
|
env(token::burn(alice, token::getID(alice, 0, seq++)));
|
|
env.close();
|
|
checkAliceOwnerMintedBurned((33 - seq) ? 1 : 0, 33, seq, __LINE__);
|
|
}
|
|
|
|
// alice burns a non-existent NFT.
|
|
env(token::burn(alice, token::getID(alice, 197, 5)), ter(tecNO_ENTRY));
|
|
env.close();
|
|
checkAliceOwnerMintedBurned(0, 33, 33, __LINE__);
|
|
|
|
// That was fun! Now let's see what happens when we let someone else
|
|
// mint NFTs on alice's behalf. alice gives permission to minter.
|
|
env(token::setMinter(alice, minter));
|
|
env.close();
|
|
BEAST_EXPECT(
|
|
env.le(alice)->getAccountID(sfNFTokenMinter) == minter.id());
|
|
|
|
// A lambda that checks minter's and alice's ownerCount,
|
|
// mintedCount, and burnedCount all in one fell swoop.
|
|
auto checkMintersOwnerMintedBurned = [&env, this, &alice, &minter](
|
|
std::uint32_t aliceOwners,
|
|
std::uint32_t aliceMinted,
|
|
std::uint32_t aliceBurned,
|
|
std::uint32_t minterOwners,
|
|
std::uint32_t minterMinted,
|
|
std::uint32_t minterBurned,
|
|
int line) {
|
|
auto oneCheck = [this](
|
|
char const* type,
|
|
std::uint32_t found,
|
|
std::uint32_t exp,
|
|
int line) {
|
|
if (found == exp)
|
|
pass();
|
|
else
|
|
{
|
|
std::stringstream ss;
|
|
ss << "Wrong " << type << " count. Found: " << found
|
|
<< "; Expected: " << exp;
|
|
fail(ss.str(), __FILE__, line);
|
|
}
|
|
};
|
|
oneCheck("alice owner", ownerCount(env, alice), aliceOwners, line);
|
|
oneCheck(
|
|
"alice minted", mintedCount(env, alice), aliceMinted, line);
|
|
oneCheck(
|
|
"alice burned", burnedCount(env, alice), aliceBurned, line);
|
|
oneCheck(
|
|
"minter owner", ownerCount(env, minter), minterOwners, line);
|
|
oneCheck(
|
|
"minter minted", mintedCount(env, minter), minterMinted, line);
|
|
oneCheck(
|
|
"minter burned", burnedCount(env, minter), minterBurned, line);
|
|
};
|
|
|
|
std::uint32_t nftSeq = 33;
|
|
|
|
// Pay minter almost enough to make the reserve for an NFT page.
|
|
env(pay(env.master, minter, XRP(50) - drops(1)));
|
|
env.close();
|
|
checkMintersOwnerMintedBurned(0, 33, nftSeq, 0, 0, 0, __LINE__);
|
|
|
|
// minter still does not have enough XRP for the reserve of an NFT page.
|
|
// Just for grins (and code coverage), minter mints NFTs that include
|
|
// a URI.
|
|
env(token::mint(minter),
|
|
token::issuer(alice),
|
|
token::uri("uri"),
|
|
ter(tecINSUFFICIENT_RESERVE));
|
|
env.close();
|
|
checkMintersOwnerMintedBurned(0, 33, nftSeq, 0, 0, 0, __LINE__);
|
|
|
|
// Pay minter enough to make the reserve for an NFT page.
|
|
env(pay(env.master, minter, drops(11)));
|
|
env.close();
|
|
|
|
// Now minter can mint an NFT for alice.
|
|
env(token::mint(minter), token::issuer(alice), token::uri("uri"));
|
|
env.close();
|
|
checkMintersOwnerMintedBurned(0, 34, nftSeq, 1, 0, 0, __LINE__);
|
|
|
|
// Minter should be able to mint an additional 31 NFTs for alice
|
|
// without any additional reserve requirements.
|
|
for (int i = 1; i < 32; ++i)
|
|
{
|
|
env(token::mint(minter), token::issuer(alice), token::uri("uri"));
|
|
checkMintersOwnerMintedBurned(0, i + 34, nftSeq, 1, 0, 0, __LINE__);
|
|
}
|
|
|
|
// Pay minter almost enough for the reserve of an additional NFT page.
|
|
env(pay(env.master, minter, XRP(50) + drops(319)));
|
|
env.close();
|
|
|
|
// That NFT page is full. Creating an additional NFT page requires
|
|
// additional reserve.
|
|
env(token::mint(minter),
|
|
token::issuer(alice),
|
|
token::uri("uri"),
|
|
ter(tecINSUFFICIENT_RESERVE));
|
|
env.close();
|
|
checkMintersOwnerMintedBurned(0, 65, nftSeq, 1, 0, 0, __LINE__);
|
|
|
|
// Pay minter enough for the reserve of an additional NFT page.
|
|
env(pay(env.master, minter, drops(11)));
|
|
env.close();
|
|
|
|
// Now minter can mint an NFT.
|
|
env(token::mint(minter), token::issuer(alice), token::uri("uri"));
|
|
env.close();
|
|
checkMintersOwnerMintedBurned(0, 66, nftSeq, 2, 0, 0, __LINE__);
|
|
|
|
// minter burns the NFTs she created.
|
|
while (nftSeq < 65)
|
|
{
|
|
env(token::burn(minter, token::getID(alice, 0, nftSeq++)));
|
|
env.close();
|
|
checkMintersOwnerMintedBurned(
|
|
0, 66, nftSeq, (65 - seq) ? 1 : 0, 0, 0, __LINE__);
|
|
}
|
|
|
|
// minter has one more NFT to burn. Should take her owner count to 0.
|
|
env(token::burn(minter, token::getID(alice, 0, nftSeq++)));
|
|
env.close();
|
|
checkMintersOwnerMintedBurned(0, 66, nftSeq, 0, 0, 0, __LINE__);
|
|
|
|
// minter burns a non-existent NFT.
|
|
env(token::burn(minter, token::getID(alice, 2009, 3)),
|
|
ter(tecNO_ENTRY));
|
|
env.close();
|
|
checkMintersOwnerMintedBurned(0, 66, nftSeq, 0, 0, 0, __LINE__);
|
|
}
|
|
|
|
void
|
|
testMintMaxTokens(FeatureBitset features)
|
|
{
|
|
// Make sure that an account cannot cause the sfMintedNFTokens
|
|
// field to wrap by minting more than 0xFFFF'FFFF tokens.
|
|
testcase("Mint max tokens");
|
|
|
|
using namespace test::jtx;
|
|
|
|
Account const alice{"alice"};
|
|
Env env{*this, features};
|
|
env.fund(XRP(1000), alice);
|
|
env.close();
|
|
|
|
// We're going to hack the ledger in order to avoid generating
|
|
// 4 billion or so NFTs. Because we're hacking the ledger we
|
|
// need alice's account to have non-zero sfMintedNFTokens and
|
|
// sfBurnedNFTokens fields. This prevents an exception when the
|
|
// AccountRoot template is applied.
|
|
{
|
|
uint256 const nftId0{token::getNextID(env, alice, 0u)};
|
|
env(token::mint(alice, 0u));
|
|
env.close();
|
|
|
|
env(token::burn(alice, nftId0));
|
|
env.close();
|
|
}
|
|
|
|
// Note that we're bypassing almost all of the ledger's safety
|
|
// checks with this modify() call. If you call close() between
|
|
// here and the end of the test all the effort will be lost.
|
|
env.app().openLedger().modify(
|
|
[&alice](OpenView& view, beast::Journal j) {
|
|
// Get the account root we want to hijack.
|
|
auto const sle = view.read(keylet::account(alice.id()));
|
|
if (!sle)
|
|
return false; // This would be really surprising!
|
|
|
|
// Just for sanity's sake we'll check that the current value
|
|
// of sfMintedNFTokens matches what we expect.
|
|
auto replacement = std::make_shared<SLE>(*sle, sle->key());
|
|
if (replacement->getFieldU32(sfMintedNFTokens) != 1)
|
|
return false; // Unexpected test conditions.
|
|
|
|
// Now replace sfMintedNFTokens with the largest valid value.
|
|
(*replacement)[sfMintedNFTokens] = 0xFFFF'FFFE;
|
|
view.rawReplace(replacement);
|
|
return true;
|
|
});
|
|
|
|
// See whether alice is at the boundary that causes an error.
|
|
env(token::mint(alice, 0u), ter(tesSUCCESS));
|
|
env(token::mint(alice, 0u), ter(tecMAX_SEQUENCE_REACHED));
|
|
}
|
|
|
|
void
|
|
testMintInvalid(FeatureBitset features)
|
|
{
|
|
// Explore many of the invalid ways to mint an NFT.
|
|
testcase("Mint invalid");
|
|
|
|
using namespace test::jtx;
|
|
|
|
Env env{*this, features};
|
|
Account const alice{"alice"};
|
|
Account const minter{"minter"};
|
|
|
|
// Fund alice and minter enough to exist, but not enough to meet
|
|
// the reserve for creating their first NFT. Account reserve for unit
|
|
// tests is 200 XRP, not 20.
|
|
env.fund(XRP(200), alice, minter);
|
|
env.close();
|
|
|
|
env(token::mint(alice, 0u), ter(tecINSUFFICIENT_RESERVE));
|
|
env.close();
|
|
|
|
// Fund alice enough to start minting NFTs.
|
|
env(pay(env.master, alice, XRP(1000)));
|
|
env.close();
|
|
|
|
//----------------------------------------------------------------------
|
|
// preflight
|
|
|
|
// Set a negative fee.
|
|
env(token::mint(alice, 0u),
|
|
fee(STAmount(10ull, true)),
|
|
ter(temBAD_FEE));
|
|
|
|
// Set an invalid flag.
|
|
env(token::mint(alice, 0u), txflags(0x00008000), ter(temINVALID_FLAG));
|
|
|
|
// Can't set a transfer fee if the NFT does not have the tfTRANSFERABLE
|
|
// flag set.
|
|
env(token::mint(alice, 0u),
|
|
token::xferFee(maxTransferFee),
|
|
ter(temMALFORMED));
|
|
|
|
// Set a bad transfer fee.
|
|
env(token::mint(alice, 0u),
|
|
token::xferFee(maxTransferFee + 1),
|
|
txflags(tfTransferable),
|
|
ter(temBAD_NFTOKEN_TRANSFER_FEE));
|
|
|
|
// Account can't also be issuer.
|
|
env(token::mint(alice, 0u), token::issuer(alice), ter(temMALFORMED));
|
|
|
|
// Invalid URI: zero length.
|
|
env(token::mint(alice, 0u), token::uri(""), ter(temMALFORMED));
|
|
|
|
// Invalid URI: too long.
|
|
env(token::mint(alice, 0u),
|
|
token::uri(std::string(maxTokenURILength + 1, 'q')),
|
|
ter(temMALFORMED));
|
|
|
|
//----------------------------------------------------------------------
|
|
// preflight
|
|
|
|
// Non-existent issuer.
|
|
env(token::mint(alice, 0u),
|
|
token::issuer(Account("demon")),
|
|
ter(tecNO_ISSUER));
|
|
|
|
//----------------------------------------------------------------------
|
|
// doApply
|
|
|
|
// Existent issuer, but not given minting permission
|
|
env(token::mint(minter, 0u),
|
|
token::issuer(alice),
|
|
ter(tecNO_PERMISSION));
|
|
}
|
|
|
|
void
|
|
testBurnInvalid(FeatureBitset features)
|
|
{
|
|
// Explore many of the invalid ways to burn an NFT.
|
|
testcase("Burn invalid");
|
|
|
|
using namespace test::jtx;
|
|
|
|
Env env{*this, features};
|
|
Account const alice{"alice"};
|
|
Account const buyer{"buyer"};
|
|
Account const minter{"minter"};
|
|
Account const gw("gw");
|
|
IOU const gwAUD(gw["AUD"]);
|
|
|
|
// Fund alice and minter enough to exist and create an NFT, but not
|
|
// enough to meet the reserve for creating their first NFTOffer.
|
|
// Account reserve for unit tests is 200 XRP, not 20.
|
|
env.fund(XRP(250), alice, buyer, minter, gw);
|
|
env.close();
|
|
BEAST_EXPECT(ownerCount(env, alice) == 0);
|
|
|
|
uint256 const nftAlice0ID =
|
|
token::getNextID(env, alice, 0, tfTransferable);
|
|
env(token::mint(alice, 0u), txflags(tfTransferable));
|
|
env.close();
|
|
BEAST_EXPECT(ownerCount(env, alice) == 1);
|
|
|
|
//----------------------------------------------------------------------
|
|
// preflight
|
|
|
|
// Set a negative fee.
|
|
env(token::burn(alice, nftAlice0ID),
|
|
fee(STAmount(10ull, true)),
|
|
ter(temBAD_FEE));
|
|
env.close();
|
|
BEAST_EXPECT(ownerCount(env, alice) == 1);
|
|
|
|
// Set an invalid flag.
|
|
env(token::burn(alice, nftAlice0ID),
|
|
txflags(0x00008000),
|
|
ter(temINVALID_FLAG));
|
|
env.close();
|
|
BEAST_EXPECT(ownerCount(env, buyer) == 0);
|
|
|
|
//----------------------------------------------------------------------
|
|
// preclaim
|
|
|
|
// Try to burn a token that doesn't exist.
|
|
env(token::burn(alice, token::getID(alice, 0, 1)), ter(tecNO_ENTRY));
|
|
env.close();
|
|
BEAST_EXPECT(ownerCount(env, buyer) == 0);
|
|
|
|
// Can't burn a token with many buy or sell offers. But that is
|
|
// verified in testManyNftOffers().
|
|
|
|
//----------------------------------------------------------------------
|
|
// doApply
|
|
}
|
|
|
|
void
|
|
testCreateOfferInvalid(FeatureBitset features)
|
|
{
|
|
testcase("Invalid NFT offer create");
|
|
|
|
using namespace test::jtx;
|
|
|
|
Env env{*this, features};
|
|
Account const alice{"alice"};
|
|
Account const buyer{"buyer"};
|
|
Account const gw("gw");
|
|
IOU const gwAUD(gw["AUD"]);
|
|
|
|
// Fund alice enough to exist and create an NFT, but not
|
|
// enough to meet the reserve for creating their first NFTOffer.
|
|
// Account reserve for unit tests is 200 XRP, not 20.
|
|
env.fund(XRP(250), alice, buyer, gw);
|
|
env.close();
|
|
BEAST_EXPECT(ownerCount(env, alice) == 0);
|
|
|
|
uint256 const nftAlice0ID =
|
|
token::getNextID(env, alice, 0, tfTransferable, 10);
|
|
env(token::mint(alice, 0u),
|
|
txflags(tfTransferable),
|
|
token::xferFee(10));
|
|
env.close();
|
|
BEAST_EXPECT(ownerCount(env, alice) == 1);
|
|
|
|
uint256 const nftXrpOnlyID =
|
|
token::getNextID(env, alice, 0, tfOnlyXRP | tfTransferable);
|
|
env(token::mint(alice, 0), txflags(tfOnlyXRP | tfTransferable));
|
|
env.close();
|
|
BEAST_EXPECT(ownerCount(env, alice) == 1);
|
|
|
|
uint256 nftNoXferID = token::getNextID(env, alice, 0);
|
|
env(token::mint(alice, 0));
|
|
env.close();
|
|
BEAST_EXPECT(ownerCount(env, alice) == 1);
|
|
|
|
//----------------------------------------------------------------------
|
|
// preflight
|
|
|
|
// buyer burns a fee, so they no longer have enough XRP to cover the
|
|
// reserve for a token offer.
|
|
env(noop(buyer));
|
|
env.close();
|
|
|
|
// buyer tries to create an NFTokenOffer, but doesn't have the reserve.
|
|
env(token::createOffer(buyer, nftAlice0ID, XRP(1000)),
|
|
token::owner(alice),
|
|
ter(tecINSUFFICIENT_RESERVE));
|
|
env.close();
|
|
BEAST_EXPECT(ownerCount(env, buyer) == 0);
|
|
|
|
// Set a negative fee.
|
|
env(token::createOffer(buyer, nftAlice0ID, XRP(1000)),
|
|
fee(STAmount(10ull, true)),
|
|
ter(temBAD_FEE));
|
|
env.close();
|
|
BEAST_EXPECT(ownerCount(env, buyer) == 0);
|
|
|
|
// Set an invalid flag.
|
|
env(token::createOffer(buyer, nftAlice0ID, XRP(1000)),
|
|
txflags(0x00008000),
|
|
ter(temINVALID_FLAG));
|
|
env.close();
|
|
BEAST_EXPECT(ownerCount(env, buyer) == 0);
|
|
|
|
// Set an invalid amount.
|
|
env(token::createOffer(buyer, nftXrpOnlyID, buyer["USD"](1)),
|
|
ter(temBAD_AMOUNT));
|
|
env(token::createOffer(buyer, nftAlice0ID, buyer["USD"](0)),
|
|
ter(temBAD_AMOUNT));
|
|
env(token::createOffer(buyer, nftXrpOnlyID, drops(0)),
|
|
ter(temBAD_AMOUNT));
|
|
env.close();
|
|
BEAST_EXPECT(ownerCount(env, buyer) == 0);
|
|
|
|
// Set a bad expiration.
|
|
env(token::createOffer(buyer, nftAlice0ID, buyer["USD"](1)),
|
|
token::expiration(0),
|
|
ter(temBAD_EXPIRATION));
|
|
env.close();
|
|
BEAST_EXPECT(ownerCount(env, buyer) == 0);
|
|
|
|
// Invalid Owner field and tfSellToken flag relationships.
|
|
// A buy offer must specify the owner.
|
|
env(token::createOffer(buyer, nftXrpOnlyID, XRP(1000)),
|
|
ter(temMALFORMED));
|
|
env.close();
|
|
BEAST_EXPECT(ownerCount(env, buyer) == 0);
|
|
|
|
// A sell offer must not specify the owner; the owner is implicit.
|
|
env(token::createOffer(alice, nftXrpOnlyID, XRP(1000)),
|
|
token::owner(alice),
|
|
txflags(tfSellNFToken),
|
|
ter(temMALFORMED));
|
|
env.close();
|
|
BEAST_EXPECT(ownerCount(env, alice) == 1);
|
|
|
|
// An owner may not offer to buy their own token.
|
|
env(token::createOffer(alice, nftXrpOnlyID, XRP(1000)),
|
|
token::owner(alice),
|
|
ter(temMALFORMED));
|
|
env.close();
|
|
BEAST_EXPECT(ownerCount(env, alice) == 1);
|
|
|
|
// The destination may not be the account submitting the transaction.
|
|
env(token::createOffer(alice, nftXrpOnlyID, XRP(1000)),
|
|
token::destination(alice),
|
|
txflags(tfSellNFToken),
|
|
ter(temMALFORMED));
|
|
env.close();
|
|
BEAST_EXPECT(ownerCount(env, alice) == 1);
|
|
|
|
// The destination must be an account already established in the ledger.
|
|
env(token::createOffer(alice, nftXrpOnlyID, XRP(1000)),
|
|
token::destination(Account("demon")),
|
|
txflags(tfSellNFToken),
|
|
ter(tecNO_DST));
|
|
env.close();
|
|
BEAST_EXPECT(ownerCount(env, alice) == 1);
|
|
|
|
//----------------------------------------------------------------------
|
|
// preclaim
|
|
|
|
// The new NFTokenOffer may not have passed its expiration time.
|
|
env(token::createOffer(buyer, nftXrpOnlyID, XRP(1000)),
|
|
token::owner(alice),
|
|
token::expiration(lastClose(env)),
|
|
ter(tecEXPIRED));
|
|
env.close();
|
|
BEAST_EXPECT(ownerCount(env, buyer) == 0);
|
|
|
|
// The nftID must be present in the ledger.
|
|
env(token::createOffer(buyer, token::getID(alice, 0, 1), XRP(1000)),
|
|
token::owner(alice),
|
|
ter(tecNO_ENTRY));
|
|
env.close();
|
|
BEAST_EXPECT(ownerCount(env, buyer) == 0);
|
|
|
|
// The nftID must be present in the ledger of a sell offer too.
|
|
env(token::createOffer(alice, token::getID(alice, 0, 1), XRP(1000)),
|
|
txflags(tfSellNFToken),
|
|
ter(tecNO_ENTRY));
|
|
env.close();
|
|
BEAST_EXPECT(ownerCount(env, buyer) == 0);
|
|
|
|
// buyer must have the funds to pay for their offer.
|
|
env(token::createOffer(buyer, nftAlice0ID, gwAUD(1000)),
|
|
token::owner(alice),
|
|
ter(tecNO_LINE));
|
|
env.close();
|
|
BEAST_EXPECT(ownerCount(env, buyer) == 0);
|
|
|
|
env(trust(buyer, gwAUD(1000)));
|
|
env.close();
|
|
BEAST_EXPECT(ownerCount(env, buyer) == 1);
|
|
env.close();
|
|
|
|
// Issuer (alice) must have a trust line for the offered funds.
|
|
env(token::createOffer(buyer, nftAlice0ID, gwAUD(1000)),
|
|
token::owner(alice),
|
|
ter(tecNO_LINE));
|
|
env.close();
|
|
BEAST_EXPECT(ownerCount(env, buyer) == 1);
|
|
|
|
// Give alice the needed trust line, but freeze it.
|
|
env(trust(gw, alice["AUD"](999), tfSetFreeze));
|
|
env.close();
|
|
|
|
// Issuer (alice) must have a trust line for the offered funds and
|
|
// the trust line may not be frozen.
|
|
env(token::createOffer(buyer, nftAlice0ID, gwAUD(1000)),
|
|
token::owner(alice),
|
|
ter(tecFROZEN));
|
|
env.close();
|
|
BEAST_EXPECT(ownerCount(env, buyer) == 1);
|
|
|
|
// Unfreeze alice's trustline.
|
|
env(trust(gw, alice["AUD"](999), tfClearFreeze));
|
|
env.close();
|
|
|
|
// Can't transfer the NFT if the transferable flag is not set.
|
|
env(token::createOffer(buyer, nftNoXferID, gwAUD(1000)),
|
|
token::owner(alice),
|
|
ter(tefNFTOKEN_IS_NOT_TRANSFERABLE));
|
|
env.close();
|
|
BEAST_EXPECT(ownerCount(env, buyer) == 1);
|
|
|
|
// Give buyer the needed trust line, but freeze it.
|
|
env(trust(gw, buyer["AUD"](999), tfSetFreeze));
|
|
env.close();
|
|
|
|
env(token::createOffer(buyer, nftAlice0ID, gwAUD(1000)),
|
|
token::owner(alice),
|
|
ter(tecFROZEN));
|
|
env.close();
|
|
BEAST_EXPECT(ownerCount(env, buyer) == 1);
|
|
|
|
// Unfreeze buyer's trust line, but buyer has no actual gwAUD.
|
|
// to cover the offer.
|
|
env(trust(gw, buyer["AUD"](999), tfClearFreeze));
|
|
env(trust(buyer, gwAUD(1000)));
|
|
env.close();
|
|
|
|
env(token::createOffer(buyer, nftAlice0ID, gwAUD(1000)),
|
|
token::owner(alice),
|
|
ter(tecUNFUNDED_OFFER));
|
|
env.close();
|
|
BEAST_EXPECT(ownerCount(env, buyer) == 1); // the trust line.
|
|
|
|
//----------------------------------------------------------------------
|
|
// doApply
|
|
|
|
// Give buyer almost enough AUD to cover the offer...
|
|
env(pay(gw, buyer, gwAUD(999)));
|
|
env.close();
|
|
|
|
// However buyer doesn't have enough XRP to cover the reserve for
|
|
// an NFT offer.
|
|
env(token::createOffer(buyer, nftAlice0ID, gwAUD(1000)),
|
|
token::owner(alice),
|
|
ter(tecINSUFFICIENT_RESERVE));
|
|
env.close();
|
|
BEAST_EXPECT(ownerCount(env, buyer) == 1);
|
|
|
|
// Give buyer almost enough XRP to cover the reserve.
|
|
env(pay(env.master, buyer, XRP(50) + drops(119)));
|
|
env.close();
|
|
|
|
env(token::createOffer(buyer, nftAlice0ID, gwAUD(1000)),
|
|
token::owner(alice),
|
|
ter(tecINSUFFICIENT_RESERVE));
|
|
env.close();
|
|
BEAST_EXPECT(ownerCount(env, buyer) == 1);
|
|
|
|
// Give buyer just enough XRP to cover the reserve for the offer.
|
|
env(pay(env.master, buyer, drops(11)));
|
|
env.close();
|
|
|
|
// We don't care whether the offer is fully funded until the offer is
|
|
// accepted. Success at last!
|
|
env(token::createOffer(buyer, nftAlice0ID, gwAUD(1000)),
|
|
token::owner(alice),
|
|
ter(tesSUCCESS));
|
|
env.close();
|
|
BEAST_EXPECT(ownerCount(env, buyer) == 2);
|
|
}
|
|
|
|
void
|
|
testCancelOfferInvalid(FeatureBitset features)
|
|
{
|
|
testcase("Invalid NFT offer cancel");
|
|
|
|
using namespace test::jtx;
|
|
|
|
Env env{*this, features};
|
|
Account const alice{"alice"};
|
|
Account const buyer{"buyer"};
|
|
Account const gw("gw");
|
|
IOU const gwAUD(gw["AUD"]);
|
|
|
|
env.fund(XRP(1000), alice, buyer, gw);
|
|
env.close();
|
|
BEAST_EXPECT(ownerCount(env, alice) == 0);
|
|
|
|
uint256 const nftAlice0ID =
|
|
token::getNextID(env, alice, 0, tfTransferable);
|
|
env(token::mint(alice, 0u), txflags(tfTransferable));
|
|
env.close();
|
|
BEAST_EXPECT(ownerCount(env, alice) == 1);
|
|
|
|
// This is the offer we'll try to cancel.
|
|
uint256 const buyerOfferIndex =
|
|
keylet::nftoffer(buyer, env.seq(buyer)).key;
|
|
env(token::createOffer(buyer, nftAlice0ID, XRP(1)),
|
|
token::owner(alice),
|
|
ter(tesSUCCESS));
|
|
env.close();
|
|
BEAST_EXPECT(ownerCount(env, buyer) == 1);
|
|
|
|
//----------------------------------------------------------------------
|
|
// preflight
|
|
|
|
// Set a negative fee.
|
|
env(token::cancelOffer(buyer, {buyerOfferIndex}),
|
|
fee(STAmount(10ull, true)),
|
|
ter(temBAD_FEE));
|
|
env.close();
|
|
BEAST_EXPECT(ownerCount(env, buyer) == 1);
|
|
|
|
// Set an invalid flag.
|
|
env(token::cancelOffer(buyer, {buyerOfferIndex}),
|
|
txflags(0x00008000),
|
|
ter(temINVALID_FLAG));
|
|
env.close();
|
|
BEAST_EXPECT(ownerCount(env, buyer) == 1);
|
|
|
|
// Empty list of tokens to delete.
|
|
{
|
|
Json::Value jv = token::cancelOffer(buyer);
|
|
jv[sfNFTokenOffers.jsonName] = Json::arrayValue;
|
|
env(jv, ter(temMALFORMED));
|
|
env.close();
|
|
BEAST_EXPECT(ownerCount(env, buyer) == 1);
|
|
}
|
|
|
|
// List of tokens to delete is too long.
|
|
{
|
|
std::vector<uint256> offers(
|
|
maxTokenOfferCancelCount + 1, buyerOfferIndex);
|
|
|
|
env(token::cancelOffer(buyer, offers), ter(temMALFORMED));
|
|
env.close();
|
|
BEAST_EXPECT(ownerCount(env, buyer) == 1);
|
|
}
|
|
|
|
// Duplicate entries are not allowed in the list of offers to cancel.
|
|
env(token::cancelOffer(buyer, {buyerOfferIndex, buyerOfferIndex}),
|
|
ter(temMALFORMED));
|
|
env.close();
|
|
BEAST_EXPECT(ownerCount(env, buyer) == 1);
|
|
|
|
// Provide neither offers to cancel nor a root index.
|
|
env(token::cancelOffer(buyer), ter(temMALFORMED));
|
|
env.close();
|
|
BEAST_EXPECT(ownerCount(env, buyer) == 1);
|
|
|
|
//----------------------------------------------------------------------
|
|
// preclaim
|
|
|
|
// Make a non-root directory that we can pass as a root index.
|
|
env(pay(env.master, gw, XRP(5000)));
|
|
env.close();
|
|
for (std::uint32_t i = 1; i < 34; ++i)
|
|
{
|
|
env(offer(gw, XRP(i), gwAUD(1)));
|
|
env.close();
|
|
}
|
|
|
|
{
|
|
// gw attempts to cancel a Check as through it is an NFTokenOffer.
|
|
auto const gwCheckId = keylet::check(gw, env.seq(gw)).key;
|
|
env(check::create(gw, env.master, XRP(300)));
|
|
env.close();
|
|
|
|
env(token::cancelOffer(gw, {gwCheckId}), ter(tecNO_PERMISSION));
|
|
env.close();
|
|
|
|
// Cancel the check so it doesn't mess up later tests.
|
|
env(check::cancel(gw, gwCheckId));
|
|
env.close();
|
|
}
|
|
|
|
// gw attempts to cancel an offer they don't have permission to cancel.
|
|
env(token::cancelOffer(gw, {buyerOfferIndex}), ter(tecNO_PERMISSION));
|
|
env.close();
|
|
BEAST_EXPECT(ownerCount(env, buyer) == 1);
|
|
|
|
//----------------------------------------------------------------------
|
|
// doApply
|
|
//
|
|
// The tefBAD_LEDGER conditions are too hard to test.
|
|
// But let's see a successful offer cancel.
|
|
env(token::cancelOffer(buyer, {buyerOfferIndex}));
|
|
env.close();
|
|
BEAST_EXPECT(ownerCount(env, buyer) == 0);
|
|
}
|
|
|
|
void
|
|
testAcceptOfferInvalid(FeatureBitset features)
|
|
{
|
|
testcase("Invalid NFT offer accept");
|
|
|
|
using namespace test::jtx;
|
|
|
|
Env env{*this, features};
|
|
Account const alice{"alice"};
|
|
Account const buyer{"buyer"};
|
|
Account const gw("gw");
|
|
IOU const gwAUD(gw["AUD"]);
|
|
|
|
env.fund(XRP(1000), alice, buyer, gw);
|
|
env.close();
|
|
BEAST_EXPECT(ownerCount(env, alice) == 0);
|
|
|
|
uint256 const nftAlice0ID =
|
|
token::getNextID(env, alice, 0, tfTransferable);
|
|
env(token::mint(alice, 0u), txflags(tfTransferable));
|
|
env.close();
|
|
BEAST_EXPECT(ownerCount(env, alice) == 1);
|
|
|
|
uint256 const nftXrpOnlyID =
|
|
token::getNextID(env, alice, 0, tfOnlyXRP | tfTransferable);
|
|
env(token::mint(alice, 0), txflags(tfOnlyXRP | tfTransferable));
|
|
env.close();
|
|
BEAST_EXPECT(ownerCount(env, alice) == 1);
|
|
|
|
uint256 nftNoXferID = token::getNextID(env, alice, 0);
|
|
env(token::mint(alice, 0));
|
|
env.close();
|
|
BEAST_EXPECT(ownerCount(env, alice) == 1);
|
|
|
|
// alice creates sell offers for her nfts.
|
|
uint256 const plainOfferIndex =
|
|
keylet::nftoffer(alice, env.seq(alice)).key;
|
|
env(token::createOffer(alice, nftAlice0ID, XRP(10)),
|
|
txflags(tfSellNFToken));
|
|
env.close();
|
|
BEAST_EXPECT(ownerCount(env, alice) == 2);
|
|
|
|
uint256 const audOfferIndex =
|
|
keylet::nftoffer(alice, env.seq(alice)).key;
|
|
env(token::createOffer(alice, nftAlice0ID, gwAUD(30)),
|
|
txflags(tfSellNFToken));
|
|
env.close();
|
|
BEAST_EXPECT(ownerCount(env, alice) == 3);
|
|
|
|
uint256 const xrpOnlyOfferIndex =
|
|
keylet::nftoffer(alice, env.seq(alice)).key;
|
|
env(token::createOffer(alice, nftXrpOnlyID, XRP(20)),
|
|
txflags(tfSellNFToken));
|
|
env.close();
|
|
BEAST_EXPECT(ownerCount(env, alice) == 4);
|
|
|
|
uint256 const noXferOfferIndex =
|
|
keylet::nftoffer(alice, env.seq(alice)).key;
|
|
env(token::createOffer(alice, nftNoXferID, XRP(30)),
|
|
txflags(tfSellNFToken));
|
|
env.close();
|
|
BEAST_EXPECT(ownerCount(env, alice) == 5);
|
|
|
|
// alice creates a sell offer that will expire soon.
|
|
uint256 const aliceExpOfferIndex =
|
|
keylet::nftoffer(alice, env.seq(alice)).key;
|
|
env(token::createOffer(alice, nftNoXferID, XRP(40)),
|
|
txflags(tfSellNFToken),
|
|
token::expiration(lastClose(env) + 5));
|
|
env.close();
|
|
BEAST_EXPECT(ownerCount(env, alice) == 6);
|
|
|
|
//----------------------------------------------------------------------
|
|
// preflight
|
|
|
|
// Set a negative fee.
|
|
env(token::acceptSellOffer(buyer, noXferOfferIndex),
|
|
fee(STAmount(10ull, true)),
|
|
ter(temBAD_FEE));
|
|
env.close();
|
|
BEAST_EXPECT(ownerCount(env, buyer) == 0);
|
|
|
|
// Set an invalid flag.
|
|
env(token::acceptSellOffer(buyer, noXferOfferIndex),
|
|
txflags(0x00008000),
|
|
ter(temINVALID_FLAG));
|
|
env.close();
|
|
BEAST_EXPECT(ownerCount(env, buyer) == 0);
|
|
|
|
// Supply nether an sfNFTokenBuyOffer nor an sfNFTokenSellOffer field.
|
|
{
|
|
Json::Value jv = token::acceptSellOffer(buyer, noXferOfferIndex);
|
|
jv.removeMember(sfNFTokenSellOffer.jsonName);
|
|
env(jv, ter(temMALFORMED));
|
|
env.close();
|
|
BEAST_EXPECT(ownerCount(env, buyer) == 0);
|
|
}
|
|
|
|
// A buy offer may not contain a sfNFTokenBrokerFee field.
|
|
{
|
|
Json::Value jv = token::acceptBuyOffer(buyer, noXferOfferIndex);
|
|
jv[sfNFTokenBrokerFee.jsonName] =
|
|
STAmount(500000).getJson(JsonOptions::none);
|
|
env(jv, ter(temMALFORMED));
|
|
env.close();
|
|
BEAST_EXPECT(ownerCount(env, buyer) == 0);
|
|
}
|
|
|
|
// A sell offer may not contain a sfNFTokenBrokerFee field.
|
|
{
|
|
Json::Value jv = token::acceptSellOffer(buyer, noXferOfferIndex);
|
|
jv[sfNFTokenBrokerFee.jsonName] =
|
|
STAmount(500000).getJson(JsonOptions::none);
|
|
env(jv, ter(temMALFORMED));
|
|
env.close();
|
|
BEAST_EXPECT(ownerCount(env, buyer) == 0);
|
|
}
|
|
|
|
// A brokered offer may not contain a negative or zero brokerFee.
|
|
env(token::brokerOffers(buyer, noXferOfferIndex, xrpOnlyOfferIndex),
|
|
token::brokerFee(gwAUD(0)),
|
|
ter(temMALFORMED));
|
|
env.close();
|
|
BEAST_EXPECT(ownerCount(env, buyer) == 0);
|
|
|
|
//----------------------------------------------------------------------
|
|
// preclaim
|
|
|
|
// The buy offer must be non-zero.
|
|
env(token::acceptBuyOffer(buyer, beast::zero),
|
|
ter(tecOBJECT_NOT_FOUND));
|
|
env.close();
|
|
BEAST_EXPECT(ownerCount(env, buyer) == 0);
|
|
|
|
// The buy offer must be present in the ledger.
|
|
uint256 const missingOfferIndex = keylet::nftoffer(alice, 1).key;
|
|
env(token::acceptBuyOffer(buyer, missingOfferIndex),
|
|
ter(tecOBJECT_NOT_FOUND));
|
|
env.close();
|
|
BEAST_EXPECT(ownerCount(env, buyer) == 0);
|
|
|
|
// The buy offer must not have expired.
|
|
env(token::acceptBuyOffer(buyer, aliceExpOfferIndex), ter(tecEXPIRED));
|
|
env.close();
|
|
BEAST_EXPECT(ownerCount(env, buyer) == 0);
|
|
|
|
// The sell offer must be non-zero.
|
|
env(token::acceptSellOffer(buyer, beast::zero),
|
|
ter(tecOBJECT_NOT_FOUND));
|
|
env.close();
|
|
BEAST_EXPECT(ownerCount(env, buyer) == 0);
|
|
|
|
// The sell offer must be present in the ledger.
|
|
env(token::acceptSellOffer(buyer, missingOfferIndex),
|
|
ter(tecOBJECT_NOT_FOUND));
|
|
env.close();
|
|
BEAST_EXPECT(ownerCount(env, buyer) == 0);
|
|
|
|
// The sell offer must not have expired.
|
|
env(token::acceptSellOffer(buyer, aliceExpOfferIndex), ter(tecEXPIRED));
|
|
env.close();
|
|
BEAST_EXPECT(ownerCount(env, buyer) == 0);
|
|
|
|
//----------------------------------------------------------------------
|
|
// preclaim brokered
|
|
|
|
// alice and buyer need trustlines before buyer can to create an
|
|
// offer for gwAUD.
|
|
env(trust(alice, gwAUD(1000)));
|
|
env(trust(buyer, gwAUD(1000)));
|
|
env.close();
|
|
env(pay(gw, buyer, gwAUD(30)));
|
|
env.close();
|
|
BEAST_EXPECT(ownerCount(env, alice) == 7);
|
|
BEAST_EXPECT(ownerCount(env, buyer) == 1);
|
|
|
|
// We're about to exercise offer brokering, so we need
|
|
// corresponding buy and sell offers.
|
|
{
|
|
// buyer creates a buy offer for one of alice's nfts.
|
|
uint256 const buyerOfferIndex =
|
|
keylet::nftoffer(buyer, env.seq(buyer)).key;
|
|
env(token::createOffer(buyer, nftAlice0ID, gwAUD(29)),
|
|
token::owner(alice));
|
|
env.close();
|
|
BEAST_EXPECT(ownerCount(env, buyer) == 2);
|
|
|
|
// gw attempts to broker offers that are not for the same token.
|
|
env(token::brokerOffers(gw, buyerOfferIndex, xrpOnlyOfferIndex),
|
|
ter(tecNFTOKEN_BUY_SELL_MISMATCH));
|
|
env.close();
|
|
BEAST_EXPECT(ownerCount(env, buyer) == 2);
|
|
|
|
// gw attempts to broker offers that are not for the same currency.
|
|
env(token::brokerOffers(gw, buyerOfferIndex, plainOfferIndex),
|
|
ter(tecNFTOKEN_BUY_SELL_MISMATCH));
|
|
env.close();
|
|
BEAST_EXPECT(ownerCount(env, buyer) == 2);
|
|
|
|
// In a brokered offer, the buyer must offer greater than or
|
|
// equal to the selling price.
|
|
env(token::brokerOffers(gw, buyerOfferIndex, audOfferIndex),
|
|
ter(tecINSUFFICIENT_PAYMENT));
|
|
env.close();
|
|
BEAST_EXPECT(ownerCount(env, buyer) == 2);
|
|
|
|
// Remove buyer's offer.
|
|
env(token::cancelOffer(buyer, {buyerOfferIndex}));
|
|
env.close();
|
|
BEAST_EXPECT(ownerCount(env, buyer) == 1);
|
|
}
|
|
{
|
|
// buyer creates a buy offer for one of alice's nfts.
|
|
uint256 const buyerOfferIndex =
|
|
keylet::nftoffer(buyer, env.seq(buyer)).key;
|
|
env(token::createOffer(buyer, nftAlice0ID, gwAUD(31)),
|
|
token::owner(alice));
|
|
env.close();
|
|
BEAST_EXPECT(ownerCount(env, buyer) == 2);
|
|
|
|
// Broker sets their fee in a denomination other than the one
|
|
// used by the offers
|
|
env(token::brokerOffers(gw, buyerOfferIndex, audOfferIndex),
|
|
token::brokerFee(XRP(40)),
|
|
ter(tecNFTOKEN_BUY_SELL_MISMATCH));
|
|
env.close();
|
|
BEAST_EXPECT(ownerCount(env, buyer) == 2);
|
|
|
|
// Broker fee way too big.
|
|
env(token::brokerOffers(gw, buyerOfferIndex, audOfferIndex),
|
|
token::brokerFee(gwAUD(31)),
|
|
ter(tecINSUFFICIENT_PAYMENT));
|
|
env.close();
|
|
BEAST_EXPECT(ownerCount(env, buyer) == 2);
|
|
|
|
// Broker fee is smaller, but still too big once the offer
|
|
// seller's minimum is taken into account.
|
|
env(token::brokerOffers(gw, buyerOfferIndex, audOfferIndex),
|
|
token::brokerFee(gwAUD(1.5)),
|
|
ter(tecINSUFFICIENT_PAYMENT));
|
|
env.close();
|
|
BEAST_EXPECT(ownerCount(env, buyer) == 2);
|
|
|
|
// Remove buyer's offer.
|
|
env(token::cancelOffer(buyer, {buyerOfferIndex}));
|
|
env.close();
|
|
BEAST_EXPECT(ownerCount(env, buyer) == 1);
|
|
}
|
|
//----------------------------------------------------------------------
|
|
// preclaim buy
|
|
{
|
|
// buyer creates a buy offer for one of alice's nfts.
|
|
uint256 const buyerOfferIndex =
|
|
keylet::nftoffer(buyer, env.seq(buyer)).key;
|
|
env(token::createOffer(buyer, nftAlice0ID, gwAUD(30)),
|
|
token::owner(alice));
|
|
env.close();
|
|
BEAST_EXPECT(ownerCount(env, buyer) == 2);
|
|
|
|
// Don't accept a buy offer if the sell flag is set.
|
|
env(token::acceptBuyOffer(buyer, plainOfferIndex),
|
|
ter(tecNFTOKEN_OFFER_TYPE_MISMATCH));
|
|
env.close();
|
|
BEAST_EXPECT(ownerCount(env, alice) == 7);
|
|
|
|
// An account can't accept its own offer.
|
|
env(token::acceptBuyOffer(buyer, buyerOfferIndex),
|
|
ter(tecCANT_ACCEPT_OWN_NFTOKEN_OFFER));
|
|
env.close();
|
|
BEAST_EXPECT(ownerCount(env, buyer) == 2);
|
|
|
|
// An offer acceptor must have enough funds to pay for the offer.
|
|
env(pay(buyer, gw, gwAUD(30)));
|
|
env.close();
|
|
BEAST_EXPECT(env.balance(buyer, gwAUD) == gwAUD(0));
|
|
env(token::acceptBuyOffer(alice, buyerOfferIndex),
|
|
ter(tecINSUFFICIENT_FUNDS));
|
|
env.close();
|
|
BEAST_EXPECT(ownerCount(env, buyer) == 2);
|
|
|
|
// alice gives her NFT to gw, so alice no longer owns nftAlice0.
|
|
{
|
|
uint256 const offerIndex =
|
|
keylet::nftoffer(alice, env.seq(alice)).key;
|
|
env(token::createOffer(alice, nftAlice0ID, XRP(0)),
|
|
txflags(tfSellNFToken));
|
|
env.close();
|
|
env(token::acceptSellOffer(gw, offerIndex));
|
|
env.close();
|
|
BEAST_EXPECT(ownerCount(env, alice) == 7);
|
|
}
|
|
env(pay(gw, buyer, gwAUD(30)));
|
|
env.close();
|
|
|
|
// alice can't accept a buy offer for an NFT she no longer owns.
|
|
env(token::acceptBuyOffer(alice, buyerOfferIndex),
|
|
ter(tecNO_PERMISSION));
|
|
env.close();
|
|
BEAST_EXPECT(ownerCount(env, buyer) == 2);
|
|
|
|
// Remove buyer's offer.
|
|
env(token::cancelOffer(buyer, {buyerOfferIndex}));
|
|
env.close();
|
|
BEAST_EXPECT(ownerCount(env, buyer) == 1);
|
|
}
|
|
//----------------------------------------------------------------------
|
|
// preclaim sell
|
|
{
|
|
// buyer creates a buy offer for one of alice's nfts.
|
|
uint256 const buyerOfferIndex =
|
|
keylet::nftoffer(buyer, env.seq(buyer)).key;
|
|
env(token::createOffer(buyer, nftXrpOnlyID, XRP(30)),
|
|
token::owner(alice));
|
|
env.close();
|
|
BEAST_EXPECT(ownerCount(env, buyer) == 2);
|
|
|
|
// Don't accept a sell offer without the sell flag set.
|
|
env(token::acceptSellOffer(alice, buyerOfferIndex),
|
|
ter(tecNFTOKEN_OFFER_TYPE_MISMATCH));
|
|
env.close();
|
|
BEAST_EXPECT(ownerCount(env, alice) == 7);
|
|
|
|
// An account can't accept its own offer.
|
|
env(token::acceptSellOffer(alice, plainOfferIndex),
|
|
ter(tecCANT_ACCEPT_OWN_NFTOKEN_OFFER));
|
|
env.close();
|
|
BEAST_EXPECT(ownerCount(env, buyer) == 2);
|
|
|
|
// The seller must currently be in possession of the token they
|
|
// are selling. alice gave nftAlice0ID to gw.
|
|
env(token::acceptSellOffer(buyer, plainOfferIndex),
|
|
ter(tecNO_PERMISSION));
|
|
env.close();
|
|
BEAST_EXPECT(ownerCount(env, buyer) == 2);
|
|
|
|
// gw gives nftAlice0ID back to alice. That allows us to check
|
|
// buyer attempting to accept one of alice's offers with
|
|
// insufficient funds.
|
|
{
|
|
uint256 const offerIndex =
|
|
keylet::nftoffer(gw, env.seq(gw)).key;
|
|
env(token::createOffer(gw, nftAlice0ID, XRP(0)),
|
|
txflags(tfSellNFToken));
|
|
env.close();
|
|
env(token::acceptSellOffer(alice, offerIndex));
|
|
env.close();
|
|
BEAST_EXPECT(ownerCount(env, alice) == 7);
|
|
}
|
|
env(pay(buyer, gw, gwAUD(30)));
|
|
env.close();
|
|
BEAST_EXPECT(env.balance(buyer, gwAUD) == gwAUD(0));
|
|
env(token::acceptSellOffer(buyer, audOfferIndex),
|
|
ter(tecINSUFFICIENT_FUNDS));
|
|
env.close();
|
|
BEAST_EXPECT(ownerCount(env, buyer) == 2);
|
|
}
|
|
|
|
//----------------------------------------------------------------------
|
|
// doApply
|
|
//
|
|
// As far as I can see none of the failure modes are accessible as
|
|
// long as the preflight and preclaim conditions are met.
|
|
}
|
|
|
|
void
|
|
testMintFlagBurnable(FeatureBitset features)
|
|
{
|
|
// Exercise NFTs with flagBurnable set and not set.
|
|
testcase("Mint flagBurnable");
|
|
|
|
using namespace test::jtx;
|
|
|
|
Env env{*this, features};
|
|
Account const alice{"alice"};
|
|
Account const buyer{"buyer"};
|
|
Account const minter1{"minter1"};
|
|
Account const minter2{"minter2"};
|
|
|
|
env.fund(XRP(1000), alice, buyer, minter1, minter2);
|
|
env.close();
|
|
BEAST_EXPECT(ownerCount(env, alice) == 0);
|
|
|
|
// alice selects minter as her minter.
|
|
env(token::setMinter(alice, minter1));
|
|
env.close();
|
|
|
|
// A lambda that...
|
|
// 1. creates an alice nft
|
|
// 2. minted by minter and
|
|
// 3. transfers that nft to buyer.
|
|
auto nftToBuyer = [&env, &alice, &minter1, &buyer](
|
|
std::uint32_t flags) {
|
|
uint256 const nftID{token::getNextID(env, alice, 0u, flags)};
|
|
env(token::mint(minter1, 0u), token::issuer(alice), txflags(flags));
|
|
env.close();
|
|
|
|
uint256 const offerIndex =
|
|
keylet::nftoffer(minter1, env.seq(minter1)).key;
|
|
env(token::createOffer(minter1, nftID, XRP(0)),
|
|
txflags(tfSellNFToken));
|
|
env.close();
|
|
|
|
env(token::acceptSellOffer(buyer, offerIndex));
|
|
env.close();
|
|
|
|
return nftID;
|
|
};
|
|
|
|
// An NFT without flagBurnable can only be burned by its owner.
|
|
{
|
|
uint256 const noBurnID = nftToBuyer(0);
|
|
env(token::burn(alice, noBurnID),
|
|
token::owner(buyer),
|
|
ter(tecNO_PERMISSION));
|
|
env.close();
|
|
env(token::burn(minter1, noBurnID),
|
|
token::owner(buyer),
|
|
ter(tecNO_PERMISSION));
|
|
env.close();
|
|
env(token::burn(minter2, noBurnID),
|
|
token::owner(buyer),
|
|
ter(tecNO_PERMISSION));
|
|
env.close();
|
|
|
|
BEAST_EXPECT(ownerCount(env, buyer) == 1);
|
|
env(token::burn(buyer, noBurnID), token::owner(buyer));
|
|
env.close();
|
|
BEAST_EXPECT(ownerCount(env, buyer) == 0);
|
|
}
|
|
// An NFT with flagBurnable can be burned by the issuer.
|
|
{
|
|
uint256 const burnableID = nftToBuyer(tfBurnable);
|
|
env(token::burn(minter2, burnableID),
|
|
token::owner(buyer),
|
|
ter(tecNO_PERMISSION));
|
|
env.close();
|
|
|
|
BEAST_EXPECT(ownerCount(env, buyer) == 1);
|
|
env(token::burn(alice, burnableID), token::owner(buyer));
|
|
env.close();
|
|
BEAST_EXPECT(ownerCount(env, buyer) == 0);
|
|
}
|
|
// An NFT with flagBurnable can be burned by the owner.
|
|
{
|
|
uint256 const burnableID = nftToBuyer(tfBurnable);
|
|
BEAST_EXPECT(ownerCount(env, buyer) == 1);
|
|
env(token::burn(buyer, burnableID));
|
|
env.close();
|
|
BEAST_EXPECT(ownerCount(env, buyer) == 0);
|
|
}
|
|
// An NFT with flagBurnable can be burned by the minter.
|
|
{
|
|
uint256 const burnableID = nftToBuyer(tfBurnable);
|
|
BEAST_EXPECT(ownerCount(env, buyer) == 1);
|
|
env(token::burn(buyer, burnableID), token::owner(buyer));
|
|
env.close();
|
|
BEAST_EXPECT(ownerCount(env, buyer) == 0);
|
|
}
|
|
// An nft with flagBurnable may be burned by the issuers' minter,
|
|
// who may not be the original minter.
|
|
{
|
|
uint256 const burnableID = nftToBuyer(tfBurnable);
|
|
BEAST_EXPECT(ownerCount(env, buyer) == 1);
|
|
|
|
env(token::setMinter(alice, minter2));
|
|
env.close();
|
|
|
|
// minter1 is no longer alice's minter, so no longer has
|
|
// permisson to burn alice's nfts.
|
|
env(token::burn(minter1, burnableID),
|
|
token::owner(buyer),
|
|
ter(tecNO_PERMISSION));
|
|
env.close();
|
|
BEAST_EXPECT(ownerCount(env, buyer) == 1);
|
|
|
|
// minter2, however, can burn alice's nfts.
|
|
env(token::burn(minter2, burnableID), token::owner(buyer));
|
|
env.close();
|
|
BEAST_EXPECT(ownerCount(env, buyer) == 0);
|
|
}
|
|
}
|
|
|
|
void
|
|
testMintFlagOnlyXRP(FeatureBitset features)
|
|
{
|
|
// Exercise NFTs with flagOnlyXRP set and not set.
|
|
testcase("Mint flagOnlyXRP");
|
|
|
|
using namespace test::jtx;
|
|
|
|
Env env{*this, features};
|
|
Account const alice{"alice"};
|
|
Account const buyer{"buyer"};
|
|
Account const gw("gw");
|
|
IOU const gwAUD(gw["AUD"]);
|
|
|
|
// Set trust lines so alice and buyer can use gwAUD.
|
|
env.fund(XRP(1000), alice, buyer, gw);
|
|
env.close();
|
|
env(trust(alice, gwAUD(1000)));
|
|
env(trust(buyer, gwAUD(1000)));
|
|
env.close();
|
|
env(pay(gw, buyer, gwAUD(100)));
|
|
|
|
// Don't set flagOnlyXRP and offers can be made with IOUs.
|
|
{
|
|
uint256 const nftIOUsOkayID{
|
|
token::getNextID(env, alice, 0u, tfTransferable)};
|
|
env(token::mint(alice, 0u), txflags(tfTransferable));
|
|
env.close();
|
|
|
|
BEAST_EXPECT(ownerCount(env, alice) == 2);
|
|
uint256 const aliceOfferIndex =
|
|
keylet::nftoffer(alice, env.seq(alice)).key;
|
|
env(token::createOffer(alice, nftIOUsOkayID, gwAUD(50)),
|
|
txflags(tfSellNFToken));
|
|
env.close();
|
|
BEAST_EXPECT(ownerCount(env, alice) == 3);
|
|
|
|
BEAST_EXPECT(ownerCount(env, buyer) == 1);
|
|
uint256 const buyerOfferIndex =
|
|
keylet::nftoffer(buyer, env.seq(buyer)).key;
|
|
env(token::createOffer(buyer, nftIOUsOkayID, gwAUD(50)),
|
|
token::owner(alice));
|
|
env.close();
|
|
BEAST_EXPECT(ownerCount(env, buyer) == 2);
|
|
|
|
// Cancel the two offers just to be tidy.
|
|
env(token::cancelOffer(alice, {aliceOfferIndex}));
|
|
env(token::cancelOffer(buyer, {buyerOfferIndex}));
|
|
env.close();
|
|
BEAST_EXPECT(ownerCount(env, alice) == 2);
|
|
BEAST_EXPECT(ownerCount(env, buyer) == 1);
|
|
|
|
// Also burn alice's nft.
|
|
env(token::burn(alice, nftIOUsOkayID));
|
|
env.close();
|
|
BEAST_EXPECT(ownerCount(env, alice) == 1);
|
|
}
|
|
|
|
// Set flagOnlyXRP and offers using IOUs are rejected.
|
|
{
|
|
uint256 const nftOnlyXRPID{
|
|
token::getNextID(env, alice, 0u, tfOnlyXRP | tfTransferable)};
|
|
env(token::mint(alice, 0u), txflags(tfOnlyXRP | tfTransferable));
|
|
env.close();
|
|
|
|
BEAST_EXPECT(ownerCount(env, alice) == 2);
|
|
env(token::createOffer(alice, nftOnlyXRPID, gwAUD(50)),
|
|
txflags(tfSellNFToken),
|
|
ter(temBAD_AMOUNT));
|
|
env.close();
|
|
BEAST_EXPECT(ownerCount(env, alice) == 2);
|
|
|
|
BEAST_EXPECT(ownerCount(env, buyer) == 1);
|
|
env(token::createOffer(buyer, nftOnlyXRPID, gwAUD(50)),
|
|
token::owner(alice),
|
|
ter(temBAD_AMOUNT));
|
|
env.close();
|
|
BEAST_EXPECT(ownerCount(env, buyer) == 1);
|
|
|
|
// However offers for XRP are okay.
|
|
BEAST_EXPECT(ownerCount(env, alice) == 2);
|
|
env(token::createOffer(alice, nftOnlyXRPID, XRP(60)),
|
|
txflags(tfSellNFToken));
|
|
env.close();
|
|
BEAST_EXPECT(ownerCount(env, alice) == 3);
|
|
|
|
BEAST_EXPECT(ownerCount(env, buyer) == 1);
|
|
env(token::createOffer(buyer, nftOnlyXRPID, XRP(60)),
|
|
token::owner(alice));
|
|
env.close();
|
|
BEAST_EXPECT(ownerCount(env, buyer) == 2);
|
|
}
|
|
}
|
|
|
|
void
|
|
testMintFlagCreateTrustLine(FeatureBitset features)
|
|
{
|
|
// Exercise NFTs with flagCreateTrustLines set and not set.
|
|
testcase("Mint flagCreateTrustLines");
|
|
|
|
using namespace test::jtx;
|
|
|
|
Account const alice{"alice"};
|
|
Account const becky{"becky"};
|
|
Account const cheri{"cheri"};
|
|
Account const gw("gw");
|
|
IOU const gwAUD(gw["AUD"]);
|
|
IOU const gwCAD(gw["CAD"]);
|
|
IOU const gwEUR(gw["EUR"]);
|
|
|
|
// The behavior of this test changes dramatically based on the
|
|
// presence (or absence) of the fixRemoveNFTokenAutoTrustLine
|
|
// amendment. So we test both cases here.
|
|
for (auto const& tweakedFeatures :
|
|
{features - fixRemoveNFTokenAutoTrustLine,
|
|
features | fixRemoveNFTokenAutoTrustLine})
|
|
{
|
|
Env env{*this, tweakedFeatures};
|
|
env.fund(XRP(1000), alice, becky, cheri, gw);
|
|
env.close();
|
|
|
|
// Set trust lines so becky and cheri can use gw's currency.
|
|
env(trust(becky, gwAUD(1000)));
|
|
env(trust(cheri, gwAUD(1000)));
|
|
env(trust(becky, gwCAD(1000)));
|
|
env(trust(cheri, gwCAD(1000)));
|
|
env(trust(becky, gwEUR(1000)));
|
|
env(trust(cheri, gwEUR(1000)));
|
|
env.close();
|
|
env(pay(gw, becky, gwAUD(500)));
|
|
env(pay(gw, becky, gwCAD(500)));
|
|
env(pay(gw, becky, gwEUR(500)));
|
|
env(pay(gw, cheri, gwAUD(500)));
|
|
env(pay(gw, cheri, gwCAD(500)));
|
|
env.close();
|
|
|
|
// An nft without flagCreateTrustLines but with a non-zero transfer
|
|
// fee will not allow creating offers that use IOUs for payment.
|
|
for (std::uint32_t xferFee : {0, 1})
|
|
{
|
|
uint256 const nftNoAutoTrustID{
|
|
token::getNextID(env, alice, 0u, tfTransferable, xferFee)};
|
|
env(token::mint(alice, 0u),
|
|
token::xferFee(xferFee),
|
|
txflags(tfTransferable));
|
|
env.close();
|
|
|
|
// becky buys the nft for 1 drop.
|
|
uint256 const beckyBuyOfferIndex =
|
|
keylet::nftoffer(becky, env.seq(becky)).key;
|
|
env(token::createOffer(becky, nftNoAutoTrustID, drops(1)),
|
|
token::owner(alice));
|
|
env.close();
|
|
env(token::acceptBuyOffer(alice, beckyBuyOfferIndex));
|
|
env.close();
|
|
|
|
// becky attempts to sell the nft for AUD.
|
|
TER const createOfferTER =
|
|
xferFee ? TER(tecNO_LINE) : TER(tesSUCCESS);
|
|
uint256 const beckyOfferIndex =
|
|
keylet::nftoffer(becky, env.seq(becky)).key;
|
|
env(token::createOffer(becky, nftNoAutoTrustID, gwAUD(100)),
|
|
txflags(tfSellNFToken),
|
|
ter(createOfferTER));
|
|
env.close();
|
|
|
|
// cheri offers to buy the nft for CAD.
|
|
uint256 const cheriOfferIndex =
|
|
keylet::nftoffer(cheri, env.seq(cheri)).key;
|
|
env(token::createOffer(cheri, nftNoAutoTrustID, gwCAD(100)),
|
|
token::owner(becky),
|
|
ter(createOfferTER));
|
|
env.close();
|
|
|
|
// To keep things tidy, cancel the offers.
|
|
env(token::cancelOffer(becky, {beckyOfferIndex}));
|
|
env(token::cancelOffer(cheri, {cheriOfferIndex}));
|
|
env.close();
|
|
}
|
|
// An nft with flagCreateTrustLines but with a non-zero transfer
|
|
// fee allows transfers using IOUs for payment.
|
|
{
|
|
std::uint16_t transferFee = 10000; // 10%
|
|
|
|
uint256 const nftAutoTrustID{token::getNextID(
|
|
env, alice, 0u, tfTransferable | tfTrustLine, transferFee)};
|
|
|
|
// If the fixRemoveNFTokenAutoTrustLine amendment is active
|
|
// then this transaction fails.
|
|
{
|
|
TER const mintTER =
|
|
tweakedFeatures[fixRemoveNFTokenAutoTrustLine]
|
|
? static_cast<TER>(temINVALID_FLAG)
|
|
: static_cast<TER>(tesSUCCESS);
|
|
|
|
env(token::mint(alice, 0u),
|
|
token::xferFee(transferFee),
|
|
txflags(tfTransferable | tfTrustLine),
|
|
ter(mintTER));
|
|
env.close();
|
|
|
|
// If fixRemoveNFTokenAutoTrustLine is active the rest
|
|
// of this test falls on its face.
|
|
if (tweakedFeatures[fixRemoveNFTokenAutoTrustLine])
|
|
break;
|
|
}
|
|
// becky buys the nft for 1 drop.
|
|
uint256 const beckyBuyOfferIndex =
|
|
keylet::nftoffer(becky, env.seq(becky)).key;
|
|
env(token::createOffer(becky, nftAutoTrustID, drops(1)),
|
|
token::owner(alice));
|
|
env.close();
|
|
env(token::acceptBuyOffer(alice, beckyBuyOfferIndex));
|
|
env.close();
|
|
|
|
// becky sells the nft for AUD.
|
|
uint256 const beckySellOfferIndex =
|
|
keylet::nftoffer(becky, env.seq(becky)).key;
|
|
env(token::createOffer(becky, nftAutoTrustID, gwAUD(100)),
|
|
txflags(tfSellNFToken));
|
|
env.close();
|
|
env(token::acceptSellOffer(cheri, beckySellOfferIndex));
|
|
env.close();
|
|
|
|
// alice should now have a trust line for gwAUD.
|
|
BEAST_EXPECT(env.balance(alice, gwAUD) == gwAUD(10));
|
|
|
|
// becky buys the nft back for CAD.
|
|
uint256 const beckyBuyBackOfferIndex =
|
|
keylet::nftoffer(becky, env.seq(becky)).key;
|
|
env(token::createOffer(becky, nftAutoTrustID, gwCAD(50)),
|
|
token::owner(cheri));
|
|
env.close();
|
|
env(token::acceptBuyOffer(cheri, beckyBuyBackOfferIndex));
|
|
env.close();
|
|
|
|
// alice should now have a trust line for gwAUD and gwCAD.
|
|
BEAST_EXPECT(env.balance(alice, gwAUD) == gwAUD(10));
|
|
BEAST_EXPECT(env.balance(alice, gwCAD) == gwCAD(5));
|
|
}
|
|
// Now that alice has trust lines preestablished, an nft without
|
|
// flagCreateTrustLines will work for preestablished trust lines.
|
|
{
|
|
std::uint16_t transferFee = 5000; // 5%
|
|
uint256 const nftNoAutoTrustID{token::getNextID(
|
|
env, alice, 0u, tfTransferable, transferFee)};
|
|
env(token::mint(alice, 0u),
|
|
token::xferFee(transferFee),
|
|
txflags(tfTransferable));
|
|
env.close();
|
|
|
|
// alice sells the nft using AUD.
|
|
uint256 const aliceSellOfferIndex =
|
|
keylet::nftoffer(alice, env.seq(alice)).key;
|
|
env(token::createOffer(alice, nftNoAutoTrustID, gwAUD(200)),
|
|
txflags(tfSellNFToken));
|
|
env.close();
|
|
env(token::acceptSellOffer(cheri, aliceSellOfferIndex));
|
|
env.close();
|
|
|
|
// alice should now have AUD(210):
|
|
// o 200 for this sale and
|
|
// o 10 for the previous sale's fee.
|
|
BEAST_EXPECT(env.balance(alice, gwAUD) == gwAUD(210));
|
|
|
|
// cheri can't sell the NFT for EUR, but can for CAD.
|
|
env(token::createOffer(cheri, nftNoAutoTrustID, gwEUR(50)),
|
|
txflags(tfSellNFToken),
|
|
ter(tecNO_LINE));
|
|
env.close();
|
|
uint256 const cheriSellOfferIndex =
|
|
keylet::nftoffer(cheri, env.seq(cheri)).key;
|
|
env(token::createOffer(cheri, nftNoAutoTrustID, gwCAD(100)),
|
|
txflags(tfSellNFToken));
|
|
env.close();
|
|
env(token::acceptSellOffer(becky, cheriSellOfferIndex));
|
|
env.close();
|
|
|
|
// alice should now have CAD(10):
|
|
// o 5 from this sale's fee and
|
|
// o 5 for the previous sale's fee.
|
|
BEAST_EXPECT(env.balance(alice, gwCAD) == gwCAD(10));
|
|
}
|
|
}
|
|
}
|
|
|
|
void
|
|
testMintFlagTransferable(FeatureBitset features)
|
|
{
|
|
// Exercise NFTs with flagTransferable set and not set.
|
|
testcase("Mint flagTransferable");
|
|
|
|
using namespace test::jtx;
|
|
|
|
Env env{*this, features};
|
|
|
|
Account const alice{"alice"};
|
|
Account const becky{"becky"};
|
|
Account const minter{"minter"};
|
|
|
|
env.fund(XRP(1000), alice, becky, minter);
|
|
env.close();
|
|
|
|
// First try an nft made by alice without flagTransferable set.
|
|
{
|
|
BEAST_EXPECT(ownerCount(env, alice) == 0);
|
|
uint256 const nftAliceNoTransferID{
|
|
token::getNextID(env, alice, 0u)};
|
|
env(token::mint(alice, 0u), token::xferFee(0));
|
|
env.close();
|
|
BEAST_EXPECT(ownerCount(env, alice) == 1);
|
|
|
|
// becky tries to offer to buy alice's nft.
|
|
BEAST_EXPECT(ownerCount(env, becky) == 0);
|
|
env(token::createOffer(becky, nftAliceNoTransferID, XRP(20)),
|
|
token::owner(alice),
|
|
ter(tefNFTOKEN_IS_NOT_TRANSFERABLE));
|
|
|
|
// alice offers to sell the nft and becky accepts the offer.
|
|
uint256 const aliceSellOfferIndex =
|
|
keylet::nftoffer(alice, env.seq(alice)).key;
|
|
env(token::createOffer(alice, nftAliceNoTransferID, XRP(20)),
|
|
txflags(tfSellNFToken));
|
|
env.close();
|
|
env(token::acceptSellOffer(becky, aliceSellOfferIndex));
|
|
env.close();
|
|
BEAST_EXPECT(ownerCount(env, alice) == 0);
|
|
BEAST_EXPECT(ownerCount(env, becky) == 1);
|
|
|
|
// becky tries to offer the nft for sale.
|
|
env(token::createOffer(becky, nftAliceNoTransferID, XRP(21)),
|
|
txflags(tfSellNFToken),
|
|
ter(tefNFTOKEN_IS_NOT_TRANSFERABLE));
|
|
env.close();
|
|
BEAST_EXPECT(ownerCount(env, alice) == 0);
|
|
BEAST_EXPECT(ownerCount(env, becky) == 1);
|
|
|
|
// becky tries to offer the nft for sale with alice as the
|
|
// destination. That also doesn't work.
|
|
env(token::createOffer(becky, nftAliceNoTransferID, XRP(21)),
|
|
txflags(tfSellNFToken),
|
|
token::destination(alice),
|
|
ter(tefNFTOKEN_IS_NOT_TRANSFERABLE));
|
|
env.close();
|
|
BEAST_EXPECT(ownerCount(env, alice) == 0);
|
|
BEAST_EXPECT(ownerCount(env, becky) == 1);
|
|
|
|
// alice offers to buy the nft back from becky. becky accepts
|
|
// the offer.
|
|
uint256 const aliceBuyOfferIndex =
|
|
keylet::nftoffer(alice, env.seq(alice)).key;
|
|
env(token::createOffer(alice, nftAliceNoTransferID, XRP(22)),
|
|
token::owner(becky));
|
|
env.close();
|
|
env(token::acceptBuyOffer(becky, aliceBuyOfferIndex));
|
|
env.close();
|
|
BEAST_EXPECT(ownerCount(env, alice) == 1);
|
|
BEAST_EXPECT(ownerCount(env, becky) == 0);
|
|
|
|
// alice burns her nft so accounting is simpler below.
|
|
env(token::burn(alice, nftAliceNoTransferID));
|
|
env.close();
|
|
BEAST_EXPECT(ownerCount(env, alice) == 0);
|
|
BEAST_EXPECT(ownerCount(env, becky) == 0);
|
|
}
|
|
// Try an nft minted by minter for alice without flagTransferable set.
|
|
{
|
|
env(token::setMinter(alice, minter));
|
|
env.close();
|
|
|
|
BEAST_EXPECT(ownerCount(env, minter) == 0);
|
|
uint256 const nftMinterNoTransferID{
|
|
token::getNextID(env, alice, 0u)};
|
|
env(token::mint(minter), token::issuer(alice));
|
|
env.close();
|
|
BEAST_EXPECT(ownerCount(env, minter) == 1);
|
|
|
|
// becky tries to offer to buy minter's nft.
|
|
BEAST_EXPECT(ownerCount(env, becky) == 0);
|
|
env(token::createOffer(becky, nftMinterNoTransferID, XRP(20)),
|
|
token::owner(minter),
|
|
ter(tefNFTOKEN_IS_NOT_TRANSFERABLE));
|
|
env.close();
|
|
BEAST_EXPECT(ownerCount(env, becky) == 0);
|
|
|
|
// alice removes authorization of minter.
|
|
env(token::clearMinter(alice));
|
|
env.close();
|
|
|
|
// minter tries to offer their nft for sale.
|
|
BEAST_EXPECT(ownerCount(env, minter) == 1);
|
|
env(token::createOffer(minter, nftMinterNoTransferID, XRP(21)),
|
|
txflags(tfSellNFToken),
|
|
ter(tefNFTOKEN_IS_NOT_TRANSFERABLE));
|
|
env.close();
|
|
BEAST_EXPECT(ownerCount(env, minter) == 1);
|
|
|
|
// Let enough ledgers pass that old transactions are no longer
|
|
// retried, then alice gives authorization back to minter.
|
|
for (int i = 0; i < 10; ++i)
|
|
env.close();
|
|
|
|
env(token::setMinter(alice, minter));
|
|
env.close();
|
|
BEAST_EXPECT(ownerCount(env, minter) == 1);
|
|
|
|
// minter successfully offers their nft for sale.
|
|
BEAST_EXPECT(ownerCount(env, minter) == 1);
|
|
uint256 const minterSellOfferIndex =
|
|
keylet::nftoffer(minter, env.seq(minter)).key;
|
|
env(token::createOffer(minter, nftMinterNoTransferID, XRP(22)),
|
|
txflags(tfSellNFToken));
|
|
env.close();
|
|
BEAST_EXPECT(ownerCount(env, minter) == 2);
|
|
|
|
// alice removes authorization of minter so we can see whether
|
|
// minter's pre-existing offer still works.
|
|
env(token::clearMinter(alice));
|
|
env.close();
|
|
|
|
// becky buys minter's nft even though minter is no longer alice's
|
|
// official minter.
|
|
BEAST_EXPECT(ownerCount(env, becky) == 0);
|
|
env(token::acceptSellOffer(becky, minterSellOfferIndex));
|
|
env.close();
|
|
BEAST_EXPECT(ownerCount(env, becky) == 1);
|
|
BEAST_EXPECT(ownerCount(env, minter) == 0);
|
|
|
|
// becky attempts to sell the nft.
|
|
env(token::createOffer(becky, nftMinterNoTransferID, XRP(23)),
|
|
txflags(tfSellNFToken),
|
|
ter(tefNFTOKEN_IS_NOT_TRANSFERABLE));
|
|
env.close();
|
|
|
|
// Since minter is not, at the moment, alice's official minter
|
|
// they cannot create an offer to buy the nft they minted.
|
|
BEAST_EXPECT(ownerCount(env, minter) == 0);
|
|
env(token::createOffer(minter, nftMinterNoTransferID, XRP(24)),
|
|
token::owner(becky),
|
|
ter(tefNFTOKEN_IS_NOT_TRANSFERABLE));
|
|
env.close();
|
|
BEAST_EXPECT(ownerCount(env, minter) == 0);
|
|
|
|
// alice can create an offer to buy the nft.
|
|
BEAST_EXPECT(ownerCount(env, alice) == 0);
|
|
uint256 const aliceBuyOfferIndex =
|
|
keylet::nftoffer(alice, env.seq(alice)).key;
|
|
env(token::createOffer(alice, nftMinterNoTransferID, XRP(25)),
|
|
token::owner(becky));
|
|
env.close();
|
|
BEAST_EXPECT(ownerCount(env, alice) == 1);
|
|
|
|
// Let enough ledgers pass that old transactions are no longer
|
|
// retried, then alice gives authorization back to minter.
|
|
for (int i = 0; i < 10; ++i)
|
|
env.close();
|
|
|
|
env(token::setMinter(alice, minter));
|
|
env.close();
|
|
|
|
// Now minter can create an offer to buy the nft.
|
|
BEAST_EXPECT(ownerCount(env, minter) == 0);
|
|
uint256 const minterBuyOfferIndex =
|
|
keylet::nftoffer(minter, env.seq(minter)).key;
|
|
env(token::createOffer(minter, nftMinterNoTransferID, XRP(26)),
|
|
token::owner(becky));
|
|
env.close();
|
|
BEAST_EXPECT(ownerCount(env, minter) == 1);
|
|
|
|
// alice removes authorization of minter so we can see whether
|
|
// minter's pre-existing buy offer still works.
|
|
env(token::clearMinter(alice));
|
|
env.close();
|
|
|
|
// becky accepts minter's sell offer.
|
|
BEAST_EXPECT(ownerCount(env, minter) == 1);
|
|
BEAST_EXPECT(ownerCount(env, becky) == 1);
|
|
env(token::acceptBuyOffer(becky, minterBuyOfferIndex));
|
|
env.close();
|
|
BEAST_EXPECT(ownerCount(env, minter) == 1);
|
|
BEAST_EXPECT(ownerCount(env, becky) == 0);
|
|
BEAST_EXPECT(ownerCount(env, alice) == 1);
|
|
|
|
// minter burns their nft and alice cancels her offer so the
|
|
// next tests can start with a clean slate.
|
|
env(token::burn(minter, nftMinterNoTransferID), ter(tesSUCCESS));
|
|
env.close();
|
|
env(token::cancelOffer(alice, {aliceBuyOfferIndex}));
|
|
env.close();
|
|
BEAST_EXPECT(ownerCount(env, alice) == 0);
|
|
BEAST_EXPECT(ownerCount(env, becky) == 0);
|
|
BEAST_EXPECT(ownerCount(env, minter) == 0);
|
|
}
|
|
// nfts with flagTransferable set should be buyable and salable
|
|
// by anybody.
|
|
{
|
|
BEAST_EXPECT(ownerCount(env, alice) == 0);
|
|
uint256 const nftAliceID{
|
|
token::getNextID(env, alice, 0u, tfTransferable)};
|
|
env(token::mint(alice, 0u), txflags(tfTransferable));
|
|
env.close();
|
|
BEAST_EXPECT(ownerCount(env, alice) == 1);
|
|
|
|
// Both alice and becky can make offers for alice's nft.
|
|
uint256 const aliceSellOfferIndex =
|
|
keylet::nftoffer(alice, env.seq(alice)).key;
|
|
env(token::createOffer(alice, nftAliceID, XRP(20)),
|
|
txflags(tfSellNFToken));
|
|
env.close();
|
|
BEAST_EXPECT(ownerCount(env, alice) == 2);
|
|
|
|
uint256 const beckyBuyOfferIndex =
|
|
keylet::nftoffer(becky, env.seq(becky)).key;
|
|
env(token::createOffer(becky, nftAliceID, XRP(21)),
|
|
token::owner(alice));
|
|
env.close();
|
|
BEAST_EXPECT(ownerCount(env, alice) == 2);
|
|
|
|
// becky accepts alice's sell offer.
|
|
env(token::acceptSellOffer(becky, aliceSellOfferIndex));
|
|
env.close();
|
|
BEAST_EXPECT(ownerCount(env, alice) == 0);
|
|
BEAST_EXPECT(ownerCount(env, becky) == 2);
|
|
|
|
// becky offers to sell the nft.
|
|
uint256 const beckySellOfferIndex =
|
|
keylet::nftoffer(becky, env.seq(becky)).key;
|
|
env(token::createOffer(becky, nftAliceID, XRP(22)),
|
|
txflags(tfSellNFToken));
|
|
env.close();
|
|
BEAST_EXPECT(ownerCount(env, alice) == 0);
|
|
BEAST_EXPECT(ownerCount(env, becky) == 3);
|
|
|
|
// minter buys the nft (even though minter is not currently
|
|
// alice's minter).
|
|
env(token::acceptSellOffer(minter, beckySellOfferIndex));
|
|
env.close();
|
|
BEAST_EXPECT(ownerCount(env, alice) == 0);
|
|
BEAST_EXPECT(ownerCount(env, becky) == 1);
|
|
BEAST_EXPECT(ownerCount(env, minter) == 1);
|
|
|
|
// minter offers to sell the nft.
|
|
uint256 const minterSellOfferIndex =
|
|
keylet::nftoffer(minter, env.seq(minter)).key;
|
|
env(token::createOffer(minter, nftAliceID, XRP(23)),
|
|
txflags(tfSellNFToken));
|
|
env.close();
|
|
BEAST_EXPECT(ownerCount(env, alice) == 0);
|
|
BEAST_EXPECT(ownerCount(env, becky) == 1);
|
|
BEAST_EXPECT(ownerCount(env, minter) == 2);
|
|
|
|
// alice buys back the nft.
|
|
env(token::acceptSellOffer(alice, minterSellOfferIndex));
|
|
env.close();
|
|
BEAST_EXPECT(ownerCount(env, alice) == 1);
|
|
BEAST_EXPECT(ownerCount(env, becky) == 1);
|
|
BEAST_EXPECT(ownerCount(env, minter) == 0);
|
|
|
|
// Remember the buy offer that becky made for alice's token way
|
|
// back when? It's still in the ledger, and alice accepts it.
|
|
env(token::acceptBuyOffer(alice, beckyBuyOfferIndex));
|
|
env.close();
|
|
BEAST_EXPECT(ownerCount(env, alice) == 0);
|
|
BEAST_EXPECT(ownerCount(env, becky) == 1);
|
|
BEAST_EXPECT(ownerCount(env, minter) == 0);
|
|
|
|
// Just for tidyness, becky burns the token before shutting
|
|
// things down.
|
|
env(token::burn(becky, nftAliceID));
|
|
env.close();
|
|
BEAST_EXPECT(ownerCount(env, alice) == 0);
|
|
BEAST_EXPECT(ownerCount(env, becky) == 0);
|
|
BEAST_EXPECT(ownerCount(env, minter) == 0);
|
|
}
|
|
}
|
|
|
|
void
|
|
testMintTransferFee(FeatureBitset features)
|
|
{
|
|
// Exercise NFTs with and without a transferFee.
|
|
testcase("Mint transferFee");
|
|
|
|
using namespace test::jtx;
|
|
|
|
Env env{*this, features};
|
|
|
|
Account const alice{"alice"};
|
|
Account const becky{"becky"};
|
|
Account const carol{"carol"};
|
|
Account const minter{"minter"};
|
|
Account const gw{"gw"};
|
|
IOU const gwXAU(gw["XAU"]);
|
|
|
|
env.fund(XRP(1000), alice, becky, carol, minter, gw);
|
|
env.close();
|
|
|
|
env(trust(alice, gwXAU(2000)));
|
|
env(trust(becky, gwXAU(2000)));
|
|
env(trust(carol, gwXAU(2000)));
|
|
env(trust(minter, gwXAU(2000)));
|
|
env.close();
|
|
env(pay(gw, alice, gwXAU(1000)));
|
|
env(pay(gw, becky, gwXAU(1000)));
|
|
env(pay(gw, carol, gwXAU(1000)));
|
|
env(pay(gw, minter, gwXAU(1000)));
|
|
env.close();
|
|
|
|
// Giving alice a minter helps us see if transfer rates are affected
|
|
// by that.
|
|
env(token::setMinter(alice, minter));
|
|
env.close();
|
|
|
|
// If there is no transferFee, then alice gets nothing for the
|
|
// transfer.
|
|
{
|
|
BEAST_EXPECT(ownerCount(env, alice) == 1);
|
|
BEAST_EXPECT(ownerCount(env, becky) == 1);
|
|
BEAST_EXPECT(ownerCount(env, carol) == 1);
|
|
BEAST_EXPECT(ownerCount(env, minter) == 1);
|
|
|
|
uint256 const nftID =
|
|
token::getNextID(env, alice, 0u, tfTransferable);
|
|
env(token::mint(alice), txflags(tfTransferable));
|
|
env.close();
|
|
|
|
// Becky buys the nft for XAU(10). Check balances.
|
|
uint256 const beckyBuyOfferIndex =
|
|
keylet::nftoffer(becky, env.seq(becky)).key;
|
|
env(token::createOffer(becky, nftID, gwXAU(10)),
|
|
token::owner(alice));
|
|
env.close();
|
|
BEAST_EXPECT(env.balance(alice, gwXAU) == gwXAU(1000));
|
|
BEAST_EXPECT(env.balance(becky, gwXAU) == gwXAU(1000));
|
|
|
|
env(token::acceptBuyOffer(alice, beckyBuyOfferIndex));
|
|
env.close();
|
|
BEAST_EXPECT(env.balance(alice, gwXAU) == gwXAU(1010));
|
|
BEAST_EXPECT(env.balance(becky, gwXAU) == gwXAU(990));
|
|
|
|
// becky sells nft to carol. alice's balance should not change.
|
|
uint256 const beckySellOfferIndex =
|
|
keylet::nftoffer(becky, env.seq(becky)).key;
|
|
env(token::createOffer(becky, nftID, gwXAU(10)),
|
|
txflags(tfSellNFToken));
|
|
env.close();
|
|
env(token::acceptSellOffer(carol, beckySellOfferIndex));
|
|
env.close();
|
|
BEAST_EXPECT(env.balance(alice, gwXAU) == gwXAU(1010));
|
|
BEAST_EXPECT(env.balance(becky, gwXAU) == gwXAU(1000));
|
|
BEAST_EXPECT(env.balance(carol, gwXAU) == gwXAU(990));
|
|
|
|
// minter buys nft from carol. alice's balance should not change.
|
|
uint256 const minterBuyOfferIndex =
|
|
keylet::nftoffer(minter, env.seq(minter)).key;
|
|
env(token::createOffer(minter, nftID, gwXAU(10)),
|
|
token::owner(carol));
|
|
env.close();
|
|
env(token::acceptBuyOffer(carol, minterBuyOfferIndex));
|
|
env.close();
|
|
BEAST_EXPECT(env.balance(alice, gwXAU) == gwXAU(1010));
|
|
BEAST_EXPECT(env.balance(becky, gwXAU) == gwXAU(1000));
|
|
BEAST_EXPECT(env.balance(carol, gwXAU) == gwXAU(1000));
|
|
BEAST_EXPECT(env.balance(minter, gwXAU) == gwXAU(990));
|
|
|
|
// minter sells the nft to alice. gwXAU balances should finish
|
|
// where they started.
|
|
uint256 const minterSellOfferIndex =
|
|
keylet::nftoffer(minter, env.seq(minter)).key;
|
|
env(token::createOffer(minter, nftID, gwXAU(10)),
|
|
txflags(tfSellNFToken));
|
|
env.close();
|
|
env(token::acceptSellOffer(alice, minterSellOfferIndex));
|
|
env.close();
|
|
BEAST_EXPECT(env.balance(alice, gwXAU) == gwXAU(1000));
|
|
BEAST_EXPECT(env.balance(becky, gwXAU) == gwXAU(1000));
|
|
BEAST_EXPECT(env.balance(carol, gwXAU) == gwXAU(1000));
|
|
BEAST_EXPECT(env.balance(minter, gwXAU) == gwXAU(1000));
|
|
|
|
// alice burns the nft to make later tests easier to think about.
|
|
env(token::burn(alice, nftID));
|
|
env.close();
|
|
BEAST_EXPECT(ownerCount(env, alice) == 1);
|
|
BEAST_EXPECT(ownerCount(env, becky) == 1);
|
|
BEAST_EXPECT(ownerCount(env, carol) == 1);
|
|
BEAST_EXPECT(ownerCount(env, minter) == 1);
|
|
}
|
|
|
|
// Set the smallest possible transfer fee.
|
|
{
|
|
// An nft with a transfer fee of 1 basis point.
|
|
uint256 const nftID =
|
|
token::getNextID(env, alice, 0u, tfTransferable, 1);
|
|
env(token::mint(alice), txflags(tfTransferable), token::xferFee(1));
|
|
env.close();
|
|
|
|
// Becky buys the nft for XAU(10). Check balances.
|
|
uint256 const beckyBuyOfferIndex =
|
|
keylet::nftoffer(becky, env.seq(becky)).key;
|
|
env(token::createOffer(becky, nftID, gwXAU(10)),
|
|
token::owner(alice));
|
|
env.close();
|
|
BEAST_EXPECT(env.balance(alice, gwXAU) == gwXAU(1000));
|
|
BEAST_EXPECT(env.balance(becky, gwXAU) == gwXAU(1000));
|
|
|
|
env(token::acceptBuyOffer(alice, beckyBuyOfferIndex));
|
|
env.close();
|
|
BEAST_EXPECT(env.balance(alice, gwXAU) == gwXAU(1010));
|
|
BEAST_EXPECT(env.balance(becky, gwXAU) == gwXAU(990));
|
|
|
|
// becky sells nft to carol. alice's balance goes up.
|
|
uint256 const beckySellOfferIndex =
|
|
keylet::nftoffer(becky, env.seq(becky)).key;
|
|
env(token::createOffer(becky, nftID, gwXAU(10)),
|
|
txflags(tfSellNFToken));
|
|
env.close();
|
|
env(token::acceptSellOffer(carol, beckySellOfferIndex));
|
|
env.close();
|
|
|
|
BEAST_EXPECT(env.balance(alice, gwXAU) == gwXAU(1010.0001));
|
|
BEAST_EXPECT(env.balance(becky, gwXAU) == gwXAU(999.9999));
|
|
BEAST_EXPECT(env.balance(carol, gwXAU) == gwXAU(990));
|
|
|
|
// minter buys nft from carol. alice's balance goes up.
|
|
uint256 const minterBuyOfferIndex =
|
|
keylet::nftoffer(minter, env.seq(minter)).key;
|
|
env(token::createOffer(minter, nftID, gwXAU(10)),
|
|
token::owner(carol));
|
|
env.close();
|
|
env(token::acceptBuyOffer(carol, minterBuyOfferIndex));
|
|
env.close();
|
|
|
|
BEAST_EXPECT(env.balance(alice, gwXAU) == gwXAU(1010.0002));
|
|
BEAST_EXPECT(env.balance(becky, gwXAU) == gwXAU(999.9999));
|
|
BEAST_EXPECT(env.balance(carol, gwXAU) == gwXAU(999.9999));
|
|
BEAST_EXPECT(env.balance(minter, gwXAU) == gwXAU(990));
|
|
|
|
// minter sells the nft to alice. Because alice is part of the
|
|
// transaction no tranfer fee is removed.
|
|
uint256 const minterSellOfferIndex =
|
|
keylet::nftoffer(minter, env.seq(minter)).key;
|
|
env(token::createOffer(minter, nftID, gwXAU(10)),
|
|
txflags(tfSellNFToken));
|
|
env.close();
|
|
env(token::acceptSellOffer(alice, minterSellOfferIndex));
|
|
env.close();
|
|
BEAST_EXPECT(env.balance(alice, gwXAU) == gwXAU(1000.0002));
|
|
BEAST_EXPECT(env.balance(becky, gwXAU) == gwXAU(999.9999));
|
|
BEAST_EXPECT(env.balance(carol, gwXAU) == gwXAU(999.9999));
|
|
BEAST_EXPECT(env.balance(minter, gwXAU) == gwXAU(1000));
|
|
|
|
// alice pays to becky and carol so subsequent tests are easier
|
|
// to think about.
|
|
env(pay(alice, becky, gwXAU(0.0001)));
|
|
env(pay(alice, carol, gwXAU(0.0001)));
|
|
env.close();
|
|
|
|
BEAST_EXPECT(env.balance(alice, gwXAU) == gwXAU(1000));
|
|
BEAST_EXPECT(env.balance(becky, gwXAU) == gwXAU(1000));
|
|
BEAST_EXPECT(env.balance(carol, gwXAU) == gwXAU(1000));
|
|
BEAST_EXPECT(env.balance(minter, gwXAU) == gwXAU(1000));
|
|
|
|
// alice burns the nft to make later tests easier to think about.
|
|
env(token::burn(alice, nftID));
|
|
env.close();
|
|
BEAST_EXPECT(ownerCount(env, alice) == 1);
|
|
BEAST_EXPECT(ownerCount(env, becky) == 1);
|
|
BEAST_EXPECT(ownerCount(env, carol) == 1);
|
|
BEAST_EXPECT(ownerCount(env, minter) == 1);
|
|
}
|
|
|
|
// Set the largest allowed transfer fee.
|
|
{
|
|
// A transfer fee greater than 50% is not allowed.
|
|
env(token::mint(alice),
|
|
txflags(tfTransferable),
|
|
token::xferFee(maxTransferFee + 1),
|
|
ter(temBAD_NFTOKEN_TRANSFER_FEE));
|
|
env.close();
|
|
|
|
// Make an nft with a transfer fee of 50%.
|
|
uint256 const nftID = token::getNextID(
|
|
env, alice, 0u, tfTransferable, maxTransferFee);
|
|
env(token::mint(alice),
|
|
txflags(tfTransferable),
|
|
token::xferFee(maxTransferFee));
|
|
env.close();
|
|
|
|
// Becky buys the nft for XAU(10). Check balances.
|
|
uint256 const beckyBuyOfferIndex =
|
|
keylet::nftoffer(becky, env.seq(becky)).key;
|
|
env(token::createOffer(becky, nftID, gwXAU(10)),
|
|
token::owner(alice));
|
|
env.close();
|
|
BEAST_EXPECT(env.balance(alice, gwXAU) == gwXAU(1000));
|
|
BEAST_EXPECT(env.balance(becky, gwXAU) == gwXAU(1000));
|
|
|
|
env(token::acceptBuyOffer(alice, beckyBuyOfferIndex));
|
|
env.close();
|
|
BEAST_EXPECT(env.balance(alice, gwXAU) == gwXAU(1010));
|
|
BEAST_EXPECT(env.balance(becky, gwXAU) == gwXAU(990));
|
|
|
|
// becky sells nft to minter. alice's balance goes up.
|
|
uint256 const beckySellOfferIndex =
|
|
keylet::nftoffer(becky, env.seq(becky)).key;
|
|
env(token::createOffer(becky, nftID, gwXAU(100)),
|
|
txflags(tfSellNFToken));
|
|
env.close();
|
|
env(token::acceptSellOffer(minter, beckySellOfferIndex));
|
|
env.close();
|
|
|
|
BEAST_EXPECT(env.balance(alice, gwXAU) == gwXAU(1060));
|
|
BEAST_EXPECT(env.balance(becky, gwXAU) == gwXAU(1040));
|
|
BEAST_EXPECT(env.balance(minter, gwXAU) == gwXAU(900));
|
|
|
|
// carol buys nft from minter. alice's balance goes up.
|
|
uint256 const carolBuyOfferIndex =
|
|
keylet::nftoffer(carol, env.seq(carol)).key;
|
|
env(token::createOffer(carol, nftID, gwXAU(10)),
|
|
token::owner(minter));
|
|
env.close();
|
|
env(token::acceptBuyOffer(minter, carolBuyOfferIndex));
|
|
env.close();
|
|
|
|
BEAST_EXPECT(env.balance(alice, gwXAU) == gwXAU(1065));
|
|
BEAST_EXPECT(env.balance(becky, gwXAU) == gwXAU(1040));
|
|
BEAST_EXPECT(env.balance(minter, gwXAU) == gwXAU(905));
|
|
BEAST_EXPECT(env.balance(carol, gwXAU) == gwXAU(990));
|
|
|
|
// carol sells the nft to alice. Because alice is part of the
|
|
// transaction no tranfer fee is removed.
|
|
uint256 const carolSellOfferIndex =
|
|
keylet::nftoffer(carol, env.seq(carol)).key;
|
|
env(token::createOffer(carol, nftID, gwXAU(10)),
|
|
txflags(tfSellNFToken));
|
|
env.close();
|
|
env(token::acceptSellOffer(alice, carolSellOfferIndex));
|
|
env.close();
|
|
|
|
BEAST_EXPECT(env.balance(alice, gwXAU) == gwXAU(1055));
|
|
BEAST_EXPECT(env.balance(becky, gwXAU) == gwXAU(1040));
|
|
BEAST_EXPECT(env.balance(minter, gwXAU) == gwXAU(905));
|
|
BEAST_EXPECT(env.balance(carol, gwXAU) == gwXAU(1000));
|
|
|
|
// rebalance so subsequent tests are easier to think about.
|
|
env(pay(alice, minter, gwXAU(55)));
|
|
env(pay(becky, minter, gwXAU(40)));
|
|
env.close();
|
|
BEAST_EXPECT(env.balance(alice, gwXAU) == gwXAU(1000));
|
|
BEAST_EXPECT(env.balance(becky, gwXAU) == gwXAU(1000));
|
|
BEAST_EXPECT(env.balance(carol, gwXAU) == gwXAU(1000));
|
|
BEAST_EXPECT(env.balance(minter, gwXAU) == gwXAU(1000));
|
|
|
|
// alice burns the nft to make later tests easier to think about.
|
|
env(token::burn(alice, nftID));
|
|
env.close();
|
|
BEAST_EXPECT(ownerCount(env, alice) == 1);
|
|
BEAST_EXPECT(ownerCount(env, becky) == 1);
|
|
BEAST_EXPECT(ownerCount(env, carol) == 1);
|
|
BEAST_EXPECT(ownerCount(env, minter) == 1);
|
|
}
|
|
|
|
// See the impact of rounding when the nft is sold for small amounts
|
|
// of drops.
|
|
{
|
|
// An nft with a transfer fee of 1 basis point.
|
|
uint256 const nftID =
|
|
token::getNextID(env, alice, 0u, tfTransferable, 1);
|
|
env(token::mint(alice), txflags(tfTransferable), token::xferFee(1));
|
|
env.close();
|
|
|
|
// minter buys the nft for XRP(1). Since the transfer involves
|
|
// alice there should be no transfer fee.
|
|
STAmount fee = drops(10);
|
|
STAmount aliceBalance = env.balance(alice);
|
|
STAmount minterBalance = env.balance(minter);
|
|
uint256 const minterBuyOfferIndex =
|
|
keylet::nftoffer(minter, env.seq(minter)).key;
|
|
env(token::createOffer(minter, nftID, XRP(1)), token::owner(alice));
|
|
env.close();
|
|
env(token::acceptBuyOffer(alice, minterBuyOfferIndex));
|
|
env.close();
|
|
aliceBalance += XRP(1) - fee;
|
|
minterBalance -= XRP(1) + fee;
|
|
BEAST_EXPECT(env.balance(alice) == aliceBalance);
|
|
BEAST_EXPECT(env.balance(minter) == minterBalance);
|
|
|
|
// minter sells to carol. The payment is just small enough that
|
|
// alice does not get any transfer fee.
|
|
STAmount carolBalance = env.balance(carol);
|
|
uint256 const minterSellOfferIndex =
|
|
keylet::nftoffer(minter, env.seq(minter)).key;
|
|
env(token::createOffer(minter, nftID, drops(99999)),
|
|
txflags(tfSellNFToken));
|
|
env.close();
|
|
env(token::acceptSellOffer(carol, minterSellOfferIndex));
|
|
env.close();
|
|
minterBalance += drops(99999) - fee;
|
|
carolBalance -= drops(99999) + fee;
|
|
BEAST_EXPECT(env.balance(alice) == aliceBalance);
|
|
BEAST_EXPECT(env.balance(minter) == minterBalance);
|
|
BEAST_EXPECT(env.balance(carol) == carolBalance);
|
|
|
|
// carol sells to becky. This is the smallest amount to pay for a
|
|
// transfer that enables a transfer fee of 1 basis point.
|
|
STAmount beckyBalance = env.balance(becky);
|
|
uint256 const beckyBuyOfferIndex =
|
|
keylet::nftoffer(becky, env.seq(becky)).key;
|
|
env(token::createOffer(becky, nftID, drops(100000)),
|
|
token::owner(carol));
|
|
env.close();
|
|
env(token::acceptBuyOffer(carol, beckyBuyOfferIndex));
|
|
env.close();
|
|
carolBalance += drops(99999) - fee;
|
|
beckyBalance -= drops(100000) + fee;
|
|
aliceBalance += drops(1);
|
|
|
|
BEAST_EXPECT(env.balance(alice) == aliceBalance);
|
|
BEAST_EXPECT(env.balance(minter) == minterBalance);
|
|
BEAST_EXPECT(env.balance(carol) == carolBalance);
|
|
BEAST_EXPECT(env.balance(becky) == beckyBalance);
|
|
}
|
|
|
|
// See the impact of rounding when the nft is sold for small amounts
|
|
// of an IOU.
|
|
{
|
|
// An nft with a transfer fee of 1 basis point.
|
|
uint256 const nftID =
|
|
token::getNextID(env, alice, 0u, tfTransferable, 1);
|
|
env(token::mint(alice), txflags(tfTransferable), token::xferFee(1));
|
|
env.close();
|
|
|
|
// Due to the floating point nature of IOUs we need to
|
|
// significantly reduce the gwXAU balances of our accounts prior
|
|
// to the iou transfer. Otherwise no transfers will happen.
|
|
env(pay(alice, gw, env.balance(alice, gwXAU)));
|
|
env(pay(minter, gw, env.balance(minter, gwXAU)));
|
|
env(pay(becky, gw, env.balance(becky, gwXAU)));
|
|
env.close();
|
|
|
|
STAmount const startXAUBalance(
|
|
gwXAU.issue(), STAmount::cMinValue, STAmount::cMinOffset + 5);
|
|
env(pay(gw, alice, startXAUBalance));
|
|
env(pay(gw, minter, startXAUBalance));
|
|
env(pay(gw, becky, startXAUBalance));
|
|
env.close();
|
|
|
|
// Here is the smallest expressible gwXAU amount.
|
|
STAmount tinyXAU(
|
|
gwXAU.issue(), STAmount::cMinValue, STAmount::cMinOffset);
|
|
|
|
// minter buys the nft for tinyXAU. Since the transfer involves
|
|
// alice there should be no transfer fee.
|
|
STAmount aliceBalance = env.balance(alice, gwXAU);
|
|
STAmount minterBalance = env.balance(minter, gwXAU);
|
|
uint256 const minterBuyOfferIndex =
|
|
keylet::nftoffer(minter, env.seq(minter)).key;
|
|
env(token::createOffer(minter, nftID, tinyXAU),
|
|
token::owner(alice));
|
|
env.close();
|
|
env(token::acceptBuyOffer(alice, minterBuyOfferIndex));
|
|
env.close();
|
|
aliceBalance += tinyXAU;
|
|
minterBalance -= tinyXAU;
|
|
BEAST_EXPECT(env.balance(alice, gwXAU) == aliceBalance);
|
|
BEAST_EXPECT(env.balance(minter, gwXAU) == minterBalance);
|
|
|
|
// minter sells to carol.
|
|
STAmount carolBalance = env.balance(carol, gwXAU);
|
|
uint256 const minterSellOfferIndex =
|
|
keylet::nftoffer(minter, env.seq(minter)).key;
|
|
env(token::createOffer(minter, nftID, tinyXAU),
|
|
txflags(tfSellNFToken));
|
|
env.close();
|
|
env(token::acceptSellOffer(carol, minterSellOfferIndex));
|
|
env.close();
|
|
|
|
minterBalance += tinyXAU;
|
|
carolBalance -= tinyXAU;
|
|
// tiny XAU is so small that alice does not get a transfer fee.
|
|
BEAST_EXPECT(env.balance(alice, gwXAU) == aliceBalance);
|
|
BEAST_EXPECT(env.balance(minter, gwXAU) == minterBalance);
|
|
BEAST_EXPECT(env.balance(carol, gwXAU) == carolBalance);
|
|
|
|
// carol sells to becky. This is the smallest gwXAU amount
|
|
// to pay for a transfer that enables a transfer fee of 1.
|
|
STAmount const cheapNFT(
|
|
gwXAU.issue(), STAmount::cMinValue, STAmount::cMinOffset + 5);
|
|
|
|
STAmount beckyBalance = env.balance(becky, gwXAU);
|
|
uint256 const beckyBuyOfferIndex =
|
|
keylet::nftoffer(becky, env.seq(becky)).key;
|
|
env(token::createOffer(becky, nftID, cheapNFT),
|
|
token::owner(carol));
|
|
env.close();
|
|
env(token::acceptBuyOffer(carol, beckyBuyOfferIndex));
|
|
env.close();
|
|
|
|
aliceBalance += tinyXAU;
|
|
beckyBalance -= cheapNFT;
|
|
carolBalance += cheapNFT - tinyXAU;
|
|
BEAST_EXPECT(env.balance(alice, gwXAU) == aliceBalance);
|
|
BEAST_EXPECT(env.balance(minter, gwXAU) == minterBalance);
|
|
BEAST_EXPECT(env.balance(carol, gwXAU) == carolBalance);
|
|
BEAST_EXPECT(env.balance(becky, gwXAU) == beckyBalance);
|
|
}
|
|
}
|
|
|
|
void
|
|
testMintTaxon(FeatureBitset features)
|
|
{
|
|
// Exercise the NFT taxon field.
|
|
testcase("Mint taxon");
|
|
|
|
using namespace test::jtx;
|
|
|
|
Env env{*this, features};
|
|
|
|
Account const alice{"alice"};
|
|
Account const becky{"becky"};
|
|
|
|
env.fund(XRP(1000), alice, becky);
|
|
env.close();
|
|
|
|
// The taxon field is incorporated straight into the NFT ID. So
|
|
// tests only need to operate on NFT IDs; we don't need to generate
|
|
// any transactions.
|
|
|
|
// The taxon value should be recoverable from the NFT ID.
|
|
{
|
|
uint256 const nftID = token::getNextID(env, alice, 0u);
|
|
BEAST_EXPECT(nft::getTaxon(nftID) == nft::toTaxon(0));
|
|
}
|
|
|
|
// Make sure the full range of taxon values work. We just tried
|
|
// the minimum. Now try the largest.
|
|
{
|
|
uint256 const nftID = token::getNextID(env, alice, 0xFFFFFFFFu);
|
|
BEAST_EXPECT(nft::getTaxon(nftID) == nft::toTaxon((0xFFFFFFFF)));
|
|
}
|
|
|
|
// Do some touch testing to show that the taxon is recoverable no
|
|
// matter what else changes around it in the nft ID.
|
|
{
|
|
std::uint32_t const taxon = rand_int<std::uint32_t>();
|
|
for (int i = 0; i < 10; ++i)
|
|
{
|
|
// lambda to produce a useful message on error.
|
|
auto check = [this](std::uint32_t taxon, uint256 const& nftID) {
|
|
nft::Taxon const gotTaxon = nft::getTaxon(nftID);
|
|
if (nft::toTaxon(taxon) == gotTaxon)
|
|
pass();
|
|
else
|
|
{
|
|
std::stringstream ss;
|
|
ss << "Taxon recovery failed from nftID "
|
|
<< to_string(nftID) << ". Expected: " << taxon
|
|
<< "; got: " << gotTaxon;
|
|
fail(ss.str());
|
|
}
|
|
};
|
|
|
|
uint256 const nftAliceID = token::getID(
|
|
alice,
|
|
taxon,
|
|
rand_int<std::uint32_t>(),
|
|
rand_int<std::uint16_t>(),
|
|
rand_int<std::uint16_t>());
|
|
check(taxon, nftAliceID);
|
|
|
|
uint256 const nftBeckyID = token::getID(
|
|
becky,
|
|
taxon,
|
|
rand_int<std::uint32_t>(),
|
|
rand_int<std::uint16_t>(),
|
|
rand_int<std::uint16_t>());
|
|
check(taxon, nftBeckyID);
|
|
}
|
|
}
|
|
}
|
|
|
|
void
|
|
testMintURI(FeatureBitset features)
|
|
{
|
|
// Exercise the NFT URI field.
|
|
// 1. Create a number of NFTs with and without URIs.
|
|
// 2. Retrieve the NFTs from the server.
|
|
// 3. Make sure the right URI is attached to each NFT.
|
|
testcase("Mint URI");
|
|
|
|
using namespace test::jtx;
|
|
|
|
Env env{*this, features};
|
|
|
|
Account const alice{"alice"};
|
|
Account const becky{"becky"};
|
|
|
|
env.fund(XRP(10000), alice, becky);
|
|
env.close();
|
|
|
|
// lambda that returns a randomly generated string which fits
|
|
// the constraints of a URI. Empty strings may be returned.
|
|
// In the empty string case do not add the URI to the nft.
|
|
auto randURI = []() {
|
|
std::string ret;
|
|
|
|
// About 20% of the returned strings should be empty
|
|
if (rand_int(4) == 0)
|
|
return ret;
|
|
|
|
std::size_t const strLen = rand_int(256);
|
|
ret.reserve(strLen);
|
|
for (std::size_t i = 0; i < strLen; ++i)
|
|
ret.push_back(rand_byte());
|
|
|
|
return ret;
|
|
};
|
|
|
|
// Make a list of URIs that we'll put in nfts.
|
|
struct Entry
|
|
{
|
|
std::string uri;
|
|
std::uint32_t taxon;
|
|
|
|
Entry(std::string uri_, std::uint32_t taxon_)
|
|
: uri(std::move(uri_)), taxon(taxon_)
|
|
{
|
|
}
|
|
};
|
|
|
|
std::vector<Entry> entries;
|
|
entries.reserve(100);
|
|
for (std::size_t i = 0; i < 100; ++i)
|
|
entries.emplace_back(randURI(), rand_int<std::uint32_t>());
|
|
|
|
// alice creates nfts using entries.
|
|
for (Entry const& entry : entries)
|
|
{
|
|
if (entry.uri.empty())
|
|
{
|
|
env(token::mint(alice, entry.taxon));
|
|
}
|
|
else
|
|
{
|
|
env(token::mint(alice, entry.taxon), token::uri(entry.uri));
|
|
}
|
|
env.close();
|
|
}
|
|
|
|
// Recover alice's nfts from the ledger.
|
|
Json::Value aliceNFTs = [&env, &alice]() {
|
|
Json::Value params;
|
|
params[jss::account] = alice.human();
|
|
params[jss::type] = "state";
|
|
return env.rpc("json", "account_nfts", to_string(params));
|
|
}();
|
|
|
|
// Verify that the returned NFTs match what we sent.
|
|
Json::Value& nfts = aliceNFTs[jss::result][jss::account_nfts];
|
|
if (!BEAST_EXPECT(nfts.size() == entries.size()))
|
|
return;
|
|
|
|
// Sort the returned NFTs by nft_serial so the are in the same order
|
|
// as entries.
|
|
std::vector<Json::Value> sortedNFTs;
|
|
sortedNFTs.reserve(nfts.size());
|
|
for (std::size_t i = 0; i < nfts.size(); ++i)
|
|
sortedNFTs.push_back(nfts[i]);
|
|
std::sort(
|
|
sortedNFTs.begin(),
|
|
sortedNFTs.end(),
|
|
[](Json::Value const& lhs, Json::Value const& rhs) {
|
|
return lhs[jss::nft_serial] < rhs[jss::nft_serial];
|
|
});
|
|
|
|
for (std::size_t i = 0; i < entries.size(); ++i)
|
|
{
|
|
Entry const& entry = entries[i];
|
|
Json::Value const& ret = sortedNFTs[i];
|
|
BEAST_EXPECT(entry.taxon == ret[sfNFTokenTaxon.jsonName]);
|
|
if (entry.uri.empty())
|
|
{
|
|
BEAST_EXPECT(!ret.isMember(sfURI.jsonName));
|
|
}
|
|
else
|
|
{
|
|
BEAST_EXPECT(strHex(entry.uri) == ret[sfURI.jsonName]);
|
|
}
|
|
}
|
|
}
|
|
|
|
void
|
|
testCreateOfferDestination(FeatureBitset features)
|
|
{
|
|
// Explore the CreateOffer Destination field.
|
|
testcase("Create offer destination");
|
|
|
|
using namespace test::jtx;
|
|
|
|
Env env{*this, features};
|
|
|
|
Account const issuer{"issuer"};
|
|
Account const minter{"minter"};
|
|
Account const buyer{"buyer"};
|
|
Account const broker{"broker"};
|
|
|
|
env.fund(XRP(1000), issuer, minter, buyer, broker);
|
|
|
|
// We want to explore how issuers vs minters fits into the permission
|
|
// scheme. So issuer issues and minter mints.
|
|
env(token::setMinter(issuer, minter));
|
|
env.close();
|
|
|
|
uint256 const nftokenID =
|
|
token::getNextID(env, issuer, 0, tfTransferable);
|
|
env(token::mint(minter, 0),
|
|
token::issuer(issuer),
|
|
txflags(tfTransferable));
|
|
env.close();
|
|
|
|
// Test how adding a Destination field to an offer affects permissions
|
|
// for canceling offers.
|
|
{
|
|
uint256 const offerMinterToIssuer =
|
|
keylet::nftoffer(minter, env.seq(minter)).key;
|
|
env(token::createOffer(minter, nftokenID, drops(1)),
|
|
token::destination(issuer),
|
|
txflags(tfSellNFToken));
|
|
|
|
uint256 const offerMinterToBuyer =
|
|
keylet::nftoffer(minter, env.seq(minter)).key;
|
|
env(token::createOffer(minter, nftokenID, drops(1)),
|
|
token::destination(buyer),
|
|
txflags(tfSellNFToken));
|
|
|
|
uint256 const offerIssuerToMinter =
|
|
keylet::nftoffer(issuer, env.seq(issuer)).key;
|
|
env(token::createOffer(issuer, nftokenID, drops(1)),
|
|
token::owner(minter),
|
|
token::destination(minter));
|
|
|
|
uint256 const offerIssuerToBuyer =
|
|
keylet::nftoffer(issuer, env.seq(issuer)).key;
|
|
env(token::createOffer(issuer, nftokenID, drops(1)),
|
|
token::owner(minter),
|
|
token::destination(buyer));
|
|
|
|
env.close();
|
|
BEAST_EXPECT(ownerCount(env, issuer) == 2);
|
|
BEAST_EXPECT(ownerCount(env, minter) == 3);
|
|
BEAST_EXPECT(ownerCount(env, buyer) == 0);
|
|
|
|
// Test who gets to cancel the offers. Anyone outside of the
|
|
// offer-owner/destination pair should not be able to cancel the
|
|
// offers.
|
|
//
|
|
// Note that issuer does not have any special permissions regarding
|
|
// offer cancellation. issuer cannot cancel an offer for an
|
|
// NFToken they issued.
|
|
env(token::cancelOffer(issuer, {offerMinterToBuyer}),
|
|
ter(tecNO_PERMISSION));
|
|
env(token::cancelOffer(buyer, {offerMinterToIssuer}),
|
|
ter(tecNO_PERMISSION));
|
|
env(token::cancelOffer(buyer, {offerIssuerToMinter}),
|
|
ter(tecNO_PERMISSION));
|
|
env(token::cancelOffer(minter, {offerIssuerToBuyer}),
|
|
ter(tecNO_PERMISSION));
|
|
env.close();
|
|
BEAST_EXPECT(ownerCount(env, issuer) == 2);
|
|
BEAST_EXPECT(ownerCount(env, minter) == 3);
|
|
BEAST_EXPECT(ownerCount(env, buyer) == 0);
|
|
|
|
// Both the offer creator and and destination should be able to
|
|
// cancel the offers.
|
|
env(token::cancelOffer(buyer, {offerMinterToBuyer}));
|
|
env(token::cancelOffer(minter, {offerMinterToIssuer}));
|
|
env(token::cancelOffer(buyer, {offerIssuerToBuyer}));
|
|
env(token::cancelOffer(issuer, {offerIssuerToMinter}));
|
|
env.close();
|
|
BEAST_EXPECT(ownerCount(env, issuer) == 0);
|
|
BEAST_EXPECT(ownerCount(env, minter) == 1);
|
|
BEAST_EXPECT(ownerCount(env, buyer) == 0);
|
|
}
|
|
|
|
// Test how adding a Destination field to a sell offer affects
|
|
// accepting that offer.
|
|
{
|
|
uint256 const offerMinterSellsToBuyer =
|
|
keylet::nftoffer(minter, env.seq(minter)).key;
|
|
env(token::createOffer(minter, nftokenID, drops(1)),
|
|
token::destination(buyer),
|
|
txflags(tfSellNFToken));
|
|
env.close();
|
|
BEAST_EXPECT(ownerCount(env, issuer) == 0);
|
|
BEAST_EXPECT(ownerCount(env, minter) == 2);
|
|
BEAST_EXPECT(ownerCount(env, buyer) == 0);
|
|
|
|
// issuer cannot accept a sell offer where they are not the
|
|
// destination.
|
|
env(token::acceptSellOffer(issuer, offerMinterSellsToBuyer),
|
|
ter(tecNO_PERMISSION));
|
|
env.close();
|
|
BEAST_EXPECT(ownerCount(env, issuer) == 0);
|
|
BEAST_EXPECT(ownerCount(env, minter) == 2);
|
|
BEAST_EXPECT(ownerCount(env, buyer) == 0);
|
|
|
|
// However buyer can accept the sell offer.
|
|
env(token::acceptSellOffer(buyer, offerMinterSellsToBuyer));
|
|
env.close();
|
|
BEAST_EXPECT(ownerCount(env, issuer) == 0);
|
|
BEAST_EXPECT(ownerCount(env, minter) == 0);
|
|
BEAST_EXPECT(ownerCount(env, buyer) == 1);
|
|
}
|
|
|
|
// Test how adding a Destination field to a buy offer affects
|
|
// accepting that offer.
|
|
{
|
|
uint256 const offerMinterBuysFromBuyer =
|
|
keylet::nftoffer(minter, env.seq(minter)).key;
|
|
env(token::createOffer(minter, nftokenID, drops(1)),
|
|
token::owner(buyer),
|
|
token::destination(buyer));
|
|
env.close();
|
|
BEAST_EXPECT(ownerCount(env, issuer) == 0);
|
|
BEAST_EXPECT(ownerCount(env, minter) == 1);
|
|
BEAST_EXPECT(ownerCount(env, buyer) == 1);
|
|
|
|
// issuer cannot accept a buy offer where they are the
|
|
// destination.
|
|
env(token::acceptBuyOffer(issuer, offerMinterBuysFromBuyer),
|
|
ter(tecNO_PERMISSION));
|
|
env.close();
|
|
BEAST_EXPECT(ownerCount(env, issuer) == 0);
|
|
BEAST_EXPECT(ownerCount(env, minter) == 1);
|
|
BEAST_EXPECT(ownerCount(env, buyer) == 1);
|
|
|
|
// Buyer accepts minter's offer.
|
|
env(token::acceptBuyOffer(buyer, offerMinterBuysFromBuyer));
|
|
env.close();
|
|
BEAST_EXPECT(ownerCount(env, issuer) == 0);
|
|
BEAST_EXPECT(ownerCount(env, minter) == 1);
|
|
BEAST_EXPECT(ownerCount(env, buyer) == 0);
|
|
|
|
// If a destination other than the NFToken owner is set, that
|
|
// destination must act as a broker. The NFToken owner may not
|
|
// simply accept the offer.
|
|
uint256 const offerBuyerBuysFromMinter =
|
|
keylet::nftoffer(buyer, env.seq(buyer)).key;
|
|
env(token::createOffer(buyer, nftokenID, drops(1)),
|
|
token::owner(minter),
|
|
token::destination(broker));
|
|
env.close();
|
|
BEAST_EXPECT(ownerCount(env, issuer) == 0);
|
|
BEAST_EXPECT(ownerCount(env, minter) == 1);
|
|
BEAST_EXPECT(ownerCount(env, buyer) == 1);
|
|
|
|
env(token::acceptBuyOffer(minter, offerBuyerBuysFromMinter),
|
|
ter(tecNO_PERMISSION));
|
|
env.close();
|
|
|
|
// Clean up the unused offer.
|
|
env(token::cancelOffer(buyer, {offerBuyerBuysFromMinter}));
|
|
env.close();
|
|
BEAST_EXPECT(ownerCount(env, issuer) == 0);
|
|
BEAST_EXPECT(ownerCount(env, minter) == 1);
|
|
BEAST_EXPECT(ownerCount(env, buyer) == 0);
|
|
}
|
|
|
|
// Show that a sell offer's Destination can broker that sell offer
|
|
// to another account.
|
|
{
|
|
uint256 const offerMinterToBroker =
|
|
keylet::nftoffer(minter, env.seq(minter)).key;
|
|
env(token::createOffer(minter, nftokenID, drops(1)),
|
|
token::destination(broker),
|
|
txflags(tfSellNFToken));
|
|
|
|
uint256 const offerBuyerToMinter =
|
|
keylet::nftoffer(buyer, env.seq(buyer)).key;
|
|
env(token::createOffer(buyer, nftokenID, drops(1)),
|
|
token::owner(minter));
|
|
|
|
env.close();
|
|
BEAST_EXPECT(ownerCount(env, issuer) == 0);
|
|
BEAST_EXPECT(ownerCount(env, minter) == 2);
|
|
BEAST_EXPECT(ownerCount(env, buyer) == 1);
|
|
|
|
// issuer cannot broker the offers, because they are not the
|
|
// Destination.
|
|
env(token::brokerOffers(
|
|
issuer, offerBuyerToMinter, offerMinterToBroker),
|
|
ter(tecNFTOKEN_BUY_SELL_MISMATCH));
|
|
env.close();
|
|
BEAST_EXPECT(ownerCount(env, issuer) == 0);
|
|
BEAST_EXPECT(ownerCount(env, minter) == 2);
|
|
BEAST_EXPECT(ownerCount(env, buyer) == 1);
|
|
|
|
// Since broker is the sell offer's destination, they can broker
|
|
// the two offers.
|
|
env(token::brokerOffers(
|
|
broker, offerBuyerToMinter, offerMinterToBroker));
|
|
env.close();
|
|
BEAST_EXPECT(ownerCount(env, issuer) == 0);
|
|
BEAST_EXPECT(ownerCount(env, minter) == 0);
|
|
BEAST_EXPECT(ownerCount(env, buyer) == 1);
|
|
}
|
|
|
|
// Show that brokered mode cannot complete a transfer where the
|
|
// Destination doesn't match, but can complete if the Destination
|
|
// does match.
|
|
{
|
|
uint256 const offerBuyerToMinter =
|
|
keylet::nftoffer(buyer, env.seq(buyer)).key;
|
|
env(token::createOffer(buyer, nftokenID, drops(1)),
|
|
token::destination(minter),
|
|
txflags(tfSellNFToken));
|
|
|
|
uint256 const offerMinterToBuyer =
|
|
keylet::nftoffer(minter, env.seq(minter)).key;
|
|
env(token::createOffer(minter, nftokenID, drops(1)),
|
|
token::owner(buyer));
|
|
|
|
uint256 const offerIssuerToBuyer =
|
|
keylet::nftoffer(issuer, env.seq(issuer)).key;
|
|
env(token::createOffer(issuer, nftokenID, drops(1)),
|
|
token::owner(buyer));
|
|
|
|
env.close();
|
|
BEAST_EXPECT(ownerCount(env, issuer) == 1);
|
|
BEAST_EXPECT(ownerCount(env, minter) == 1);
|
|
BEAST_EXPECT(ownerCount(env, buyer) == 2);
|
|
|
|
// Cannot broker offers when the sell destination is not the buyer.
|
|
env(token::brokerOffers(
|
|
broker, offerIssuerToBuyer, offerBuyerToMinter),
|
|
ter(tecNFTOKEN_BUY_SELL_MISMATCH));
|
|
env.close();
|
|
BEAST_EXPECT(ownerCount(env, issuer) == 1);
|
|
BEAST_EXPECT(ownerCount(env, minter) == 1);
|
|
BEAST_EXPECT(ownerCount(env, buyer) == 2);
|
|
|
|
// Broker is successful when destination is buyer.
|
|
env(token::brokerOffers(
|
|
broker, offerMinterToBuyer, offerBuyerToMinter));
|
|
env.close();
|
|
BEAST_EXPECT(ownerCount(env, issuer) == 1);
|
|
BEAST_EXPECT(ownerCount(env, minter) == 1);
|
|
BEAST_EXPECT(ownerCount(env, buyer) == 0);
|
|
|
|
// Clean out the unconsumed offer.
|
|
env(token::cancelOffer(issuer, {offerIssuerToBuyer}));
|
|
env.close();
|
|
BEAST_EXPECT(ownerCount(env, issuer) == 0);
|
|
BEAST_EXPECT(ownerCount(env, minter) == 1);
|
|
BEAST_EXPECT(ownerCount(env, buyer) == 0);
|
|
}
|
|
|
|
// Show that if a buy and a sell offer both have the same destination,
|
|
// then that destination can broker the offers.
|
|
{
|
|
uint256 const offerMinterToBroker =
|
|
keylet::nftoffer(minter, env.seq(minter)).key;
|
|
env(token::createOffer(minter, nftokenID, drops(1)),
|
|
token::destination(broker),
|
|
txflags(tfSellNFToken));
|
|
|
|
uint256 const offerBuyerToBroker =
|
|
keylet::nftoffer(buyer, env.seq(buyer)).key;
|
|
env(token::createOffer(buyer, nftokenID, drops(1)),
|
|
token::owner(minter),
|
|
token::destination(broker));
|
|
|
|
// Cannot broker offers when the sell destination is not the buyer
|
|
// or the broker.
|
|
env(token::brokerOffers(
|
|
issuer, offerBuyerToBroker, offerMinterToBroker),
|
|
ter(tecNFTOKEN_BUY_SELL_MISMATCH));
|
|
env.close();
|
|
BEAST_EXPECT(ownerCount(env, issuer) == 0);
|
|
BEAST_EXPECT(ownerCount(env, minter) == 2);
|
|
BEAST_EXPECT(ownerCount(env, buyer) == 1);
|
|
|
|
// Broker is successful if they are the destination of both offers.
|
|
env(token::brokerOffers(
|
|
broker, offerBuyerToBroker, offerMinterToBroker));
|
|
env.close();
|
|
BEAST_EXPECT(ownerCount(env, issuer) == 0);
|
|
BEAST_EXPECT(ownerCount(env, minter) == 0);
|
|
BEAST_EXPECT(ownerCount(env, buyer) == 1);
|
|
}
|
|
}
|
|
|
|
void
|
|
testCreateOfferExpiration(FeatureBitset features)
|
|
{
|
|
// Explore the CreateOffer Expiration field.
|
|
testcase("Create offer expiration");
|
|
|
|
using namespace test::jtx;
|
|
|
|
Env env{*this, features};
|
|
|
|
Account const issuer{"issuer"};
|
|
Account const minter{"minter"};
|
|
Account const buyer{"buyer"};
|
|
|
|
env.fund(XRP(1000), issuer, minter, buyer);
|
|
|
|
// We want to explore how issuers vs minters fits into the permission
|
|
// scheme. So issuer issues and minter mints.
|
|
env(token::setMinter(issuer, minter));
|
|
env.close();
|
|
|
|
uint256 const nftokenID0 =
|
|
token::getNextID(env, issuer, 0, tfTransferable);
|
|
env(token::mint(minter, 0),
|
|
token::issuer(issuer),
|
|
txflags(tfTransferable));
|
|
env.close();
|
|
|
|
uint256 const nftokenID1 =
|
|
token::getNextID(env, issuer, 0, tfTransferable);
|
|
env(token::mint(minter, 0),
|
|
token::issuer(issuer),
|
|
txflags(tfTransferable));
|
|
env.close();
|
|
|
|
// Test how adding an Expiration field to an offer affects permissions
|
|
// for cancelling offers.
|
|
{
|
|
std::uint32_t const expiration = lastClose(env) + 25;
|
|
|
|
uint256 const offerMinterToIssuer =
|
|
keylet::nftoffer(minter, env.seq(minter)).key;
|
|
env(token::createOffer(minter, nftokenID0, drops(1)),
|
|
token::destination(issuer),
|
|
token::expiration(expiration),
|
|
txflags(tfSellNFToken));
|
|
|
|
uint256 const offerMinterToAnyone =
|
|
keylet::nftoffer(minter, env.seq(minter)).key;
|
|
env(token::createOffer(minter, nftokenID0, drops(1)),
|
|
token::expiration(expiration),
|
|
txflags(tfSellNFToken));
|
|
|
|
uint256 const offerIssuerToMinter =
|
|
keylet::nftoffer(issuer, env.seq(issuer)).key;
|
|
env(token::createOffer(issuer, nftokenID0, drops(1)),
|
|
token::owner(minter),
|
|
token::expiration(expiration));
|
|
|
|
uint256 const offerBuyerToMinter =
|
|
keylet::nftoffer(buyer, env.seq(buyer)).key;
|
|
env(token::createOffer(buyer, nftokenID0, drops(1)),
|
|
token::owner(minter),
|
|
token::expiration(expiration));
|
|
env.close();
|
|
BEAST_EXPECT(ownerCount(env, issuer) == 1);
|
|
BEAST_EXPECT(ownerCount(env, minter) == 3);
|
|
BEAST_EXPECT(ownerCount(env, buyer) == 1);
|
|
|
|
// Test who gets to cancel the offers. Anyone outside of the
|
|
// offer-owner/destination pair should not be able to cancel
|
|
// unexpired offers.
|
|
//
|
|
// Note that these are tec responses, so these transactions will
|
|
// not be retried by the ledger.
|
|
env(token::cancelOffer(issuer, {offerMinterToAnyone}),
|
|
ter(tecNO_PERMISSION));
|
|
env(token::cancelOffer(buyer, {offerIssuerToMinter}),
|
|
ter(tecNO_PERMISSION));
|
|
env.close();
|
|
BEAST_EXPECT(lastClose(env) < expiration);
|
|
BEAST_EXPECT(ownerCount(env, issuer) == 1);
|
|
BEAST_EXPECT(ownerCount(env, minter) == 3);
|
|
BEAST_EXPECT(ownerCount(env, buyer) == 1);
|
|
|
|
// The offer creator can cancel their own unexpired offer.
|
|
env(token::cancelOffer(minter, {offerMinterToAnyone}));
|
|
|
|
// The destination of a sell offer can cancel the NFT owner's
|
|
// unexpired offer.
|
|
env(token::cancelOffer(issuer, {offerMinterToIssuer}));
|
|
|
|
// Close enough ledgers to get past the expiration.
|
|
while (lastClose(env) < expiration)
|
|
env.close();
|
|
|
|
BEAST_EXPECT(ownerCount(env, issuer) == 1);
|
|
BEAST_EXPECT(ownerCount(env, minter) == 1);
|
|
BEAST_EXPECT(ownerCount(env, buyer) == 1);
|
|
|
|
// Anyone can cancel expired offers.
|
|
env(token::cancelOffer(issuer, {offerBuyerToMinter}));
|
|
env(token::cancelOffer(buyer, {offerIssuerToMinter}));
|
|
env.close();
|
|
BEAST_EXPECT(ownerCount(env, issuer) == 0);
|
|
BEAST_EXPECT(ownerCount(env, minter) == 1);
|
|
BEAST_EXPECT(ownerCount(env, buyer) == 0);
|
|
}
|
|
// Show that:
|
|
// 1. An unexpired sell offer with an expiration can be accepted.
|
|
// 2. An expired sell offer cannot be accepted and remains
|
|
// in ledger after the accept fails.
|
|
{
|
|
std::uint32_t const expiration = lastClose(env) + 25;
|
|
|
|
uint256 const offer0 =
|
|
keylet::nftoffer(minter, env.seq(minter)).key;
|
|
env(token::createOffer(minter, nftokenID0, drops(1)),
|
|
token::expiration(expiration),
|
|
txflags(tfSellNFToken));
|
|
|
|
uint256 const offer1 =
|
|
keylet::nftoffer(minter, env.seq(minter)).key;
|
|
env(token::createOffer(minter, nftokenID1, drops(1)),
|
|
token::expiration(expiration),
|
|
txflags(tfSellNFToken));
|
|
env.close();
|
|
BEAST_EXPECT(lastClose(env) < expiration);
|
|
BEAST_EXPECT(ownerCount(env, issuer) == 0);
|
|
BEAST_EXPECT(ownerCount(env, minter) == 3);
|
|
BEAST_EXPECT(ownerCount(env, buyer) == 0);
|
|
|
|
// Anyone can accept an unexpired sell offer.
|
|
env(token::acceptSellOffer(buyer, offer0));
|
|
|
|
// Close enough ledgers to get past the expiration.
|
|
while (lastClose(env) < expiration)
|
|
env.close();
|
|
|
|
BEAST_EXPECT(ownerCount(env, issuer) == 0);
|
|
BEAST_EXPECT(ownerCount(env, minter) == 2);
|
|
BEAST_EXPECT(ownerCount(env, buyer) == 1);
|
|
|
|
// No one can accept an expired sell offer.
|
|
env(token::acceptSellOffer(buyer, offer1), ter(tecEXPIRED));
|
|
env(token::acceptSellOffer(issuer, offer1), ter(tecEXPIRED));
|
|
env.close();
|
|
|
|
// The expired sell offer is still in the ledger.
|
|
BEAST_EXPECT(ownerCount(env, issuer) == 0);
|
|
BEAST_EXPECT(ownerCount(env, minter) == 2);
|
|
BEAST_EXPECT(ownerCount(env, buyer) == 1);
|
|
|
|
// Anyone can cancel the expired sell offer.
|
|
env(token::cancelOffer(issuer, {offer1}));
|
|
env.close();
|
|
BEAST_EXPECT(ownerCount(env, issuer) == 0);
|
|
BEAST_EXPECT(ownerCount(env, minter) == 1);
|
|
BEAST_EXPECT(ownerCount(env, buyer) == 1);
|
|
|
|
// Transfer nftokenID0 back to minter so we start the next test in
|
|
// a simple place.
|
|
uint256 const offerSellBack =
|
|
keylet::nftoffer(buyer, env.seq(buyer)).key;
|
|
env(token::createOffer(buyer, nftokenID0, XRP(0)),
|
|
txflags(tfSellNFToken),
|
|
token::destination(minter));
|
|
env.close();
|
|
env(token::acceptSellOffer(minter, offerSellBack));
|
|
env.close();
|
|
BEAST_EXPECT(ownerCount(env, issuer) == 0);
|
|
BEAST_EXPECT(ownerCount(env, minter) == 1);
|
|
BEAST_EXPECT(ownerCount(env, buyer) == 0);
|
|
}
|
|
// Show that:
|
|
// 1. An unexpired buy offer with an expiration can be accepted.
|
|
// 2. An expired buy offer cannot be accepted and remains
|
|
// in ledger after the accept fails.
|
|
{
|
|
std::uint32_t const expiration = lastClose(env) + 25;
|
|
|
|
uint256 const offer0 = keylet::nftoffer(buyer, env.seq(buyer)).key;
|
|
env(token::createOffer(buyer, nftokenID0, drops(1)),
|
|
token::owner(minter),
|
|
token::expiration(expiration));
|
|
|
|
uint256 const offer1 = keylet::nftoffer(buyer, env.seq(buyer)).key;
|
|
env(token::createOffer(buyer, nftokenID1, drops(1)),
|
|
token::owner(minter),
|
|
token::expiration(expiration));
|
|
env.close();
|
|
BEAST_EXPECT(lastClose(env) < expiration);
|
|
BEAST_EXPECT(ownerCount(env, issuer) == 0);
|
|
BEAST_EXPECT(ownerCount(env, minter) == 1);
|
|
BEAST_EXPECT(ownerCount(env, buyer) == 2);
|
|
|
|
// An unexpired buy offer can be accepted.
|
|
env(token::acceptBuyOffer(minter, offer0));
|
|
|
|
// Close enough ledgers to get past the expiration.
|
|
while (lastClose(env) < expiration)
|
|
env.close();
|
|
|
|
BEAST_EXPECT(ownerCount(env, issuer) == 0);
|
|
BEAST_EXPECT(ownerCount(env, minter) == 1);
|
|
BEAST_EXPECT(ownerCount(env, buyer) == 2);
|
|
|
|
// An expired buy offer cannot be accepted.
|
|
env(token::acceptBuyOffer(minter, offer1), ter(tecEXPIRED));
|
|
env(token::acceptBuyOffer(issuer, offer1), ter(tecEXPIRED));
|
|
env.close();
|
|
|
|
// The expired buy offer is still in the ledger.
|
|
BEAST_EXPECT(ownerCount(env, issuer) == 0);
|
|
BEAST_EXPECT(ownerCount(env, minter) == 1);
|
|
BEAST_EXPECT(ownerCount(env, buyer) == 2);
|
|
|
|
// Anyone can cancel the expired buy offer.
|
|
env(token::cancelOffer(issuer, {offer1}));
|
|
env.close();
|
|
BEAST_EXPECT(ownerCount(env, issuer) == 0);
|
|
BEAST_EXPECT(ownerCount(env, minter) == 1);
|
|
BEAST_EXPECT(ownerCount(env, buyer) == 1);
|
|
|
|
// Transfer nftokenID0 back to minter so we start the next test in
|
|
// a simple place.
|
|
uint256 const offerSellBack =
|
|
keylet::nftoffer(buyer, env.seq(buyer)).key;
|
|
env(token::createOffer(buyer, nftokenID0, XRP(0)),
|
|
txflags(tfSellNFToken),
|
|
token::destination(minter));
|
|
env.close();
|
|
env(token::acceptSellOffer(minter, offerSellBack));
|
|
env.close();
|
|
BEAST_EXPECT(ownerCount(env, issuer) == 0);
|
|
BEAST_EXPECT(ownerCount(env, minter) == 1);
|
|
BEAST_EXPECT(ownerCount(env, buyer) == 0);
|
|
}
|
|
// Show that in brokered mode:
|
|
// 1. An unexpired sell offer with an expiration can be accepted.
|
|
// 2. An expired sell offer cannot be accepted and remains
|
|
// in ledger after the accept fails.
|
|
{
|
|
std::uint32_t const expiration = lastClose(env) + 25;
|
|
|
|
uint256 const sellOffer0 =
|
|
keylet::nftoffer(minter, env.seq(minter)).key;
|
|
env(token::createOffer(minter, nftokenID0, drops(1)),
|
|
token::expiration(expiration),
|
|
txflags(tfSellNFToken));
|
|
|
|
uint256 const sellOffer1 =
|
|
keylet::nftoffer(minter, env.seq(minter)).key;
|
|
env(token::createOffer(minter, nftokenID1, drops(1)),
|
|
token::expiration(expiration),
|
|
txflags(tfSellNFToken));
|
|
|
|
uint256 const buyOffer0 =
|
|
keylet::nftoffer(buyer, env.seq(buyer)).key;
|
|
env(token::createOffer(buyer, nftokenID0, drops(1)),
|
|
token::owner(minter));
|
|
|
|
uint256 const buyOffer1 =
|
|
keylet::nftoffer(buyer, env.seq(buyer)).key;
|
|
env(token::createOffer(buyer, nftokenID1, drops(1)),
|
|
token::owner(minter));
|
|
|
|
env.close();
|
|
BEAST_EXPECT(lastClose(env) < expiration);
|
|
BEAST_EXPECT(ownerCount(env, issuer) == 0);
|
|
BEAST_EXPECT(ownerCount(env, minter) == 3);
|
|
BEAST_EXPECT(ownerCount(env, buyer) == 2);
|
|
|
|
// An unexpired offer can be brokered.
|
|
env(token::brokerOffers(issuer, buyOffer0, sellOffer0));
|
|
|
|
// Close enough ledgers to get past the expiration.
|
|
while (lastClose(env) < expiration)
|
|
env.close();
|
|
|
|
BEAST_EXPECT(ownerCount(env, issuer) == 0);
|
|
BEAST_EXPECT(ownerCount(env, minter) == 2);
|
|
BEAST_EXPECT(ownerCount(env, buyer) == 2);
|
|
|
|
// If the sell offer is expired it cannot be brokered.
|
|
env(token::brokerOffers(issuer, buyOffer1, sellOffer1),
|
|
ter(tecEXPIRED));
|
|
env.close();
|
|
|
|
// The expired sell offer is still in the ledger.
|
|
BEAST_EXPECT(ownerCount(env, issuer) == 0);
|
|
BEAST_EXPECT(ownerCount(env, minter) == 2);
|
|
BEAST_EXPECT(ownerCount(env, buyer) == 2);
|
|
|
|
// Anyone can cancel the expired sell offer.
|
|
env(token::cancelOffer(buyer, {buyOffer1, sellOffer1}));
|
|
env.close();
|
|
BEAST_EXPECT(ownerCount(env, issuer) == 0);
|
|
BEAST_EXPECT(ownerCount(env, minter) == 1);
|
|
BEAST_EXPECT(ownerCount(env, buyer) == 1);
|
|
|
|
// Transfer nftokenID0 back to minter so we start the next test in
|
|
// a simple place.
|
|
uint256 const offerSellBack =
|
|
keylet::nftoffer(buyer, env.seq(buyer)).key;
|
|
env(token::createOffer(buyer, nftokenID0, XRP(0)),
|
|
txflags(tfSellNFToken),
|
|
token::destination(minter));
|
|
env.close();
|
|
env(token::acceptSellOffer(minter, offerSellBack));
|
|
env.close();
|
|
BEAST_EXPECT(ownerCount(env, issuer) == 0);
|
|
BEAST_EXPECT(ownerCount(env, minter) == 1);
|
|
BEAST_EXPECT(ownerCount(env, buyer) == 0);
|
|
}
|
|
// Show that in brokered mode:
|
|
// 1. An unexpired buy offer with an expiration can be accepted.
|
|
// 2. An expired buy offer cannot be accepted and remains
|
|
// in ledger after the accept fails.
|
|
{
|
|
std::uint32_t const expiration = lastClose(env) + 25;
|
|
|
|
uint256 const sellOffer0 =
|
|
keylet::nftoffer(minter, env.seq(minter)).key;
|
|
env(token::createOffer(minter, nftokenID0, drops(1)),
|
|
txflags(tfSellNFToken));
|
|
|
|
uint256 const sellOffer1 =
|
|
keylet::nftoffer(minter, env.seq(minter)).key;
|
|
env(token::createOffer(minter, nftokenID1, drops(1)),
|
|
txflags(tfSellNFToken));
|
|
|
|
uint256 const buyOffer0 =
|
|
keylet::nftoffer(buyer, env.seq(buyer)).key;
|
|
env(token::createOffer(buyer, nftokenID0, drops(1)),
|
|
token::expiration(expiration),
|
|
token::owner(minter));
|
|
|
|
uint256 const buyOffer1 =
|
|
keylet::nftoffer(buyer, env.seq(buyer)).key;
|
|
env(token::createOffer(buyer, nftokenID1, drops(1)),
|
|
token::expiration(expiration),
|
|
token::owner(minter));
|
|
|
|
env.close();
|
|
BEAST_EXPECT(lastClose(env) < expiration);
|
|
BEAST_EXPECT(ownerCount(env, issuer) == 0);
|
|
BEAST_EXPECT(ownerCount(env, minter) == 3);
|
|
BEAST_EXPECT(ownerCount(env, buyer) == 2);
|
|
|
|
// An unexpired offer can be brokered.
|
|
env(token::brokerOffers(issuer, buyOffer0, sellOffer0));
|
|
|
|
// Close enough ledgers to get past the expiration.
|
|
while (lastClose(env) < expiration)
|
|
env.close();
|
|
|
|
BEAST_EXPECT(ownerCount(env, issuer) == 0);
|
|
BEAST_EXPECT(ownerCount(env, minter) == 2);
|
|
BEAST_EXPECT(ownerCount(env, buyer) == 2);
|
|
|
|
// If the buy offer is expired it cannot be brokered.
|
|
env(token::brokerOffers(issuer, buyOffer1, sellOffer1),
|
|
ter(tecEXPIRED));
|
|
env.close();
|
|
|
|
// The expired buy offer is still in the ledger.
|
|
BEAST_EXPECT(ownerCount(env, issuer) == 0);
|
|
BEAST_EXPECT(ownerCount(env, minter) == 2);
|
|
BEAST_EXPECT(ownerCount(env, buyer) == 2);
|
|
|
|
// Anyone can cancel the expired buy offer.
|
|
env(token::cancelOffer(minter, {buyOffer1, sellOffer1}));
|
|
env.close();
|
|
BEAST_EXPECT(ownerCount(env, issuer) == 0);
|
|
BEAST_EXPECT(ownerCount(env, minter) == 1);
|
|
BEAST_EXPECT(ownerCount(env, buyer) == 1);
|
|
|
|
// Transfer nftokenID0 back to minter so we start the next test in
|
|
// a simple place.
|
|
uint256 const offerSellBack =
|
|
keylet::nftoffer(buyer, env.seq(buyer)).key;
|
|
env(token::createOffer(buyer, nftokenID0, XRP(0)),
|
|
txflags(tfSellNFToken),
|
|
token::destination(minter));
|
|
env.close();
|
|
env(token::acceptSellOffer(minter, offerSellBack));
|
|
env.close();
|
|
BEAST_EXPECT(ownerCount(env, issuer) == 0);
|
|
BEAST_EXPECT(ownerCount(env, minter) == 1);
|
|
BEAST_EXPECT(ownerCount(env, buyer) == 0);
|
|
}
|
|
// Show that in brokered mode:
|
|
// 1. An unexpired buy/sell offer pair with an expiration can be
|
|
// accepted.
|
|
// 2. An expired buy/sell offer pair cannot be accepted and they
|
|
// remain in ledger after the accept fails.
|
|
{
|
|
std::uint32_t const expiration = lastClose(env) + 25;
|
|
|
|
uint256 const sellOffer0 =
|
|
keylet::nftoffer(minter, env.seq(minter)).key;
|
|
env(token::createOffer(minter, nftokenID0, drops(1)),
|
|
token::expiration(expiration),
|
|
txflags(tfSellNFToken));
|
|
|
|
uint256 const sellOffer1 =
|
|
keylet::nftoffer(minter, env.seq(minter)).key;
|
|
env(token::createOffer(minter, nftokenID1, drops(1)),
|
|
token::expiration(expiration),
|
|
txflags(tfSellNFToken));
|
|
|
|
uint256 const buyOffer0 =
|
|
keylet::nftoffer(buyer, env.seq(buyer)).key;
|
|
env(token::createOffer(buyer, nftokenID0, drops(1)),
|
|
token::expiration(expiration),
|
|
token::owner(minter));
|
|
|
|
uint256 const buyOffer1 =
|
|
keylet::nftoffer(buyer, env.seq(buyer)).key;
|
|
env(token::createOffer(buyer, nftokenID1, drops(1)),
|
|
token::expiration(expiration),
|
|
token::owner(minter));
|
|
|
|
env.close();
|
|
BEAST_EXPECT(lastClose(env) < expiration);
|
|
BEAST_EXPECT(ownerCount(env, issuer) == 0);
|
|
BEAST_EXPECT(ownerCount(env, minter) == 3);
|
|
BEAST_EXPECT(ownerCount(env, buyer) == 2);
|
|
|
|
// Unexpired offers can be brokered.
|
|
env(token::brokerOffers(issuer, buyOffer0, sellOffer0));
|
|
|
|
// Close enough ledgers to get past the expiration.
|
|
while (lastClose(env) < expiration)
|
|
env.close();
|
|
|
|
BEAST_EXPECT(ownerCount(env, issuer) == 0);
|
|
BEAST_EXPECT(ownerCount(env, minter) == 2);
|
|
BEAST_EXPECT(ownerCount(env, buyer) == 2);
|
|
|
|
// If the offers are expired they cannot be brokered.
|
|
env(token::brokerOffers(issuer, buyOffer1, sellOffer1),
|
|
ter(tecEXPIRED));
|
|
env.close();
|
|
|
|
// The expired offers are still in the ledger.
|
|
BEAST_EXPECT(ownerCount(env, issuer) == 0);
|
|
BEAST_EXPECT(ownerCount(env, minter) == 2);
|
|
BEAST_EXPECT(ownerCount(env, buyer) == 2);
|
|
|
|
// Anyone can cancel the expired offers.
|
|
env(token::cancelOffer(issuer, {buyOffer1, sellOffer1}));
|
|
env.close();
|
|
BEAST_EXPECT(ownerCount(env, issuer) == 0);
|
|
BEAST_EXPECT(ownerCount(env, minter) == 1);
|
|
BEAST_EXPECT(ownerCount(env, buyer) == 1);
|
|
|
|
// Transfer nftokenID0 back to minter so we start the next test in
|
|
// a simple place.
|
|
uint256 const offerSellBack =
|
|
keylet::nftoffer(buyer, env.seq(buyer)).key;
|
|
env(token::createOffer(buyer, nftokenID0, XRP(0)),
|
|
txflags(tfSellNFToken),
|
|
token::destination(minter));
|
|
env.close();
|
|
env(token::acceptSellOffer(minter, offerSellBack));
|
|
env.close();
|
|
BEAST_EXPECT(ownerCount(env, issuer) == 0);
|
|
BEAST_EXPECT(ownerCount(env, minter) == 1);
|
|
BEAST_EXPECT(ownerCount(env, buyer) == 0);
|
|
}
|
|
}
|
|
|
|
void
|
|
testCancelOffers(FeatureBitset features)
|
|
{
|
|
// Look at offer canceling.
|
|
testcase("Cancel offers");
|
|
|
|
using namespace test::jtx;
|
|
|
|
Env env{*this, features};
|
|
|
|
Account const alice("alice");
|
|
Account const becky("becky");
|
|
Account const minter("minter");
|
|
env.fund(XRP(50000), alice, becky, minter);
|
|
env.close();
|
|
|
|
// alice has a minter to see if minters have offer canceling permission.
|
|
env(token::setMinter(alice, minter));
|
|
env.close();
|
|
|
|
uint256 const nftokenID =
|
|
token::getNextID(env, alice, 0, tfTransferable);
|
|
env(token::mint(alice, 0), txflags(tfTransferable));
|
|
env.close();
|
|
|
|
// Anyone can cancel an expired offer.
|
|
uint256 const expiredOfferIndex =
|
|
keylet::nftoffer(alice, env.seq(alice)).key;
|
|
|
|
env(token::createOffer(alice, nftokenID, XRP(1000)),
|
|
txflags(tfSellNFToken),
|
|
token::expiration(lastClose(env) + 13));
|
|
env.close();
|
|
|
|
// The offer has not expired yet, so becky can't cancel it now.
|
|
BEAST_EXPECT(ownerCount(env, alice) == 2);
|
|
env(token::cancelOffer(becky, {expiredOfferIndex}),
|
|
ter(tecNO_PERMISSION));
|
|
env.close();
|
|
|
|
// Close a couple of ledgers and advance the time. Then becky
|
|
// should be able to cancel the (now) expired offer.
|
|
env.close();
|
|
env.close();
|
|
env(token::cancelOffer(becky, {expiredOfferIndex}));
|
|
env.close();
|
|
BEAST_EXPECT(ownerCount(env, alice) == 1);
|
|
|
|
// Create a couple of offers with a destination. Those offers
|
|
// should be cancellable by the creator and the destination.
|
|
uint256 const dest1OfferIndex =
|
|
keylet::nftoffer(alice, env.seq(alice)).key;
|
|
|
|
env(token::createOffer(alice, nftokenID, XRP(1000)),
|
|
token::destination(becky),
|
|
txflags(tfSellNFToken));
|
|
env.close();
|
|
BEAST_EXPECT(ownerCount(env, alice) == 2);
|
|
|
|
// Minter can't cancel that offer, but becky (the destination) can.
|
|
env(token::cancelOffer(minter, {dest1OfferIndex}),
|
|
ter(tecNO_PERMISSION));
|
|
env.close();
|
|
BEAST_EXPECT(ownerCount(env, alice) == 2);
|
|
|
|
env(token::cancelOffer(becky, {dest1OfferIndex}));
|
|
env.close();
|
|
BEAST_EXPECT(ownerCount(env, alice) == 1);
|
|
|
|
// alice can cancel her own offer, even if becky is the destination.
|
|
uint256 const dest2OfferIndex =
|
|
keylet::nftoffer(alice, env.seq(alice)).key;
|
|
|
|
env(token::createOffer(alice, nftokenID, XRP(1000)),
|
|
token::destination(becky),
|
|
txflags(tfSellNFToken));
|
|
env.close();
|
|
BEAST_EXPECT(ownerCount(env, alice) == 2);
|
|
|
|
env(token::cancelOffer(alice, {dest2OfferIndex}));
|
|
env.close();
|
|
BEAST_EXPECT(ownerCount(env, alice) == 1);
|
|
|
|
// The issuer has no special permissions regarding offer cancellation.
|
|
// Minter creates a token with alice as issuer. alice cannot cancel
|
|
// minter's offer.
|
|
uint256 const mintersNFTokenID =
|
|
token::getNextID(env, alice, 0, tfTransferable);
|
|
env(token::mint(minter, 0),
|
|
token::issuer(alice),
|
|
txflags(tfTransferable));
|
|
env.close();
|
|
|
|
uint256 const minterOfferIndex =
|
|
keylet::nftoffer(minter, env.seq(minter)).key;
|
|
|
|
env(token::createOffer(minter, mintersNFTokenID, XRP(1000)),
|
|
txflags(tfSellNFToken));
|
|
env.close();
|
|
BEAST_EXPECT(ownerCount(env, minter) == 2);
|
|
|
|
// Nobody other than minter should be able to cancel minter's offer.
|
|
env(token::cancelOffer(alice, {minterOfferIndex}),
|
|
ter(tecNO_PERMISSION));
|
|
env(token::cancelOffer(becky, {minterOfferIndex}),
|
|
ter(tecNO_PERMISSION));
|
|
env.close();
|
|
BEAST_EXPECT(ownerCount(env, minter) == 2);
|
|
|
|
env(token::cancelOffer(minter, {minterOfferIndex}));
|
|
env.close();
|
|
BEAST_EXPECT(ownerCount(env, minter) == 1);
|
|
}
|
|
|
|
void
|
|
testCancelTooManyOffers(FeatureBitset features)
|
|
{
|
|
// Look at the case where too many offers are passed in a cancel.
|
|
testcase("Cancel too many offers");
|
|
|
|
using namespace test::jtx;
|
|
|
|
Env env{*this, features};
|
|
|
|
// We want to maximize the metadata from a cancel offer transaction to
|
|
// make sure we don't hit metadata limits. The way we'll do that is:
|
|
//
|
|
// 1. Generate twice as many separate funded accounts as we have
|
|
// offers.
|
|
// 2.
|
|
// a. One of these accounts mints an NFT with a full URL.
|
|
// b. The other account makes an offer that will expire soon.
|
|
// 3. After all of these offers have expired, cancel all of the
|
|
// expired offers in a single transaction.
|
|
//
|
|
// I can't think of any way to increase the metadata beyond this,
|
|
// but I'm open to ideas.
|
|
Account const alice("alice");
|
|
env.fund(XRP(1000), alice);
|
|
env.close();
|
|
|
|
std::string const uri(maxTokenURILength, '?');
|
|
std::vector<uint256> offerIndexes;
|
|
offerIndexes.reserve(maxTokenOfferCancelCount + 1);
|
|
for (uint32_t i = 0; i < maxTokenOfferCancelCount + 1; ++i)
|
|
{
|
|
Account const nftAcct(std::string("nftAcct") + std::to_string(i));
|
|
Account const offerAcct(
|
|
std::string("offerAcct") + std::to_string(i));
|
|
env.fund(XRP(1000), nftAcct, offerAcct);
|
|
env.close();
|
|
|
|
uint256 const nftokenID =
|
|
token::getNextID(env, nftAcct, 0, tfTransferable);
|
|
env(token::mint(nftAcct, 0),
|
|
token::uri(uri),
|
|
txflags(tfTransferable));
|
|
env.close();
|
|
|
|
offerIndexes.push_back(
|
|
keylet::nftoffer(offerAcct, env.seq(offerAcct)).key);
|
|
env(token::createOffer(offerAcct, nftokenID, drops(1)),
|
|
token::owner(nftAcct),
|
|
token::expiration(lastClose(env) + 5));
|
|
env.close();
|
|
}
|
|
|
|
// Close the ledger so the last of the offers expire.
|
|
env.close();
|
|
|
|
// All offers should be in the ledger.
|
|
for (uint256 const& offerIndex : offerIndexes)
|
|
{
|
|
BEAST_EXPECT(env.le(keylet::nftoffer(offerIndex)));
|
|
}
|
|
|
|
// alice attempts to cancel all of the expired offers. There is one
|
|
// too many so the request fails.
|
|
env(token::cancelOffer(alice, offerIndexes), ter(temMALFORMED));
|
|
env.close();
|
|
|
|
// However alice can cancel just one of the offers.
|
|
env(token::cancelOffer(alice, {offerIndexes.back()}));
|
|
env.close();
|
|
|
|
// Verify that offer is gone from the ledger.
|
|
BEAST_EXPECT(!env.le(keylet::nftoffer(offerIndexes.back())));
|
|
offerIndexes.pop_back();
|
|
|
|
// But alice adds a sell offer to the list...
|
|
{
|
|
uint256 const nftokenID =
|
|
token::getNextID(env, alice, 0, tfTransferable);
|
|
env(token::mint(alice, 0),
|
|
token::uri(uri),
|
|
txflags(tfTransferable));
|
|
env.close();
|
|
|
|
offerIndexes.push_back(keylet::nftoffer(alice, env.seq(alice)).key);
|
|
env(token::createOffer(alice, nftokenID, drops(1)),
|
|
txflags(tfSellNFToken));
|
|
env.close();
|
|
|
|
// alice's owner count should now to 2 for the nft and the offer.
|
|
BEAST_EXPECT(ownerCount(env, alice) == 2);
|
|
|
|
// Because alice added the sell offer there are still too many
|
|
// offers in the list to cancel.
|
|
env(token::cancelOffer(alice, offerIndexes), ter(temMALFORMED));
|
|
env.close();
|
|
|
|
// alice burns her nft which removes the nft and the offer.
|
|
env(token::burn(alice, nftokenID));
|
|
env.close();
|
|
|
|
// If alice's owner count is zero we can see that the offer
|
|
// and nft are both gone.
|
|
BEAST_EXPECT(ownerCount(env, alice) == 0);
|
|
offerIndexes.pop_back();
|
|
}
|
|
|
|
// Now there are few enough offers in the list that they can all
|
|
// be cancelled in a single transaction.
|
|
env(token::cancelOffer(alice, offerIndexes));
|
|
env.close();
|
|
|
|
// Verify that remaining offers are gone from the ledger.
|
|
for (uint256 const& offerIndex : offerIndexes)
|
|
{
|
|
BEAST_EXPECT(!env.le(keylet::nftoffer(offerIndex)));
|
|
}
|
|
}
|
|
|
|
void
|
|
testBrokeredAccept(FeatureBitset features)
|
|
{
|
|
// Look at the case where too many offers are passed in a cancel.
|
|
testcase("Brokered NFT offer accept");
|
|
|
|
using namespace test::jtx;
|
|
|
|
Env env{*this, features};
|
|
|
|
// The most important thing to explore here is the way funds are
|
|
// assigned from the buyer to...
|
|
// o the Seller,
|
|
// o the Broker, and
|
|
// o the Issuer (in the case of a transfer fee).
|
|
|
|
Account const issuer{"issuer"};
|
|
Account const minter{"minter"};
|
|
Account const buyer{"buyer"};
|
|
Account const broker{"broker"};
|
|
Account const gw{"gw"};
|
|
IOU const gwXAU(gw["XAU"]);
|
|
|
|
env.fund(XRP(1000), issuer, minter, buyer, broker, gw);
|
|
env.close();
|
|
|
|
env(trust(issuer, gwXAU(2000)));
|
|
env(trust(minter, gwXAU(2000)));
|
|
env(trust(buyer, gwXAU(2000)));
|
|
env(trust(broker, gwXAU(2000)));
|
|
env.close();
|
|
|
|
env(token::setMinter(issuer, minter));
|
|
env.close();
|
|
|
|
// Lambda to check owner count of all accounts is one.
|
|
auto checkOwnerCountIsOne =
|
|
[this, &env](
|
|
std::initializer_list<std::reference_wrapper<Account const>>
|
|
accounts,
|
|
int line) {
|
|
for (Account const& acct : accounts)
|
|
{
|
|
if (std::uint32_t ownerCount = this->ownerCount(env, acct);
|
|
ownerCount != 1)
|
|
{
|
|
std::stringstream ss;
|
|
ss << "Account " << acct.human()
|
|
<< " expected ownerCount == 1. Got " << ownerCount;
|
|
fail(ss.str(), __FILE__, line);
|
|
}
|
|
}
|
|
};
|
|
|
|
// Lambda that mints an NFT and returns the nftID.
|
|
auto mintNFT = [&env, &issuer, &minter](std::uint16_t xferFee = 0) {
|
|
uint256 const nftID =
|
|
token::getNextID(env, issuer, 0, tfTransferable, xferFee);
|
|
env(token::mint(minter, 0),
|
|
token::issuer(issuer),
|
|
token::xferFee(xferFee),
|
|
txflags(tfTransferable));
|
|
env.close();
|
|
return nftID;
|
|
};
|
|
|
|
// o Seller is selling for zero XRP.
|
|
// o Broker charges no fee.
|
|
// o No transfer fee.
|
|
//
|
|
// Since minter is selling for zero the currency must be XRP.
|
|
{
|
|
checkOwnerCountIsOne({issuer, minter, buyer, broker}, __LINE__);
|
|
|
|
uint256 const nftID = mintNFT();
|
|
|
|
// minter creates their offer.
|
|
uint256 const minterOfferIndex =
|
|
keylet::nftoffer(minter, env.seq(minter)).key;
|
|
env(token::createOffer(minter, nftID, XRP(0)),
|
|
txflags(tfSellNFToken));
|
|
env.close();
|
|
|
|
// buyer creates their offer. Note: a buy offer can never
|
|
// offer zero.
|
|
uint256 const buyOfferIndex =
|
|
keylet::nftoffer(buyer, env.seq(buyer)).key;
|
|
env(token::createOffer(buyer, nftID, XRP(1)), token::owner(minter));
|
|
env.close();
|
|
|
|
auto const minterBalance = env.balance(minter);
|
|
auto const buyerBalance = env.balance(buyer);
|
|
auto const brokerBalance = env.balance(broker);
|
|
auto const issuerBalance = env.balance(issuer);
|
|
|
|
// Broker charges no brokerFee.
|
|
env(token::brokerOffers(broker, buyOfferIndex, minterOfferIndex));
|
|
env.close();
|
|
|
|
// Note that minter's XRP balance goes up even though they
|
|
// requested XRP(0).
|
|
BEAST_EXPECT(env.balance(minter) == minterBalance + XRP(1));
|
|
BEAST_EXPECT(env.balance(buyer) == buyerBalance - XRP(1));
|
|
BEAST_EXPECT(env.balance(broker) == brokerBalance - drops(10));
|
|
BEAST_EXPECT(env.balance(issuer) == issuerBalance);
|
|
|
|
// Burn the NFT so the next test starts with a clean state.
|
|
env(token::burn(buyer, nftID));
|
|
env.close();
|
|
}
|
|
|
|
// o Seller is selling for zero XRP.
|
|
// o Broker charges a fee.
|
|
// o No transfer fee.
|
|
//
|
|
// Since minter is selling for zero the currency must be XRP.
|
|
{
|
|
checkOwnerCountIsOne({issuer, minter, buyer, broker}, __LINE__);
|
|
|
|
uint256 const nftID = mintNFT();
|
|
|
|
// minter creates their offer.
|
|
uint256 const minterOfferIndex =
|
|
keylet::nftoffer(minter, env.seq(minter)).key;
|
|
env(token::createOffer(minter, nftID, XRP(0)),
|
|
txflags(tfSellNFToken));
|
|
env.close();
|
|
|
|
// buyer creates their offer. Note: a buy offer can never
|
|
// offer zero.
|
|
uint256 const buyOfferIndex =
|
|
keylet::nftoffer(buyer, env.seq(buyer)).key;
|
|
env(token::createOffer(buyer, nftID, XRP(1)), token::owner(minter));
|
|
env.close();
|
|
|
|
// Broker attempts to charge a 1.1 XRP brokerFee and fails.
|
|
env(token::brokerOffers(broker, buyOfferIndex, minterOfferIndex),
|
|
token::brokerFee(XRP(1.1)),
|
|
ter(tecINSUFFICIENT_PAYMENT));
|
|
env.close();
|
|
|
|
auto const minterBalance = env.balance(minter);
|
|
auto const buyerBalance = env.balance(buyer);
|
|
auto const brokerBalance = env.balance(broker);
|
|
auto const issuerBalance = env.balance(issuer);
|
|
|
|
// Broker charges a 0.5 XRP brokerFee.
|
|
env(token::brokerOffers(broker, buyOfferIndex, minterOfferIndex),
|
|
token::brokerFee(XRP(0.5)));
|
|
env.close();
|
|
|
|
// Note that minter's XRP balance goes up even though they
|
|
// requested XRP(0).
|
|
BEAST_EXPECT(env.balance(minter) == minterBalance + XRP(0.5));
|
|
BEAST_EXPECT(env.balance(buyer) == buyerBalance - XRP(1));
|
|
BEAST_EXPECT(
|
|
env.balance(broker) == brokerBalance + XRP(0.5) - drops(10));
|
|
BEAST_EXPECT(env.balance(issuer) == issuerBalance);
|
|
|
|
// Burn the NFT so the next test starts with a clean state.
|
|
env(token::burn(buyer, nftID));
|
|
env.close();
|
|
}
|
|
|
|
// o Seller is selling for zero XRP.
|
|
// o Broker charges no fee.
|
|
// o 50% transfer fee.
|
|
//
|
|
// Since minter is selling for zero the currency must be XRP.
|
|
{
|
|
checkOwnerCountIsOne({issuer, minter, buyer, broker}, __LINE__);
|
|
|
|
uint256 const nftID = mintNFT(maxTransferFee);
|
|
|
|
// minter creates their offer.
|
|
uint256 const minterOfferIndex =
|
|
keylet::nftoffer(minter, env.seq(minter)).key;
|
|
env(token::createOffer(minter, nftID, XRP(0)),
|
|
txflags(tfSellNFToken));
|
|
env.close();
|
|
|
|
// buyer creates their offer. Note: a buy offer can never
|
|
// offer zero.
|
|
uint256 const buyOfferIndex =
|
|
keylet::nftoffer(buyer, env.seq(buyer)).key;
|
|
env(token::createOffer(buyer, nftID, XRP(1)), token::owner(minter));
|
|
env.close();
|
|
|
|
auto const minterBalance = env.balance(minter);
|
|
auto const buyerBalance = env.balance(buyer);
|
|
auto const brokerBalance = env.balance(broker);
|
|
auto const issuerBalance = env.balance(issuer);
|
|
|
|
// Broker charges no brokerFee.
|
|
env(token::brokerOffers(broker, buyOfferIndex, minterOfferIndex));
|
|
env.close();
|
|
|
|
// Note that minter's XRP balance goes up even though they
|
|
// requested XRP(0).
|
|
BEAST_EXPECT(env.balance(minter) == minterBalance + XRP(0.5));
|
|
BEAST_EXPECT(env.balance(buyer) == buyerBalance - XRP(1));
|
|
BEAST_EXPECT(env.balance(broker) == brokerBalance - drops(10));
|
|
BEAST_EXPECT(env.balance(issuer) == issuerBalance + XRP(0.5));
|
|
|
|
// Burn the NFT so the next test starts with a clean state.
|
|
env(token::burn(buyer, nftID));
|
|
env.close();
|
|
}
|
|
|
|
// o Seller is selling for zero XRP.
|
|
// o Broker charges 0.5 XRP.
|
|
// o 50% transfer fee.
|
|
//
|
|
// Since minter is selling for zero the currency must be XRP.
|
|
{
|
|
checkOwnerCountIsOne({issuer, minter, buyer, broker}, __LINE__);
|
|
|
|
uint256 const nftID = mintNFT(maxTransferFee);
|
|
|
|
// minter creates their offer.
|
|
uint256 const minterOfferIndex =
|
|
keylet::nftoffer(minter, env.seq(minter)).key;
|
|
env(token::createOffer(minter, nftID, XRP(0)),
|
|
txflags(tfSellNFToken));
|
|
env.close();
|
|
|
|
// buyer creates their offer. Note: a buy offer can never
|
|
// offer zero.
|
|
uint256 const buyOfferIndex =
|
|
keylet::nftoffer(buyer, env.seq(buyer)).key;
|
|
env(token::createOffer(buyer, nftID, XRP(1)), token::owner(minter));
|
|
env.close();
|
|
|
|
auto const minterBalance = env.balance(minter);
|
|
auto const buyerBalance = env.balance(buyer);
|
|
auto const brokerBalance = env.balance(broker);
|
|
auto const issuerBalance = env.balance(issuer);
|
|
|
|
// Broker charges a 0.75 XRP brokerFee.
|
|
env(token::brokerOffers(broker, buyOfferIndex, minterOfferIndex),
|
|
token::brokerFee(XRP(0.75)));
|
|
env.close();
|
|
|
|
// Note that, with a 50% transfer fee, issuer gets 1/2 of what's
|
|
// left _after_ broker takes their fee. minter gets the remainder
|
|
// after both broker and minter take their cuts
|
|
BEAST_EXPECT(env.balance(minter) == minterBalance + XRP(0.125));
|
|
BEAST_EXPECT(env.balance(buyer) == buyerBalance - XRP(1));
|
|
BEAST_EXPECT(
|
|
env.balance(broker) == brokerBalance + XRP(0.75) - drops(10));
|
|
BEAST_EXPECT(env.balance(issuer) == issuerBalance + XRP(0.125));
|
|
|
|
// Burn the NFT so the next test starts with a clean state.
|
|
env(token::burn(buyer, nftID));
|
|
env.close();
|
|
}
|
|
|
|
// Lambda to set the balance of all passed in accounts to gwXAU(1000).
|
|
auto setXAUBalance_1000 =
|
|
[this, &gw, &gwXAU, &env](
|
|
std::initializer_list<std::reference_wrapper<Account const>>
|
|
accounts,
|
|
int line) {
|
|
for (Account const& acct : accounts)
|
|
{
|
|
static const auto xau1000 = gwXAU(1000);
|
|
auto const balance = env.balance(acct, gwXAU);
|
|
if (balance < xau1000)
|
|
{
|
|
env(pay(gw, acct, xau1000 - balance));
|
|
env.close();
|
|
}
|
|
else if (balance > xau1000)
|
|
{
|
|
env(pay(acct, gw, balance - xau1000));
|
|
env.close();
|
|
}
|
|
if (env.balance(acct, gwXAU) != xau1000)
|
|
{
|
|
std::stringstream ss;
|
|
ss << "Unable to set " << acct.human()
|
|
<< " account balance to gwXAU(1000)";
|
|
this->fail(ss.str(), __FILE__, line);
|
|
}
|
|
}
|
|
};
|
|
|
|
// The buyer and seller have identical amounts and there is no
|
|
// transfer fee.
|
|
{
|
|
checkOwnerCountIsOne({issuer, minter, buyer, broker}, __LINE__);
|
|
setXAUBalance_1000({issuer, minter, buyer, broker}, __LINE__);
|
|
|
|
uint256 const nftID = mintNFT();
|
|
|
|
// minter creates their offer.
|
|
uint256 const minterOfferIndex =
|
|
keylet::nftoffer(minter, env.seq(minter)).key;
|
|
env(token::createOffer(minter, nftID, gwXAU(1000)),
|
|
txflags(tfSellNFToken));
|
|
env.close();
|
|
|
|
{
|
|
// buyer creates an offer for more XAU than they currently own.
|
|
uint256 const buyOfferIndex =
|
|
keylet::nftoffer(buyer, env.seq(buyer)).key;
|
|
env(token::createOffer(buyer, nftID, gwXAU(1001)),
|
|
token::owner(minter));
|
|
env.close();
|
|
|
|
// broker attempts to broker the offers but cannot.
|
|
env(token::brokerOffers(
|
|
broker, buyOfferIndex, minterOfferIndex),
|
|
ter(tecINSUFFICIENT_FUNDS));
|
|
env.close();
|
|
|
|
// Cancel buyer's bad offer so the next test starts in a
|
|
// clean state.
|
|
env(token::cancelOffer(buyer, {buyOfferIndex}));
|
|
env.close();
|
|
}
|
|
{
|
|
// buyer creates an offer for less that what minter is asking.
|
|
uint256 const buyOfferIndex =
|
|
keylet::nftoffer(buyer, env.seq(buyer)).key;
|
|
env(token::createOffer(buyer, nftID, gwXAU(999)),
|
|
token::owner(minter));
|
|
env.close();
|
|
|
|
// broker attempts to broker the offers but cannot.
|
|
env(token::brokerOffers(
|
|
broker, buyOfferIndex, minterOfferIndex),
|
|
ter(tecINSUFFICIENT_PAYMENT));
|
|
env.close();
|
|
|
|
// Cancel buyer's bad offer so the next test starts in a
|
|
// clean state.
|
|
env(token::cancelOffer(buyer, {buyOfferIndex}));
|
|
env.close();
|
|
}
|
|
|
|
// buyer creates a large enough offer.
|
|
uint256 const buyOfferIndex =
|
|
keylet::nftoffer(buyer, env.seq(buyer)).key;
|
|
env(token::createOffer(buyer, nftID, gwXAU(1000)),
|
|
token::owner(minter));
|
|
env.close();
|
|
|
|
// Broker attempts to charge a brokerFee but cannot.
|
|
env(token::brokerOffers(broker, buyOfferIndex, minterOfferIndex),
|
|
token::brokerFee(gwXAU(0.1)),
|
|
ter(tecINSUFFICIENT_PAYMENT));
|
|
env.close();
|
|
|
|
// broker charges no brokerFee and succeeds.
|
|
env(token::brokerOffers(broker, buyOfferIndex, minterOfferIndex));
|
|
env.close();
|
|
|
|
BEAST_EXPECT(ownerCount(env, issuer) == 1);
|
|
BEAST_EXPECT(ownerCount(env, minter) == 1);
|
|
BEAST_EXPECT(ownerCount(env, buyer) == 2);
|
|
BEAST_EXPECT(ownerCount(env, broker) == 1);
|
|
BEAST_EXPECT(env.balance(issuer, gwXAU) == gwXAU(1000));
|
|
BEAST_EXPECT(env.balance(minter, gwXAU) == gwXAU(2000));
|
|
BEAST_EXPECT(env.balance(buyer, gwXAU) == gwXAU(0));
|
|
BEAST_EXPECT(env.balance(broker, gwXAU) == gwXAU(1000));
|
|
|
|
// Burn the NFT so the next test starts with a clean state.
|
|
env(token::burn(buyer, nftID));
|
|
env.close();
|
|
}
|
|
|
|
// seller offers more than buyer is asking.
|
|
// There are both transfer and broker fees.
|
|
{
|
|
checkOwnerCountIsOne({issuer, minter, buyer, broker}, __LINE__);
|
|
setXAUBalance_1000({issuer, minter, buyer, broker}, __LINE__);
|
|
|
|
uint256 const nftID = mintNFT(maxTransferFee);
|
|
|
|
// minter creates their offer.
|
|
uint256 const minterOfferIndex =
|
|
keylet::nftoffer(minter, env.seq(minter)).key;
|
|
env(token::createOffer(minter, nftID, gwXAU(900)),
|
|
txflags(tfSellNFToken));
|
|
env.close();
|
|
{
|
|
// buyer creates an offer for more XAU than they currently own.
|
|
uint256 const buyOfferIndex =
|
|
keylet::nftoffer(buyer, env.seq(buyer)).key;
|
|
env(token::createOffer(buyer, nftID, gwXAU(1001)),
|
|
token::owner(minter));
|
|
env.close();
|
|
|
|
// broker attempts to broker the offers but cannot.
|
|
env(token::brokerOffers(
|
|
broker, buyOfferIndex, minterOfferIndex),
|
|
ter(tecINSUFFICIENT_FUNDS));
|
|
env.close();
|
|
|
|
// Cancel buyer's bad offer so the next test starts in a
|
|
// clean state.
|
|
env(token::cancelOffer(buyer, {buyOfferIndex}));
|
|
env.close();
|
|
}
|
|
{
|
|
// buyer creates an offer for less that what minter is asking.
|
|
uint256 const buyOfferIndex =
|
|
keylet::nftoffer(buyer, env.seq(buyer)).key;
|
|
env(token::createOffer(buyer, nftID, gwXAU(899)),
|
|
token::owner(minter));
|
|
env.close();
|
|
|
|
// broker attempts to broker the offers but cannot.
|
|
env(token::brokerOffers(
|
|
broker, buyOfferIndex, minterOfferIndex),
|
|
ter(tecINSUFFICIENT_PAYMENT));
|
|
env.close();
|
|
|
|
// Cancel buyer's bad offer so the next test starts in a
|
|
// clean state.
|
|
env(token::cancelOffer(buyer, {buyOfferIndex}));
|
|
env.close();
|
|
}
|
|
// buyer creates a large enough offer.
|
|
uint256 const buyOfferIndex =
|
|
keylet::nftoffer(buyer, env.seq(buyer)).key;
|
|
env(token::createOffer(buyer, nftID, gwXAU(1000)),
|
|
token::owner(minter));
|
|
env.close();
|
|
|
|
// Broker attempts to charge a brokerFee larger than the
|
|
// difference between the two offers but cannot.
|
|
env(token::brokerOffers(broker, buyOfferIndex, minterOfferIndex),
|
|
token::brokerFee(gwXAU(101)),
|
|
ter(tecINSUFFICIENT_PAYMENT));
|
|
env.close();
|
|
|
|
// broker charges the full difference between the two offers and
|
|
// succeeds.
|
|
env(token::brokerOffers(broker, buyOfferIndex, minterOfferIndex),
|
|
token::brokerFee(gwXAU(100)));
|
|
env.close();
|
|
|
|
BEAST_EXPECT(ownerCount(env, issuer) == 1);
|
|
BEAST_EXPECT(ownerCount(env, minter) == 1);
|
|
BEAST_EXPECT(ownerCount(env, buyer) == 2);
|
|
BEAST_EXPECT(ownerCount(env, broker) == 1);
|
|
BEAST_EXPECT(env.balance(issuer, gwXAU) == gwXAU(1450));
|
|
BEAST_EXPECT(env.balance(minter, gwXAU) == gwXAU(1450));
|
|
BEAST_EXPECT(env.balance(buyer, gwXAU) == gwXAU(0));
|
|
BEAST_EXPECT(env.balance(broker, gwXAU) == gwXAU(1100));
|
|
|
|
// Burn the NFT so the next test starts with a clean state.
|
|
env(token::burn(buyer, nftID));
|
|
env.close();
|
|
}
|
|
// seller offers more than buyer is asking.
|
|
// There are both transfer and broker fees, but broker takes less than
|
|
// the maximum.
|
|
{
|
|
checkOwnerCountIsOne({issuer, minter, buyer, broker}, __LINE__);
|
|
setXAUBalance_1000({issuer, minter, buyer, broker}, __LINE__);
|
|
|
|
uint256 const nftID = mintNFT(maxTransferFee / 2); // 25%
|
|
|
|
// minter creates their offer.
|
|
uint256 const minterOfferIndex =
|
|
keylet::nftoffer(minter, env.seq(minter)).key;
|
|
env(token::createOffer(minter, nftID, gwXAU(900)),
|
|
txflags(tfSellNFToken));
|
|
env.close();
|
|
|
|
// buyer creates a large enough offer.
|
|
uint256 const buyOfferIndex =
|
|
keylet::nftoffer(buyer, env.seq(buyer)).key;
|
|
env(token::createOffer(buyer, nftID, gwXAU(1000)),
|
|
token::owner(minter));
|
|
env.close();
|
|
|
|
// broker charges half difference between the two offers and
|
|
// succeeds. 25% of the remaining difference goes to issuer.
|
|
// The rest goes to minter.
|
|
env(token::brokerOffers(broker, buyOfferIndex, minterOfferIndex),
|
|
token::brokerFee(gwXAU(50)));
|
|
env.close();
|
|
|
|
BEAST_EXPECT(ownerCount(env, issuer) == 1);
|
|
BEAST_EXPECT(ownerCount(env, minter) == 1);
|
|
BEAST_EXPECT(ownerCount(env, buyer) == 2);
|
|
BEAST_EXPECT(ownerCount(env, broker) == 1);
|
|
BEAST_EXPECT(env.balance(issuer, gwXAU) == gwXAU(1237.5));
|
|
BEAST_EXPECT(env.balance(minter, gwXAU) == gwXAU(1712.5));
|
|
BEAST_EXPECT(env.balance(buyer, gwXAU) == gwXAU(0));
|
|
BEAST_EXPECT(env.balance(broker, gwXAU) == gwXAU(1050));
|
|
|
|
// Burn the NFT so the next test starts with a clean state.
|
|
env(token::burn(buyer, nftID));
|
|
env.close();
|
|
}
|
|
}
|
|
|
|
void
|
|
testNFTokenOfferOwner(FeatureBitset features)
|
|
{
|
|
// Verify the Owner field of an offer behaves as expected.
|
|
testcase("NFToken offer owner");
|
|
|
|
using namespace test::jtx;
|
|
|
|
Env env{*this, features};
|
|
|
|
Account const issuer{"issuer"};
|
|
Account const buyer1{"buyer1"};
|
|
Account const buyer2{"buyer2"};
|
|
env.fund(XRP(10000), issuer, buyer1, buyer2);
|
|
env.close();
|
|
|
|
// issuer creates an NFT.
|
|
uint256 const nftId{token::getNextID(env, issuer, 0u, tfTransferable)};
|
|
env(token::mint(issuer, 0u), txflags(tfTransferable));
|
|
env.close();
|
|
|
|
// Prove that issuer now owns nftId.
|
|
BEAST_EXPECT(nftCount(env, issuer) == 1);
|
|
BEAST_EXPECT(nftCount(env, buyer1) == 0);
|
|
BEAST_EXPECT(nftCount(env, buyer2) == 0);
|
|
|
|
// Both buyer1 and buyer2 create buy offers for nftId.
|
|
uint256 const buyer1OfferIndex =
|
|
keylet::nftoffer(buyer1, env.seq(buyer1)).key;
|
|
env(token::createOffer(buyer1, nftId, XRP(100)), token::owner(issuer));
|
|
uint256 const buyer2OfferIndex =
|
|
keylet::nftoffer(buyer2, env.seq(buyer2)).key;
|
|
env(token::createOffer(buyer2, nftId, XRP(100)), token::owner(issuer));
|
|
env.close();
|
|
|
|
// Lambda that counts the number of buy offers for a given NFT.
|
|
auto nftBuyOfferCount = [&env](uint256 const& nftId) -> std::size_t {
|
|
// We know that in this case not very many offers will be
|
|
// returned, so we skip the marker stuff.
|
|
Json::Value params;
|
|
params[jss::nft_id] = to_string(nftId);
|
|
Json::Value buyOffers =
|
|
env.rpc("json", "nft_buy_offers", to_string(params));
|
|
|
|
if (buyOffers.isMember(jss::result) &&
|
|
buyOffers[jss::result].isMember(jss::offers))
|
|
return buyOffers[jss::result][jss::offers].size();
|
|
|
|
return 0;
|
|
};
|
|
|
|
// Show there are two buy offers for nftId.
|
|
BEAST_EXPECT(nftBuyOfferCount(nftId) == 2);
|
|
|
|
// issuer accepts buyer1's offer.
|
|
env(token::acceptBuyOffer(issuer, buyer1OfferIndex));
|
|
env.close();
|
|
|
|
// Prove that buyer1 now owns nftId.
|
|
BEAST_EXPECT(nftCount(env, issuer) == 0);
|
|
BEAST_EXPECT(nftCount(env, buyer1) == 1);
|
|
BEAST_EXPECT(nftCount(env, buyer2) == 0);
|
|
|
|
// buyer1's offer was consumed, but buyer2's offer is still in the
|
|
// ledger.
|
|
BEAST_EXPECT(nftBuyOfferCount(nftId) == 1);
|
|
|
|
// buyer1 can now accept buyer2's offer, even though buyer2's
|
|
// NFTokenCreateOffer transaction specified the NFT Owner as issuer.
|
|
env(token::acceptBuyOffer(buyer1, buyer2OfferIndex));
|
|
env.close();
|
|
|
|
// Prove that buyer2 now owns nftId.
|
|
BEAST_EXPECT(nftCount(env, issuer) == 0);
|
|
BEAST_EXPECT(nftCount(env, buyer1) == 0);
|
|
BEAST_EXPECT(nftCount(env, buyer2) == 1);
|
|
|
|
// All of the NFTokenOffers are now consumed.
|
|
BEAST_EXPECT(nftBuyOfferCount(nftId) == 0);
|
|
}
|
|
|
|
void
|
|
testNFTokenWithTickets(FeatureBitset features)
|
|
{
|
|
// Make sure all NFToken transactions work with tickets.
|
|
testcase("NFToken transactions with tickets");
|
|
|
|
using namespace test::jtx;
|
|
|
|
Env env{*this, features};
|
|
|
|
Account const issuer{"issuer"};
|
|
Account const buyer{"buyer"};
|
|
env.fund(XRP(10000), issuer, buyer);
|
|
env.close();
|
|
|
|
// issuer and buyer grab enough tickets for all of the following
|
|
// transactions. Note that once the tickets are acquired issuer's
|
|
// and buyer's account sequence numbers should not advance.
|
|
std::uint32_t issuerTicketSeq{env.seq(issuer) + 1};
|
|
env(ticket::create(issuer, 10));
|
|
env.close();
|
|
std::uint32_t const issuerSeq{env.seq(issuer)};
|
|
BEAST_EXPECT(ticketCount(env, issuer) == 10);
|
|
|
|
std::uint32_t buyerTicketSeq{env.seq(buyer) + 1};
|
|
env(ticket::create(buyer, 10));
|
|
env.close();
|
|
std::uint32_t const buyerSeq{env.seq(buyer)};
|
|
BEAST_EXPECT(ticketCount(env, buyer) == 10);
|
|
|
|
// NFTokenMint
|
|
BEAST_EXPECT(ownerCount(env, issuer) == 10);
|
|
uint256 const nftId{token::getNextID(env, issuer, 0u, tfTransferable)};
|
|
env(token::mint(issuer, 0u),
|
|
txflags(tfTransferable),
|
|
ticket::use(issuerTicketSeq++));
|
|
env.close();
|
|
BEAST_EXPECT(ownerCount(env, issuer) == 10);
|
|
BEAST_EXPECT(ticketCount(env, issuer) == 9);
|
|
|
|
// NFTokenCreateOffer
|
|
BEAST_EXPECT(ownerCount(env, buyer) == 10);
|
|
uint256 const offerIndex0 = keylet::nftoffer(buyer, buyerTicketSeq).key;
|
|
env(token::createOffer(buyer, nftId, XRP(1)),
|
|
token::owner(issuer),
|
|
ticket::use(buyerTicketSeq++));
|
|
env.close();
|
|
BEAST_EXPECT(ownerCount(env, buyer) == 10);
|
|
BEAST_EXPECT(ticketCount(env, buyer) == 9);
|
|
|
|
// NFTokenCancelOffer
|
|
env(token::cancelOffer(buyer, {offerIndex0}),
|
|
ticket::use(buyerTicketSeq++));
|
|
env.close();
|
|
BEAST_EXPECT(ownerCount(env, buyer) == 8);
|
|
BEAST_EXPECT(ticketCount(env, buyer) == 8);
|
|
|
|
// NFTokenCreateOffer. buyer tries again.
|
|
uint256 const offerIndex1 = keylet::nftoffer(buyer, buyerTicketSeq).key;
|
|
env(token::createOffer(buyer, nftId, XRP(2)),
|
|
token::owner(issuer),
|
|
ticket::use(buyerTicketSeq++));
|
|
env.close();
|
|
BEAST_EXPECT(ownerCount(env, buyer) == 8);
|
|
BEAST_EXPECT(ticketCount(env, buyer) == 7);
|
|
|
|
// NFTokenAcceptOffer. issuer accepts buyer's offer.
|
|
env(token::acceptBuyOffer(issuer, offerIndex1),
|
|
ticket::use(issuerTicketSeq++));
|
|
env.close();
|
|
BEAST_EXPECT(ownerCount(env, issuer) == 8);
|
|
BEAST_EXPECT(ownerCount(env, buyer) == 8);
|
|
BEAST_EXPECT(ticketCount(env, issuer) == 8);
|
|
|
|
// NFTokenBurn. buyer burns the token they just bought.
|
|
env(token::burn(buyer, nftId), ticket::use(buyerTicketSeq++));
|
|
env.close();
|
|
BEAST_EXPECT(ownerCount(env, issuer) == 8);
|
|
BEAST_EXPECT(ownerCount(env, buyer) == 6);
|
|
BEAST_EXPECT(ticketCount(env, buyer) == 6);
|
|
|
|
// Verify that the account sequence numbers did not advance.
|
|
BEAST_EXPECT(env.seq(issuer) == issuerSeq);
|
|
BEAST_EXPECT(env.seq(buyer) == buyerSeq);
|
|
}
|
|
|
|
void
|
|
testNFTokenDeleteAccount(FeatureBitset features)
|
|
{
|
|
// Account deletion rules with NFTs:
|
|
// 1. An account holding one or more NFT offers may be deleted.
|
|
// 2. An NFT issuer with any NFTs they have issued still in the
|
|
// ledger may not be deleted.
|
|
// 3. An account holding one or more NFTs may not be deleted.
|
|
testcase("NFToken delete account");
|
|
|
|
using namespace test::jtx;
|
|
|
|
Env env{*this, features};
|
|
|
|
Account const issuer{"issuer"};
|
|
Account const minter{"minter"};
|
|
Account const becky{"becky"};
|
|
Account const carla{"carla"};
|
|
Account const daria{"daria"};
|
|
|
|
env.fund(XRP(10000), issuer, minter, becky, carla, daria);
|
|
env.close();
|
|
|
|
// Allow enough ledgers to pass so any of these accounts can be deleted.
|
|
for (int i = 0; i < 300; ++i)
|
|
env.close();
|
|
|
|
env(token::setMinter(issuer, minter));
|
|
env.close();
|
|
|
|
uint256 const nftId{token::getNextID(env, issuer, 0u, tfTransferable)};
|
|
env(token::mint(minter, 0u),
|
|
token::issuer(issuer),
|
|
txflags(tfTransferable));
|
|
env.close();
|
|
|
|
// At the momement issuer and minter cannot delete themselves.
|
|
// o issuer has an issued NFT in the ledger.
|
|
// o minter owns an NFT.
|
|
env(acctdelete(issuer, daria), fee(XRP(50)), ter(tecHAS_OBLIGATIONS));
|
|
env(acctdelete(minter, daria), fee(XRP(50)), ter(tecHAS_OBLIGATIONS));
|
|
env.close();
|
|
|
|
// Let enough ledgers pass so the account delete transactions are
|
|
// not retried.
|
|
for (int i = 0; i < 15; ++i)
|
|
env.close();
|
|
|
|
// becky and carla create offers for minter's NFT.
|
|
env(token::createOffer(becky, nftId, XRP(2)), token::owner(minter));
|
|
env.close();
|
|
|
|
uint256 const carlaOfferIndex =
|
|
keylet::nftoffer(carla, env.seq(carla)).key;
|
|
env(token::createOffer(carla, nftId, XRP(3)), token::owner(minter));
|
|
env.close();
|
|
|
|
// It should be possible for becky to delete herself, even though
|
|
// becky has an active NFT offer.
|
|
env(acctdelete(becky, daria), fee(XRP(50)));
|
|
env.close();
|
|
|
|
// minter accepts carla's offer.
|
|
env(token::acceptBuyOffer(minter, carlaOfferIndex));
|
|
env.close();
|
|
|
|
// Now it should be possible for minter to delete themselves since
|
|
// they no longer own an NFT.
|
|
env(acctdelete(minter, daria), fee(XRP(50)));
|
|
env.close();
|
|
|
|
// 1. issuer cannot delete themselves because they issued an NFT that
|
|
// is still in the ledger.
|
|
// 2. carla owns an NFT, so she cannot delete herself.
|
|
env(acctdelete(issuer, daria), fee(XRP(50)), ter(tecHAS_OBLIGATIONS));
|
|
env(acctdelete(carla, daria), fee(XRP(50)), ter(tecHAS_OBLIGATIONS));
|
|
env.close();
|
|
|
|
// Let enough ledgers pass so the account delete transactions are
|
|
// not retried.
|
|
for (int i = 0; i < 15; ++i)
|
|
env.close();
|
|
|
|
// carla burns her NFT. Since issuer's NFT is no longer in the
|
|
// ledger, both issuer and carla can delete themselves.
|
|
env(token::burn(carla, nftId));
|
|
env.close();
|
|
|
|
env(acctdelete(issuer, daria), fee(XRP(50)));
|
|
env(acctdelete(carla, daria), fee(XRP(50)));
|
|
env.close();
|
|
}
|
|
|
|
void
|
|
testNftXxxOffers(FeatureBitset features)
|
|
{
|
|
testcase("nft_buy_offers and nft_sell_offers");
|
|
|
|
// The default limit on returned NFToken offers is 250, so we need
|
|
// to produce more than 250 offers of each kind in order to exercise
|
|
// the marker.
|
|
|
|
// Fortunately there's nothing in the rules that says an account
|
|
// can't hold more than one offer for the same NFT. So we only
|
|
// need two accounts to generate the necessary offers.
|
|
using namespace test::jtx;
|
|
|
|
Env env{*this, features};
|
|
|
|
Account const issuer{"issuer"};
|
|
Account const buyer{"buyer"};
|
|
|
|
// A lot of offers requires a lot for reserve.
|
|
env.fund(XRP(1000000), issuer, buyer);
|
|
env.close();
|
|
|
|
// Create an NFT that we'll make offers for.
|
|
uint256 const nftID{token::getNextID(env, issuer, 0u, tfTransferable)};
|
|
env(token::mint(issuer, 0), txflags(tfTransferable));
|
|
env.close();
|
|
|
|
// A lambda that validates nft_XXX_offers query responses.
|
|
auto checkOffers = [this, &env, &nftID](
|
|
char const* request,
|
|
int expectCount,
|
|
int expectMarkerCount,
|
|
int line) {
|
|
int markerCount = 0;
|
|
Json::Value allOffers(Json::arrayValue);
|
|
std::string marker;
|
|
|
|
// The do/while collects results until no marker is returned.
|
|
do
|
|
{
|
|
Json::Value nftOffers = [&env, &nftID, &request, &marker]() {
|
|
Json::Value params;
|
|
params[jss::nft_id] = to_string(nftID);
|
|
|
|
if (!marker.empty())
|
|
params[jss::marker] = marker;
|
|
return env.rpc("json", request, to_string(params));
|
|
}();
|
|
|
|
// If there are no offers for the NFT we get an error
|
|
if (expectCount == 0)
|
|
{
|
|
if (expect(
|
|
nftOffers.isMember(jss::result),
|
|
"expected \"result\"",
|
|
__FILE__,
|
|
line))
|
|
{
|
|
if (expect(
|
|
nftOffers[jss::result].isMember(jss::error),
|
|
"expected \"error\"",
|
|
__FILE__,
|
|
line))
|
|
{
|
|
expect(
|
|
nftOffers[jss::result][jss::error].asString() ==
|
|
"objectNotFound",
|
|
"expected \"objectNotFound\"",
|
|
__FILE__,
|
|
line);
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
|
|
marker.clear();
|
|
if (expect(
|
|
nftOffers.isMember(jss::result),
|
|
"expected \"result\"",
|
|
__FILE__,
|
|
line))
|
|
{
|
|
Json::Value& result = nftOffers[jss::result];
|
|
|
|
if (result.isMember(jss::marker))
|
|
{
|
|
++markerCount;
|
|
marker = result[jss::marker].asString();
|
|
}
|
|
|
|
if (expect(
|
|
result.isMember(jss::offers),
|
|
"expected \"offers\"",
|
|
__FILE__,
|
|
line))
|
|
{
|
|
Json::Value& someOffers = result[jss::offers];
|
|
for (std::size_t i = 0; i < someOffers.size(); ++i)
|
|
allOffers.append(someOffers[i]);
|
|
}
|
|
}
|
|
} while (!marker.empty());
|
|
|
|
// Verify the contents of allOffers makes sense.
|
|
expect(
|
|
allOffers.size() == expectCount,
|
|
"Unexpected returned offer count",
|
|
__FILE__,
|
|
line);
|
|
expect(
|
|
markerCount == expectMarkerCount,
|
|
"Unexpected marker count",
|
|
__FILE__,
|
|
line);
|
|
std::optional<int> globalFlags;
|
|
std::set<std::string> offerIndexes;
|
|
std::set<std::string> amounts;
|
|
for (Json::Value const& offer : allOffers)
|
|
{
|
|
// The flags on all found offers should be the same.
|
|
if (!globalFlags)
|
|
globalFlags = offer[jss::flags].asInt();
|
|
|
|
expect(
|
|
*globalFlags == offer[jss::flags].asInt(),
|
|
"Inconsistent flags returned",
|
|
__FILE__,
|
|
line);
|
|
|
|
// The test conditions should produce unique indexes and
|
|
// amounts for all offers.
|
|
offerIndexes.insert(offer[jss::nft_offer_index].asString());
|
|
amounts.insert(offer[jss::amount].asString());
|
|
}
|
|
|
|
expect(
|
|
offerIndexes.size() == expectCount,
|
|
"Duplicate indexes returned?",
|
|
__FILE__,
|
|
line);
|
|
expect(
|
|
amounts.size() == expectCount,
|
|
"Duplicate amounts returned?",
|
|
__FILE__,
|
|
line);
|
|
};
|
|
|
|
// There are no sell offers.
|
|
checkOffers("nft_sell_offers", 0, false, __LINE__);
|
|
|
|
// A lambda that generates sell offers.
|
|
STAmount sellPrice = XRP(0);
|
|
auto makeSellOffers =
|
|
[&env, &issuer, &nftID, &sellPrice](STAmount const& limit) {
|
|
// Save a little test time by not closing too often.
|
|
int offerCount = 0;
|
|
while (sellPrice < limit)
|
|
{
|
|
sellPrice += XRP(1);
|
|
env(token::createOffer(issuer, nftID, sellPrice),
|
|
txflags(tfSellNFToken));
|
|
if (++offerCount % 10 == 0)
|
|
env.close();
|
|
}
|
|
env.close();
|
|
};
|
|
|
|
// There is one sell offer.
|
|
makeSellOffers(XRP(1));
|
|
checkOffers("nft_sell_offers", 1, 0, __LINE__);
|
|
|
|
// There are 250 sell offers.
|
|
makeSellOffers(XRP(250));
|
|
checkOffers("nft_sell_offers", 250, 0, __LINE__);
|
|
|
|
// There are 251 sell offers.
|
|
makeSellOffers(XRP(251));
|
|
checkOffers("nft_sell_offers", 251, 1, __LINE__);
|
|
|
|
// There are 500 sell offers.
|
|
makeSellOffers(XRP(500));
|
|
checkOffers("nft_sell_offers", 500, 1, __LINE__);
|
|
|
|
// There are 501 sell offers.
|
|
makeSellOffers(XRP(501));
|
|
checkOffers("nft_sell_offers", 501, 2, __LINE__);
|
|
|
|
// There are no buy offers.
|
|
checkOffers("nft_buy_offers", 0, 0, __LINE__);
|
|
|
|
// A lambda that generates buy offers.
|
|
STAmount buyPrice = XRP(0);
|
|
auto makeBuyOffers =
|
|
[&env, &buyer, &issuer, &nftID, &buyPrice](STAmount const& limit) {
|
|
// Save a little test time by not closing too often.
|
|
int offerCount = 0;
|
|
while (buyPrice < limit)
|
|
{
|
|
buyPrice += XRP(1);
|
|
env(token::createOffer(buyer, nftID, buyPrice),
|
|
token::owner(issuer));
|
|
if (++offerCount % 10 == 0)
|
|
env.close();
|
|
}
|
|
env.close();
|
|
};
|
|
|
|
// There is one buy offer;
|
|
makeBuyOffers(XRP(1));
|
|
checkOffers("nft_buy_offers", 1, 0, __LINE__);
|
|
|
|
// There are 250 buy offers.
|
|
makeBuyOffers(XRP(250));
|
|
checkOffers("nft_buy_offers", 250, 0, __LINE__);
|
|
|
|
// There are 251 buy offers.
|
|
makeBuyOffers(XRP(251));
|
|
checkOffers("nft_buy_offers", 251, 1, __LINE__);
|
|
|
|
// There are 500 buy offers.
|
|
makeBuyOffers(XRP(500));
|
|
checkOffers("nft_buy_offers", 500, 1, __LINE__);
|
|
|
|
// There are 501 buy offers.
|
|
makeBuyOffers(XRP(501));
|
|
checkOffers("nft_buy_offers", 501, 2, __LINE__);
|
|
}
|
|
|
|
void
|
|
testFixNFTokenNegOffer(FeatureBitset features)
|
|
{
|
|
// Exercise changes introduced by fixNFTokenNegOffer.
|
|
using namespace test::jtx;
|
|
|
|
testcase("fixNFTokenNegOffer");
|
|
|
|
Account const issuer{"issuer"};
|
|
Account const buyer{"buyer"};
|
|
Account const gw{"gw"};
|
|
IOU const gwXAU(gw["XAU"]);
|
|
|
|
// Test both with and without fixNFTokenNegOffer
|
|
for (auto const& tweakedFeatures :
|
|
{features - fixNFTokenNegOffer - featureNonFungibleTokensV1_1,
|
|
features | fixNFTokenNegOffer})
|
|
{
|
|
// There was a bug in the initial NFT implementation that
|
|
// allowed offers to be placed with negative amounts. Verify
|
|
// that fixNFTokenNegOffer addresses the problem.
|
|
Env env{*this, tweakedFeatures};
|
|
|
|
env.fund(XRP(1000000), issuer, buyer, gw);
|
|
env.close();
|
|
|
|
env(trust(issuer, gwXAU(2000)));
|
|
env(trust(buyer, gwXAU(2000)));
|
|
env.close();
|
|
|
|
env(pay(gw, issuer, gwXAU(1000)));
|
|
env(pay(gw, buyer, gwXAU(1000)));
|
|
env.close();
|
|
|
|
// Create an NFT that we'll make XRP offers for.
|
|
uint256 const nftID0{
|
|
token::getNextID(env, issuer, 0u, tfTransferable)};
|
|
env(token::mint(issuer, 0), txflags(tfTransferable));
|
|
env.close();
|
|
|
|
// Create an NFT that we'll make IOU offers for.
|
|
uint256 const nftID1{
|
|
token::getNextID(env, issuer, 1u, tfTransferable)};
|
|
env(token::mint(issuer, 1), txflags(tfTransferable));
|
|
env.close();
|
|
|
|
TER const offerCreateTER = tweakedFeatures[fixNFTokenNegOffer]
|
|
? static_cast<TER>(temBAD_AMOUNT)
|
|
: static_cast<TER>(tesSUCCESS);
|
|
|
|
// Make offers with negative amounts for the NFTs
|
|
uint256 const sellNegXrpOfferIndex =
|
|
keylet::nftoffer(issuer, env.seq(issuer)).key;
|
|
env(token::createOffer(issuer, nftID0, XRP(-2)),
|
|
txflags(tfSellNFToken),
|
|
ter(offerCreateTER));
|
|
env.close();
|
|
|
|
uint256 const sellNegIouOfferIndex =
|
|
keylet::nftoffer(issuer, env.seq(issuer)).key;
|
|
env(token::createOffer(issuer, nftID1, gwXAU(-2)),
|
|
txflags(tfSellNFToken),
|
|
ter(offerCreateTER));
|
|
env.close();
|
|
|
|
uint256 const buyNegXrpOfferIndex =
|
|
keylet::nftoffer(buyer, env.seq(buyer)).key;
|
|
env(token::createOffer(buyer, nftID0, XRP(-1)),
|
|
token::owner(issuer),
|
|
ter(offerCreateTER));
|
|
env.close();
|
|
|
|
uint256 const buyNegIouOfferIndex =
|
|
keylet::nftoffer(buyer, env.seq(buyer)).key;
|
|
env(token::createOffer(buyer, nftID1, gwXAU(-1)),
|
|
token::owner(issuer),
|
|
ter(offerCreateTER));
|
|
env.close();
|
|
|
|
{
|
|
// Now try to accept the offers.
|
|
// 1. If fixNFTokenNegOffer is NOT enabled get tecINTERNAL.
|
|
// 2. If fixNFTokenNegOffer IS enabled get tecOBJECT_NOT_FOUND.
|
|
TER const offerAcceptTER = tweakedFeatures[fixNFTokenNegOffer]
|
|
? static_cast<TER>(tecOBJECT_NOT_FOUND)
|
|
: static_cast<TER>(tecINTERNAL);
|
|
|
|
// Sell offers.
|
|
env(token::acceptSellOffer(buyer, sellNegXrpOfferIndex),
|
|
ter(offerAcceptTER));
|
|
env.close();
|
|
env(token::acceptSellOffer(buyer, sellNegIouOfferIndex),
|
|
ter(offerAcceptTER));
|
|
env.close();
|
|
|
|
// Buy offers.
|
|
env(token::acceptBuyOffer(issuer, buyNegXrpOfferIndex),
|
|
ter(offerAcceptTER));
|
|
env.close();
|
|
env(token::acceptBuyOffer(issuer, buyNegIouOfferIndex),
|
|
ter(offerAcceptTER));
|
|
env.close();
|
|
}
|
|
{
|
|
// 1. If fixNFTokenNegOffer is NOT enabled get tecSUCCESS.
|
|
// 2. If fixNFTokenNegOffer IS enabled get tecOBJECT_NOT_FOUND.
|
|
TER const offerAcceptTER = tweakedFeatures[fixNFTokenNegOffer]
|
|
? static_cast<TER>(tecOBJECT_NOT_FOUND)
|
|
: static_cast<TER>(tesSUCCESS);
|
|
|
|
// Brokered offers.
|
|
env(token::brokerOffers(
|
|
gw, buyNegXrpOfferIndex, sellNegXrpOfferIndex),
|
|
ter(offerAcceptTER));
|
|
env.close();
|
|
env(token::brokerOffers(
|
|
gw, buyNegIouOfferIndex, sellNegIouOfferIndex),
|
|
ter(offerAcceptTER));
|
|
env.close();
|
|
}
|
|
}
|
|
|
|
// Test what happens if NFTokenOffers are created with negative amounts
|
|
// and then fixNFTokenNegOffer goes live. What does an acceptOffer do?
|
|
{
|
|
Env env{
|
|
*this,
|
|
features - fixNFTokenNegOffer - featureNonFungibleTokensV1_1};
|
|
|
|
env.fund(XRP(1000000), issuer, buyer, gw);
|
|
env.close();
|
|
|
|
env(trust(issuer, gwXAU(2000)));
|
|
env(trust(buyer, gwXAU(2000)));
|
|
env.close();
|
|
|
|
env(pay(gw, issuer, gwXAU(1000)));
|
|
env(pay(gw, buyer, gwXAU(1000)));
|
|
env.close();
|
|
|
|
// Create an NFT that we'll make XRP offers for.
|
|
uint256 const nftID0{
|
|
token::getNextID(env, issuer, 0u, tfTransferable)};
|
|
env(token::mint(issuer, 0), txflags(tfTransferable));
|
|
env.close();
|
|
|
|
// Create an NFT that we'll make IOU offers for.
|
|
uint256 const nftID1{
|
|
token::getNextID(env, issuer, 1u, tfTransferable)};
|
|
env(token::mint(issuer, 1), txflags(tfTransferable));
|
|
env.close();
|
|
|
|
// Make offers with negative amounts for the NFTs
|
|
uint256 const sellNegXrpOfferIndex =
|
|
keylet::nftoffer(issuer, env.seq(issuer)).key;
|
|
env(token::createOffer(issuer, nftID0, XRP(-2)),
|
|
txflags(tfSellNFToken));
|
|
env.close();
|
|
|
|
uint256 const sellNegIouOfferIndex =
|
|
keylet::nftoffer(issuer, env.seq(issuer)).key;
|
|
env(token::createOffer(issuer, nftID1, gwXAU(-2)),
|
|
txflags(tfSellNFToken));
|
|
env.close();
|
|
|
|
uint256 const buyNegXrpOfferIndex =
|
|
keylet::nftoffer(buyer, env.seq(buyer)).key;
|
|
env(token::createOffer(buyer, nftID0, XRP(-1)),
|
|
token::owner(issuer));
|
|
env.close();
|
|
|
|
uint256 const buyNegIouOfferIndex =
|
|
keylet::nftoffer(buyer, env.seq(buyer)).key;
|
|
env(token::createOffer(buyer, nftID1, gwXAU(-1)),
|
|
token::owner(issuer));
|
|
env.close();
|
|
|
|
// Now the amendment passes.
|
|
env.enableFeature(fixNFTokenNegOffer);
|
|
env.close();
|
|
|
|
// All attempts to accept the offers with negative amounts
|
|
// should fail with temBAD_OFFER.
|
|
env(token::acceptSellOffer(buyer, sellNegXrpOfferIndex),
|
|
ter(temBAD_OFFER));
|
|
env.close();
|
|
env(token::acceptSellOffer(buyer, sellNegIouOfferIndex),
|
|
ter(temBAD_OFFER));
|
|
env.close();
|
|
|
|
// Buy offers.
|
|
env(token::acceptBuyOffer(issuer, buyNegXrpOfferIndex),
|
|
ter(temBAD_OFFER));
|
|
env.close();
|
|
env(token::acceptBuyOffer(issuer, buyNegIouOfferIndex),
|
|
ter(temBAD_OFFER));
|
|
env.close();
|
|
|
|
// Brokered offers.
|
|
env(token::brokerOffers(
|
|
gw, buyNegXrpOfferIndex, sellNegXrpOfferIndex),
|
|
ter(temBAD_OFFER));
|
|
env.close();
|
|
env(token::brokerOffers(
|
|
gw, buyNegIouOfferIndex, sellNegIouOfferIndex),
|
|
ter(temBAD_OFFER));
|
|
env.close();
|
|
}
|
|
|
|
// Test buy offers with a destination with and without
|
|
// fixNFTokenNegOffer.
|
|
for (auto const& tweakedFeatures :
|
|
{features - fixNFTokenNegOffer - featureNonFungibleTokensV1_1,
|
|
features | fixNFTokenNegOffer})
|
|
{
|
|
Env env{*this, tweakedFeatures};
|
|
|
|
env.fund(XRP(1000000), issuer, buyer);
|
|
|
|
// Create an NFT that we'll make offers for.
|
|
uint256 const nftID{
|
|
token::getNextID(env, issuer, 0u, tfTransferable)};
|
|
env(token::mint(issuer, 0), txflags(tfTransferable));
|
|
env.close();
|
|
|
|
TER const offerCreateTER = tweakedFeatures[fixNFTokenNegOffer]
|
|
? static_cast<TER>(tesSUCCESS)
|
|
: static_cast<TER>(temMALFORMED);
|
|
|
|
env(token::createOffer(buyer, nftID, drops(1)),
|
|
token::owner(issuer),
|
|
token::destination(issuer),
|
|
ter(offerCreateTER));
|
|
env.close();
|
|
}
|
|
}
|
|
|
|
void
|
|
testWithFeats(FeatureBitset features)
|
|
{
|
|
testEnabled(features);
|
|
testMintReserve(features);
|
|
testMintMaxTokens(features);
|
|
testMintInvalid(features);
|
|
testBurnInvalid(features);
|
|
testCreateOfferInvalid(features);
|
|
testCancelOfferInvalid(features);
|
|
testAcceptOfferInvalid(features);
|
|
testMintFlagBurnable(features);
|
|
testMintFlagOnlyXRP(features);
|
|
testMintFlagCreateTrustLine(features);
|
|
testMintFlagTransferable(features);
|
|
testMintTransferFee(features);
|
|
testMintTaxon(features);
|
|
testMintURI(features);
|
|
testCreateOfferDestination(features);
|
|
testCreateOfferExpiration(features);
|
|
testCancelOffers(features);
|
|
testCancelTooManyOffers(features);
|
|
testBrokeredAccept(features);
|
|
testNFTokenOfferOwner(features);
|
|
testNFTokenWithTickets(features);
|
|
testNFTokenDeleteAccount(features);
|
|
testNftXxxOffers(features);
|
|
testFixNFTokenNegOffer(features);
|
|
}
|
|
|
|
public:
|
|
void
|
|
run() override
|
|
{
|
|
using namespace test::jtx;
|
|
FeatureBitset const all{supported_amendments()};
|
|
FeatureBitset const fixNFTDir{fixNFTokenDirV1};
|
|
|
|
testWithFeats(all - fixNFTDir);
|
|
testWithFeats(all);
|
|
}
|
|
};
|
|
|
|
BEAST_DEFINE_TESTSUITE_PRIO(NFToken, tx, ripple, 2);
|
|
|
|
} // namespace ripple
|