xrpld
Loading...
Searching...
No Matches
NFTokenDir_test.cpp
1
2#include <test/jtx/Account.h>
3#include <test/jtx/Env.h>
4#include <test/jtx/amount.h>
5#include <test/jtx/envconfig.h>
6#include <test/jtx/owners.h> // IWYU pragma: keep
7#include <test/jtx/ter.h>
8#include <test/jtx/token.h>
9#include <test/jtx/txflags.h>
10
11#include <xrpl/basics/base_uint.h>
12#include <xrpl/beast/unit_test/suite.h>
13#include <xrpl/beast/utility/Journal.h>
14#include <xrpl/json/json_forwards.h>
15#include <xrpl/json/json_value.h>
16#include <xrpl/json/to_string.h>
17#include <xrpl/protocol/Feature.h>
18#include <xrpl/protocol/Indexes.h>
19#include <xrpl/protocol/SField.h>
20#include <xrpl/protocol/SeqProxy.h>
21#include <xrpl/protocol/TER.h>
22#include <xrpl/protocol/TxFlags.h>
23#include <xrpl/protocol/jss.h>
24#include <xrpl/protocol/nft.h>
25#include <xrpl/protocol/nftPageMask.h>
26
27#include <algorithm>
28#include <array>
29#include <cstddef>
30#include <cstdint>
31#include <initializer_list>
32#include <iostream>
33#include <ostream>
34#include <set>
35#include <string_view>
36#include <vector>
37
38namespace xrpl {
39
41{
42 // printNFTPages is a helper function that may be used for debugging.
43 //
44 // It uses the ledger RPC command to show the NFT pages in the ledger.
45 // This parameter controls how noisy the output is.
46 enum class Volume : bool {
47 Quiet = false,
48 Noisy = true,
49 };
50
51 static void
53 {
54 json::Value jvParams;
55 jvParams[jss::ledger_index] = "current";
56 jvParams[jss::binary] = false;
57 {
58 json::Value jrr = env.rpc("json", "ledger_data", to_string(jvParams));
59
60 // Iterate the state and print all NFTokenPages.
61 if (!jrr.isMember(jss::result) || !jrr[jss::result].isMember(jss::state))
62 {
63 std::cout << "No ledger state found!" << std::endl;
64 return;
65 }
66 json::Value& state = jrr[jss::result][jss::state];
67 if (!state.isArray())
68 {
69 std::cout << "Ledger state is not array!" << std::endl;
70 return;
71 }
72 for (json::UInt i = 0; i < state.size(); ++i)
73 {
74 if (state[i].isMember(sfNFTokens.jsonName) &&
75 state[i][sfNFTokens.jsonName].isArray())
76 {
77 std::uint32_t const tokenCount = state[i][sfNFTokens.jsonName].size();
78 std::cout << tokenCount << " NFtokens in page "
79 << state[i][jss::index].asString() << std::endl;
80
81 if (vol == Volume::Noisy)
82 {
83 std::cout << state[i].toStyledString() << std::endl;
84 }
85 else
86 {
87 if (tokenCount > 0)
88 {
90 << "first: " << state[i][sfNFTokens.jsonName][0u].toStyledString()
91 << std::endl;
92 }
93 if (tokenCount > 1)
94 {
96 << "last: "
97 << state[i][sfNFTokens.jsonName][tokenCount - 1].toStyledString()
98 << std::endl;
99 }
100 }
101 }
102 }
103 }
104 }
105
106 void
108 {
109 // It should be possible to store many consecutive NFTs.
110 testcase("Sequential NFTs");
111
112 using namespace test::jtx;
113 Env env{*this, features};
114
115 // A single minter tends not to mint numerically sequential NFTokens
116 // because the taxon cipher mixes things up. We can override the
117 // cipher, however, and mint many sequential NFTokens with no gaps
118 // between them.
119 //
120 // Here we'll simply mint 100 sequential NFTs. Then we'll create
121 // offers for them to verify that the ledger can find them.
122
123 Account const issuer{"issuer"};
124 Account const buyer{"buyer"};
125 env.fund(XRP(10000), buyer, issuer);
126 env.close();
127
128 // Mint 100 sequential NFTs. Tweak the taxon so zero is always stored.
129 // That's what makes them sequential.
130 static constexpr std::size_t kNftCount = 100;
132 nftIDs.reserve(kNftCount);
133 for (int i = 0; i < kNftCount; ++i)
134 {
135 std::uint32_t const taxon = toUInt32(nft::cipheredTaxon(i, nft::toTaxon(0)));
136 nftIDs.emplace_back(token::getNextID(env, issuer, taxon, tfTransferable));
137 env(token::mint(issuer, taxon), Txflags(tfTransferable));
138 env.close();
139 }
140
141 // Create an offer for each of the NFTs. This verifies that the ledger
142 // can find all of the minted NFTs.
144 for (uint256 const& nftID : nftIDs)
145 {
146 offers.emplace_back(
147 keylet::nftokenOffer(issuer, SeqProxy::rawSequence(env.seq(issuer))).key);
148 env(token::createOffer(issuer, nftID, XRP(0)), Txflags(tfSellNFToken));
149 env.close();
150 }
151
152 // Buyer accepts all of the offers in reverse order.
153 std::ranges::reverse(offers);
154 for (uint256 const& offer : offers)
155 {
156 env(token::acceptSellOffer(buyer, offer));
157 env.close();
158 }
159 }
160
161 void
163 {
164 // All NFT IDs with the same low 96 bits must stay on the same NFT page.
165 testcase("Lopsided splits");
166
167 using namespace test::jtx;
168
169 // When a single NFT page exceeds 32 entries, the code is inclined
170 // to split that page into two equal pieces. That's fine, but
171 // the code also needs to keep NFTs with identical low 96-bits on
172 // the same page.
173 //
174 // Here we synthesize cases where there are several NFTs with
175 // identical 96-low-bits in the middle of a page. When that page
176 // is split because it overflows, we need to see that the NFTs
177 // with identical 96-low-bits are all kept on the same page.
178
179 // Lambda that exercises the lopsided splits.
180 auto exerciseLopsided = [this,
182 Env env{*this, features};
183
184 // Eventually all of the NFTokens will be owned by buyer.
185 Account const buyer{"buyer"};
186 env.fund(XRP(10000), buyer);
187 env.close();
188
189 // Create accounts for all of the seeds and fund those accounts.
190 std::vector<Account> accounts;
191 accounts.reserve(seeds.size());
192 for (std::string_view const seed : seeds)
193 {
194 Account const& account =
195 accounts.emplace_back(Account::AcctStringType::Base58Seed, std::string(seed));
196 env.fund(XRP(10000), account);
197
198 // Do not close the ledger inside the loop. If accounts are
199 // initialized at different ledgers, they will have
200 // different account sequences. That would cause the
201 // accounts to have different NFTokenID sequence numbers.
202 }
203 env.close();
204
205 // All of the accounts create one NFT and and offer that NFT to
206 // buyer.
209 offers.reserve(accounts.size());
210 for (Account const& account : accounts)
211 {
212 // Mint the NFT.
213 uint256 const& nftID =
214 nftIDs.emplace_back(token::getNextID(env, account, 0, tfTransferable));
215 env(token::mint(account, 0), Txflags(tfTransferable));
216 env.close();
217
218 // Create an offer to give the NFT to buyer for free.
219 offers.emplace_back(
220 keylet::nftokenOffer(account, SeqProxy::rawSequence(env.seq(account))).key);
221 env(token::createOffer(account, nftID, XRP(0)),
222 token::Destination(buyer),
223 Txflags(tfSellNFToken));
224 }
225 env.close();
226
227 // buyer accepts all of the offers.
228 for (uint256 const& offer : offers)
229 {
230 env(token::acceptSellOffer(buyer, offer));
231 env.close();
232 }
233
234 // This can be a good time to look at the NFT pages.
235 // printNFTPages(env, noisy);
236
237 // Verify that all NFTs are owned by buyer and findable in the
238 // ledger by having buyer create sell offers for all of their
239 // NFTs. Attempting to sell an offer that the ledger can't find
240 // generates a non-tesSUCCESS error code.
241 for (uint256 const& nftID : nftIDs)
242 {
243 uint256 const offerID =
244 keylet::nftokenOffer(buyer, SeqProxy::rawSequence(env.seq(buyer))).key;
245 env(token::createOffer(buyer, nftID, XRP(100)), Txflags(tfSellNFToken));
246 env.close();
247
248 env(token::cancelOffer(buyer, {offerID}));
249 }
250
251 // Verify that all the NFTs are owned by buyer.
252 json::Value buyerNFTs = [&env, &buyer]() {
253 json::Value params;
254 params[jss::account] = buyer.human();
255 params[jss::type] = "state";
256 return env.rpc("json", "account_nfts", to_string(params));
257 }();
258
259 BEAST_EXPECT(buyerNFTs[jss::result][jss::account_nfts].size() == nftIDs.size());
260 for (json::Value const& ownedNFT : buyerNFTs[jss::result][jss::account_nfts])
261 {
262 uint256 ownedID;
263 BEAST_EXPECT(ownedID.parseHex(ownedNFT[sfNFTokenID.jsonName].asString()));
264 auto const foundIter = std::ranges::find(nftIDs, ownedID);
265
266 // Assuming we find the NFT, erase it so we know it's been
267 // found and can't be found again.
268 if (BEAST_EXPECT(foundIter != nftIDs.end()))
269 nftIDs.erase(foundIter);
270 }
271
272 // All NFTs should now be accounted for, so nftIDs should be
273 // empty.
274 BEAST_EXPECT(nftIDs.empty());
275 };
276
277 // These seeds cause a lopsided split where the new NFT is added
278 // to the upper page.
279 static std::initializer_list<std::string_view const> const kSplitAndAddToHi{
280 "sp6JS7f14BuwFY8Mw5p3b8jjQBBTK", // 0. 0x1d2932ea
281 "sp6JS7f14BuwFY8Mw6F7X3EiGKazu", // 1. 0x1d2932ea
282 "sp6JS7f14BuwFY8Mw6FxjntJJfKXq", // 2. 0x1d2932ea
283 "sp6JS7f14BuwFY8Mw6eSF1ydEozJg", // 3. 0x1d2932ea
284 "sp6JS7f14BuwFY8Mw6koPB91um2ej", // 4. 0x1d2932ea
285 "sp6JS7f14BuwFY8Mw6m6D64iwquSe", // 5. 0x1d2932ea
286
287 "sp6JS7f14BuwFY8Mw5rC43sN4adC2", // 6. 0x208dbc24
288 "sp6JS7f14BuwFY8Mw65L9DDQqgebz", // 7. 0x208dbc24
289 "sp6JS7f14BuwFY8Mw65nKvU8pPQNn", // 8. 0x208dbc24
290 "sp6JS7f14BuwFY8Mw6bxZLyTrdipw", // 9. 0x208dbc24
291 "sp6JS7f14BuwFY8Mw6d5abucntSoX", // 10. 0x208dbc24
292 "sp6JS7f14BuwFY8Mw6qXK5awrRRP8", // 11. 0x208dbc24
293
294 // These eight need to be kept together by the implementation.
295 "sp6JS7f14BuwFY8Mw66EBtMxoMcCa", // 12. 0x309b67ed
296 "sp6JS7f14BuwFY8Mw66dGfE9jVfGv", // 13. 0x309b67ed
297 "sp6JS7f14BuwFY8Mw6APdZa7PH566", // 14. 0x309b67ed
298 "sp6JS7f14BuwFY8Mw6C3QX5CZyET5", // 15. 0x309b67ed
299 "sp6JS7f14BuwFY8Mw6CSysFf8GvaR", // 16. 0x309b67ed
300 "sp6JS7f14BuwFY8Mw6c7QSDmoAeRV", // 17. 0x309b67ed
301 "sp6JS7f14BuwFY8Mw6mvonveaZhW7", // 18. 0x309b67ed
302 "sp6JS7f14BuwFY8Mw6vtHHG7dYcXi", // 19. 0x309b67ed
303
304 "sp6JS7f14BuwFY8Mw66yppUNxESaw", // 20. 0x40d4b96f
305 "sp6JS7f14BuwFY8Mw6ATYQvobXiDT", // 21. 0x40d4b96f
306 "sp6JS7f14BuwFY8Mw6bis8D1Wa9Uy", // 22. 0x40d4b96f
307 "sp6JS7f14BuwFY8Mw6cTiGCWA8Wfa", // 23. 0x40d4b96f
308 "sp6JS7f14BuwFY8Mw6eAy2fpXmyYf", // 24. 0x40d4b96f
309 "sp6JS7f14BuwFY8Mw6icn58TRs8YG", // 25. 0x40d4b96f
310
311 "sp6JS7f14BuwFY8Mw68tj2eQEWoJt", // 26. 0x503b6ba9
312 "sp6JS7f14BuwFY8Mw6AjnAinNnMHT", // 27. 0x503b6ba9
313 "sp6JS7f14BuwFY8Mw6CKDUwB4LrhL", // 28. 0x503b6ba9
314 "sp6JS7f14BuwFY8Mw6d2yPszEFA6J", // 29. 0x503b6ba9
315 "sp6JS7f14BuwFY8Mw6jcBQBH3PfnB", // 30. 0x503b6ba9
316 "sp6JS7f14BuwFY8Mw6qxx19KSnN1w", // 31. 0x503b6ba9
317
318 // Adding this NFT splits the page. It is added to the upper
319 // page.
320 "sp6JS7f14BuwFY8Mw6ut1hFrqWoY5", // 32. 0x503b6ba9
321 };
322
323 // These seeds cause a lopsided split where the new NFT is added
324 // to the lower page.
325 static std::initializer_list<std::string_view const> const kSplitAndAddToLo{
326 "sp6JS7f14BuwFY8Mw5p3b8jjQBBTK", // 0. 0x1d2932ea
327 "sp6JS7f14BuwFY8Mw6F7X3EiGKazu", // 1. 0x1d2932ea
328 "sp6JS7f14BuwFY8Mw6FxjntJJfKXq", // 2. 0x1d2932ea
329 "sp6JS7f14BuwFY8Mw6eSF1ydEozJg", // 3. 0x1d2932ea
330 "sp6JS7f14BuwFY8Mw6koPB91um2ej", // 4. 0x1d2932ea
331 "sp6JS7f14BuwFY8Mw6m6D64iwquSe", // 5. 0x1d2932ea
332
333 "sp6JS7f14BuwFY8Mw5rC43sN4adC2", // 6. 0x208dbc24
334 "sp6JS7f14BuwFY8Mw65L9DDQqgebz", // 7. 0x208dbc24
335 "sp6JS7f14BuwFY8Mw65nKvU8pPQNn", // 8. 0x208dbc24
336 "sp6JS7f14BuwFY8Mw6bxZLyTrdipw", // 9. 0x208dbc24
337 "sp6JS7f14BuwFY8Mw6d5abucntSoX", // 10. 0x208dbc24
338 "sp6JS7f14BuwFY8Mw6qXK5awrRRP8", // 11. 0x208dbc24
339
340 // These eight need to be kept together by the implementation.
341 "sp6JS7f14BuwFY8Mw66EBtMxoMcCa", // 12. 0x309b67ed
342 "sp6JS7f14BuwFY8Mw66dGfE9jVfGv", // 13. 0x309b67ed
343 "sp6JS7f14BuwFY8Mw6APdZa7PH566", // 14. 0x309b67ed
344 "sp6JS7f14BuwFY8Mw6C3QX5CZyET5", // 15. 0x309b67ed
345 "sp6JS7f14BuwFY8Mw6CSysFf8GvaR", // 16. 0x309b67ed
346 "sp6JS7f14BuwFY8Mw6c7QSDmoAeRV", // 17. 0x309b67ed
347 "sp6JS7f14BuwFY8Mw6mvonveaZhW7", // 18. 0x309b67ed
348 "sp6JS7f14BuwFY8Mw6vtHHG7dYcXi", // 19. 0x309b67ed
349
350 "sp6JS7f14BuwFY8Mw66yppUNxESaw", // 20. 0x40d4b96f
351 "sp6JS7f14BuwFY8Mw6ATYQvobXiDT", // 21. 0x40d4b96f
352 "sp6JS7f14BuwFY8Mw6bis8D1Wa9Uy", // 22. 0x40d4b96f
353 "sp6JS7f14BuwFY8Mw6cTiGCWA8Wfa", // 23. 0x40d4b96f
354 "sp6JS7f14BuwFY8Mw6eAy2fpXmyYf", // 24. 0x40d4b96f
355 "sp6JS7f14BuwFY8Mw6icn58TRs8YG", // 25. 0x40d4b96f
356
357 "sp6JS7f14BuwFY8Mw68tj2eQEWoJt", // 26. 0x503b6ba9
358 "sp6JS7f14BuwFY8Mw6AjnAinNnMHT", // 27. 0x503b6ba9
359 "sp6JS7f14BuwFY8Mw6CKDUwB4LrhL", // 28. 0x503b6ba9
360 "sp6JS7f14BuwFY8Mw6d2yPszEFA6J", // 29. 0x503b6ba9
361 "sp6JS7f14BuwFY8Mw6jcBQBH3PfnB", // 30. 0x503b6ba9
362 "sp6JS7f14BuwFY8Mw6qxx19KSnN1w", // 31. 0x503b6ba9
363
364 // Adding this NFT splits the page. It is added to the lower
365 // page.
366 "sp6JS7f14BuwFY8Mw6xCigaMwC6Dp", // 32. 0x309b67ed
367 };
368
369 // Run the test cases.
370 exerciseLopsided(kSplitAndAddToHi);
371 exerciseLopsided(kSplitAndAddToLo);
372 }
373
374 void
376 {
377 testcase("NFTokenDir");
378
379 using namespace test::jtx;
380
381 // When a single NFT page exceeds 32 entries, the code is inclined
382 // to split that page into two equal pieces. The new page is lower
383 // than the original. There was an off-by-one in the selection of
384 // the index for the new page. This test recreates the problem.
385
386 // Lambda that exercises the split.
387 auto exercise = [this, &features](std::initializer_list<std::string_view const> seeds) {
388 Env env{*this, envconfig(), features, nullptr, beast::Severity::Disabled};
389
390 // Eventually all of the NFTokens will be owned by buyer.
391 Account const buyer{"buyer"};
392 env.fund(XRP(10000), buyer);
393 env.close();
394
395 // Create accounts for all of the seeds and fund those accounts.
396 std::vector<Account> accounts;
397 accounts.reserve(seeds.size());
398 for (std::string_view const seed : seeds)
399 {
400 Account const& account =
401 accounts.emplace_back(Account::AcctStringType::Base58Seed, std::string(seed));
402 env.fund(XRP(10000), account);
403
404 // Do not close the ledger inside the loop. If accounts are
405 // initialized at different ledgers, they will have
406 // different account sequences. That would cause the
407 // accounts to have different NFTokenID sequence numbers.
408 }
409 env.close();
410
411 // All of the accounts create one NFT and and offer that NFT to
412 // buyer.
415 offers.reserve(accounts.size());
416 for (Account const& account : accounts)
417 {
418 // Mint the NFT.
419 uint256 const& nftID =
420 nftIDs.emplace_back(token::getNextID(env, account, 0, tfTransferable));
421 env(token::mint(account, 0), Txflags(tfTransferable));
422 env.close();
423
424 // Create an offer to give the NFT to buyer for free.
425 offers.emplace_back(
426 keylet::nftokenOffer(account, SeqProxy::rawSequence(env.seq(account))).key);
427 env(token::createOffer(account, nftID, XRP(0)),
428 token::Destination(buyer),
429 Txflags(tfSellNFToken));
430 }
431 env.close();
432
433 // buyer accepts all of the but the last. The last offer
434 // causes the page to split.
435 for (std::size_t i = 0; i < offers.size() - 1; ++i)
436 {
437 env(token::acceptSellOffer(buyer, offers[i]));
438 env.close();
439 }
440
441 env(token::acceptSellOffer(buyer, offers.back()));
442 env.close();
443
444 // This can be a good time to look at the NFT pages.
445 // printNFTPages(env, noisy);
446
447 // Verify that all NFTs are owned by buyer and findable in the
448 // ledger by having buyer create sell offers for all of their
449 // NFTs. Attempting to sell an offer that the ledger can't find
450 // generates a non-tesSUCCESS error code.
451 for (uint256 const& nftID : nftIDs)
452 {
453 uint256 const offerID =
454 keylet::nftokenOffer(buyer, SeqProxy::rawSequence(env.seq(buyer))).key;
455 env(token::createOffer(buyer, nftID, XRP(100)), Txflags(tfSellNFToken));
456 env.close();
457
458 env(token::cancelOffer(buyer, {offerID}));
459 }
460
461 // Verify that all the NFTs are owned by buyer.
462 json::Value buyerNFTs = [&env, &buyer]() {
463 json::Value params;
464 params[jss::account] = buyer.human();
465 params[jss::type] = "state";
466 return env.rpc("json", "account_nfts", to_string(params));
467 }();
468
469 BEAST_EXPECT(buyerNFTs[jss::result][jss::account_nfts].size() == nftIDs.size());
470 for (json::Value const& ownedNFT : buyerNFTs[jss::result][jss::account_nfts])
471 {
472 uint256 ownedID;
473 BEAST_EXPECT(ownedID.parseHex(ownedNFT[sfNFTokenID.jsonName].asString()));
474 auto const foundIter = std::ranges::find(nftIDs, ownedID);
475
476 // Assuming we find the NFT, erase it so we know it's been
477 // found and can't be found again.
478 if (BEAST_EXPECT(foundIter != nftIDs.end()))
479 nftIDs.erase(foundIter);
480 }
481
482 // All NFTs should now be accounted for, so nftIDs should be
483 // empty.
484 BEAST_EXPECT(nftIDs.empty());
485 };
486
487 // These seeds fill the last 17 entries of the initial page with
488 // equivalent NFTs. The split should keep these together.
489 static std::initializer_list<std::string_view const> const kSeventeenHi{
490 // These 16 need to be kept together by the implementation.
491 "sp6JS7f14BuwFY8Mw5EYu5z86hKDL", // 0. 0x399187e9
492 "sp6JS7f14BuwFY8Mw5PUAMwc5ygd7", // 1. 0x399187e9
493 "sp6JS7f14BuwFY8Mw5R3xUBcLSeTs", // 2. 0x399187e9
494 "sp6JS7f14BuwFY8Mw5W6oS5sdC3oF", // 3. 0x399187e9
495 "sp6JS7f14BuwFY8Mw5pYc3D9iuLcw", // 4. 0x399187e9
496 "sp6JS7f14BuwFY8Mw5pfGVnhcdp3b", // 5. 0x399187e9
497 "sp6JS7f14BuwFY8Mw6jS6RdEqXqrN", // 6. 0x399187e9
498 "sp6JS7f14BuwFY8Mw6krt6AKbvRXW", // 7. 0x399187e9
499 "sp6JS7f14BuwFY8Mw6mnVBQq7cAN2", // 8. 0x399187e9
500 "sp6JS7f14BuwFY8Mw8ECJxPjmkufQ", // 9. 0x399187e9
501 "sp6JS7f14BuwFY8Mw8asgzcceGWYm", // 10. 0x399187e9
502 "sp6JS7f14BuwFY8MwF6J3FXnPCgL8", // 11. 0x399187e9
503 "sp6JS7f14BuwFY8MwFEud2w5czv5q", // 12. 0x399187e9
504 "sp6JS7f14BuwFY8MwFNxKVqJnx8P5", // 13. 0x399187e9
505 "sp6JS7f14BuwFY8MwFnTCXg3eRidL", // 14. 0x399187e9
506 "sp6JS7f14BuwFY8Mwj47hv1vrDge6", // 15. 0x399187e9
507
508 // These 17 need to be kept together by the implementation.
509 "sp6JS7f14BuwFY8MwjJCwYr9zSfAv", // 16. 0xabb11898
510 "sp6JS7f14BuwFY8MwjYa5yLkgCLuT", // 17. 0xabb11898
511 "sp6JS7f14BuwFY8MwjenxuJ3TH2Bc", // 18. 0xabb11898
512 "sp6JS7f14BuwFY8MwjriN7Ui11NzB", // 19. 0xabb11898
513 "sp6JS7f14BuwFY8Mwk3AuoJNSEo34", // 20. 0xabb11898
514 "sp6JS7f14BuwFY8MwkT36hnRv8hTo", // 21. 0xabb11898
515 "sp6JS7f14BuwFY8MwkTQixEXfi1Cr", // 22. 0xabb11898
516 "sp6JS7f14BuwFY8MwkYJaZM1yTJBF", // 23. 0xabb11898
517 "sp6JS7f14BuwFY8Mwkc4k1uo85qp2", // 24. 0xabb11898
518 "sp6JS7f14BuwFY8Mwkf7cFhF1uuxx", // 25. 0xabb11898
519 "sp6JS7f14BuwFY8MwmCK2un99wb4e", // 26. 0xabb11898
520 "sp6JS7f14BuwFY8MwmETztNHYu2Bx", // 27. 0xabb11898
521 "sp6JS7f14BuwFY8MwmJws9UwRASfR", // 28. 0xabb11898
522 "sp6JS7f14BuwFY8MwoH5PQkGK8tEb", // 29. 0xabb11898
523 "sp6JS7f14BuwFY8MwoVXtP2yCzjJV", // 30. 0xabb11898
524 "sp6JS7f14BuwFY8MwobxRXA9vsTeX", // 31. 0xabb11898
525 "sp6JS7f14BuwFY8Mwos3pc5Gb3ihU", // 32. 0xabb11898
526 };
527
528 // These seeds fill the first entries of the initial page with
529 // equivalent NFTs. The split should keep these together.
530 static std::initializer_list<std::string_view const> const kSeventeenLo{
531 // These 17 need to be kept together by the implementation.
532 "sp6JS7f14BuwFY8Mw5EYu5z86hKDL", // 0. 0x399187e9
533 "sp6JS7f14BuwFY8Mw5PUAMwc5ygd7", // 1. 0x399187e9
534 "sp6JS7f14BuwFY8Mw5R3xUBcLSeTs", // 2. 0x399187e9
535 "sp6JS7f14BuwFY8Mw5W6oS5sdC3oF", // 3. 0x399187e9
536 "sp6JS7f14BuwFY8Mw5pYc3D9iuLcw", // 4. 0x399187e9
537 "sp6JS7f14BuwFY8Mw5pfGVnhcdp3b", // 5. 0x399187e9
538 "sp6JS7f14BuwFY8Mw6jS6RdEqXqrN", // 6. 0x399187e9
539 "sp6JS7f14BuwFY8Mw6krt6AKbvRXW", // 7. 0x399187e9
540 "sp6JS7f14BuwFY8Mw6mnVBQq7cAN2", // 8. 0x399187e9
541 "sp6JS7f14BuwFY8Mw8ECJxPjmkufQ", // 9. 0x399187e9
542 "sp6JS7f14BuwFY8Mw8asgzcceGWYm", // 10. 0x399187e9
543 "sp6JS7f14BuwFY8MwF6J3FXnPCgL8", // 11. 0x399187e9
544 "sp6JS7f14BuwFY8MwFEud2w5czv5q", // 12. 0x399187e9
545 "sp6JS7f14BuwFY8MwFNxKVqJnx8P5", // 13. 0x399187e9
546 "sp6JS7f14BuwFY8MwFnTCXg3eRidL", // 14. 0x399187e9
547 "sp6JS7f14BuwFY8Mwj47hv1vrDge6", // 15. 0x399187e9
548 "sp6JS7f14BuwFY8Mwj6TYekeeyukh", // 16. 0x399187e9
549
550 // These 16 need to be kept together by the implementation.
551 "sp6JS7f14BuwFY8MwjYa5yLkgCLuT", // 17. 0xabb11898
552 "sp6JS7f14BuwFY8MwjenxuJ3TH2Bc", // 18. 0xabb11898
553 "sp6JS7f14BuwFY8MwjriN7Ui11NzB", // 19. 0xabb11898
554 "sp6JS7f14BuwFY8Mwk3AuoJNSEo34", // 20. 0xabb11898
555 "sp6JS7f14BuwFY8MwkT36hnRv8hTo", // 21. 0xabb11898
556 "sp6JS7f14BuwFY8MwkTQixEXfi1Cr", // 22. 0xabb11898
557 "sp6JS7f14BuwFY8MwkYJaZM1yTJBF", // 23. 0xabb11898
558 "sp6JS7f14BuwFY8Mwkc4k1uo85qp2", // 24. 0xabb11898
559 "sp6JS7f14BuwFY8Mwkf7cFhF1uuxx", // 25. 0xabb11898
560 "sp6JS7f14BuwFY8MwmCK2un99wb4e", // 26. 0xabb11898
561 "sp6JS7f14BuwFY8MwmETztNHYu2Bx", // 27. 0xabb11898
562 "sp6JS7f14BuwFY8MwmJws9UwRASfR", // 28. 0xabb11898
563 "sp6JS7f14BuwFY8MwoH5PQkGK8tEb", // 29. 0xabb11898
564 "sp6JS7f14BuwFY8MwoVXtP2yCzjJV", // 30. 0xabb11898
565 "sp6JS7f14BuwFY8MwobxRXA9vsTeX", // 31. 0xabb11898
566 "sp6JS7f14BuwFY8Mwos3pc5Gb3ihU", // 32. 0xabb11898
567 };
568
569 // Run the test cases.
570 exercise(kSeventeenHi);
571 exercise(kSeventeenLo);
572 }
573
574 void
576 {
577 // Exercise the case where 33 NFTs with identical sort
578 // characteristics are owned by the same account.
579 testcase("NFToken too many same");
580
581 using namespace test::jtx;
582
583 Env env{*this, features};
584
585 // Eventually all of the NFTokens will be owned by buyer.
586 Account const buyer{"buyer"};
587 env.fund(XRP(10000), buyer);
588 env.close();
589
590 // Here are 33 seeds that produce identical low 32-bits in their
591 // corresponding AccountIDs.
593 "sp6JS7f14BuwFY8Mw5FnqmbciPvH6", // 0. 0x9a8ebed3
594 "sp6JS7f14BuwFY8Mw5MBGbyMSsXLp", // 1. 0x9a8ebed3
595 "sp6JS7f14BuwFY8Mw5S4PnDyBdKKm", // 2. 0x9a8ebed3
596 "sp6JS7f14BuwFY8Mw6kcXpM2enE35", // 3. 0x9a8ebed3
597 "sp6JS7f14BuwFY8Mw6tuuSMMwyJ44", // 4. 0x9a8ebed3
598 "sp6JS7f14BuwFY8Mw8E8JWLQ1P8pt", // 5. 0x9a8ebed3
599 "sp6JS7f14BuwFY8Mw8WwdgWkCHhEx", // 6. 0x9a8ebed3
600 "sp6JS7f14BuwFY8Mw8XDUYvU6oGhQ", // 7. 0x9a8ebed3
601 "sp6JS7f14BuwFY8Mw8ceVGL4M1zLQ", // 8. 0x9a8ebed3
602 "sp6JS7f14BuwFY8Mw8fdSwLCZWDFd", // 9. 0x9a8ebed3
603 "sp6JS7f14BuwFY8Mw8zuF6Fg65i1E", // 10. 0x9a8ebed3
604 "sp6JS7f14BuwFY8MwF2k7bihVfqes", // 11. 0x9a8ebed3
605 "sp6JS7f14BuwFY8MwF6X24WXGn557", // 12. 0x9a8ebed3
606 "sp6JS7f14BuwFY8MwFMpn7strjekg", // 13. 0x9a8ebed3
607 "sp6JS7f14BuwFY8MwFSdy9sYVrwJs", // 14. 0x9a8ebed3
608 "sp6JS7f14BuwFY8MwFdMcLy9UkrXn", // 15. 0x9a8ebed3
609 "sp6JS7f14BuwFY8MwFdbwFm1AAboa", // 16. 0x9a8ebed3
610 "sp6JS7f14BuwFY8MwFdr5AhKThVtU", // 17. 0x9a8ebed3
611 "sp6JS7f14BuwFY8MwjFc3Q9YatvAw", // 18. 0x9a8ebed3
612 "sp6JS7f14BuwFY8MwjRXcNs1ozEXn", // 19. 0x9a8ebed3
613 "sp6JS7f14BuwFY8MwkQGUKL7v1FBt", // 20. 0x9a8ebed3
614 "sp6JS7f14BuwFY8Mwkamsoxx1wECt", // 21. 0x9a8ebed3
615 "sp6JS7f14BuwFY8Mwm3hus1dG6U8y", // 22. 0x9a8ebed3
616 "sp6JS7f14BuwFY8Mwm589M8vMRpXF", // 23. 0x9a8ebed3
617 "sp6JS7f14BuwFY8MwmJTRJ4Fqz1A3", // 24. 0x9a8ebed3
618 "sp6JS7f14BuwFY8MwmRfy8fer4QbL", // 25. 0x9a8ebed3
619 "sp6JS7f14BuwFY8MwmkkFx1HtgWRx", // 26. 0x9a8ebed3
620 "sp6JS7f14BuwFY8MwmwP9JFdKa4PS", // 27. 0x9a8ebed3
621 "sp6JS7f14BuwFY8MwoXWJLB3ciHfo", // 28. 0x9a8ebed3
622 "sp6JS7f14BuwFY8MwoYc1gTtT2mWL", // 29. 0x9a8ebed3
623 "sp6JS7f14BuwFY8MwogXtHH7FNVoo", // 30. 0x9a8ebed3
624 "sp6JS7f14BuwFY8MwoqYoA9P8gf3r", // 31. 0x9a8ebed3
625 "sp6JS7f14BuwFY8MwoujwMJofGnsA", // 32. 0x9a8ebed3
626 };
627
628 // Create accounts for all of the seeds and fund those accounts.
629 std::vector<Account> accounts;
630 accounts.reserve(kSeeds.size());
631 for (std::string_view const seed : kSeeds)
632 {
633 Account const& account =
634 accounts.emplace_back(Account::AcctStringType::Base58Seed, std::string(seed));
635 env.fund(XRP(10000), account);
636
637 // Do not close the ledger inside the loop. If accounts are
638 // initialized at different ledgers, they will have different
639 // account sequences. That would cause the accounts to have
640 // different NFTokenID sequence numbers.
641 }
642 env.close();
643
644 // All of the accounts create one NFT and and offer that NFT to buyer.
647 offers.reserve(accounts.size());
648 for (Account const& account : accounts)
649 {
650 // Mint the NFT.
651 uint256 const& nftID =
652 nftIDs.emplace_back(token::getNextID(env, account, 0, tfTransferable));
653 env(token::mint(account, 0), Txflags(tfTransferable));
654 env.close();
655
656 // Create an offer to give the NFT to buyer for free.
657 offers.emplace_back(
658 keylet::nftokenOffer(account, SeqProxy::rawSequence(env.seq(account))).key);
659 env(token::createOffer(account, nftID, XRP(0)),
660 token::Destination(buyer),
661 Txflags(tfSellNFToken));
662 }
663 env.close();
664
665 // Verify that the low 96 bits of all generated NFTs is identical.
666 uint256 const expectLowBits = nftIDs.front() & nft::kPageMask;
667 for (uint256 const& nftID : nftIDs)
668 {
669 BEAST_EXPECT(expectLowBits == (nftID & nft::kPageMask));
670 }
671
672 // Remove one NFT and offer from the vectors. This offer is the one
673 // that will overflow the page.
674 nftIDs.pop_back();
675 uint256 const offerForPageOverflow = offers.back();
676 offers.pop_back();
677
678 // buyer accepts all of the offers but one.
679 for (uint256 const& offer : offers)
680 {
681 env(token::acceptSellOffer(buyer, offer));
682 env.close();
683 }
684
685 // buyer accepts the last offer which causes a page overflow.
686 env(token::acceptSellOffer(buyer, offerForPageOverflow), Ter(tecNO_SUITABLE_NFTOKEN_PAGE));
687
688 // Verify that all expected NFTs are owned by buyer and findable in
689 // the ledger by having buyer create sell offers for all of their NFTs.
690 // Attempting to sell an offer that the ledger can't find generates
691 // a non-tesSUCCESS error code.
692 for (uint256 const& nftID : nftIDs)
693 {
694 uint256 const offerID =
695 keylet::nftokenOffer(buyer, SeqProxy::rawSequence(env.seq(buyer))).key;
696 env(token::createOffer(buyer, nftID, XRP(100)), Txflags(tfSellNFToken));
697 env.close();
698
699 env(token::cancelOffer(buyer, {offerID}));
700 }
701
702 // Verify that all the NFTs are owned by buyer.
703 json::Value buyerNFTs = [&env, &buyer]() {
704 json::Value params;
705 params[jss::account] = buyer.human();
706 params[jss::type] = "state";
707 return env.rpc("json", "account_nfts", to_string(params));
708 }();
709
710 BEAST_EXPECT(buyerNFTs[jss::result][jss::account_nfts].size() == nftIDs.size());
711 for (json::Value const& ownedNFT : buyerNFTs[jss::result][jss::account_nfts])
712 {
713 uint256 ownedID;
714 BEAST_EXPECT(ownedID.parseHex(ownedNFT[sfNFTokenID.jsonName].asString()));
715 auto const foundIter = std::ranges::find(nftIDs, ownedID);
716
717 // Assuming we find the NFT, erase it so we know it's been found
718 // and can't be found again.
719 if (BEAST_EXPECT(foundIter != nftIDs.end()))
720 nftIDs.erase(foundIter);
721 }
722
723 // All NFTs should now be accounted for, so nftIDs should be empty.
724 BEAST_EXPECT(nftIDs.empty());
725
726 TER const expect = tesSUCCESS;
727 env(token::mint(buyer, 0), Txflags(tfTransferable), Ter(expect));
728 env.close();
729 }
730
731 void
733 {
734 // We'll make a worst case scenario for NFT packing:
735 //
736 // 1. 33 accounts with identical low-32 bits mint 7 consecutive NFTs.
737 // 2. The taxon is manipulated to always be stored as zero.
738 // 3. A single account buys all 7x32 of the 33 NFTs.
739 //
740 // All of the NFTs should be acquired by the buyer.
741 //
742 // Lastly, kNone of the remaining NFTs should be acquirable by the
743 // buyer. They would cause page overflow.
744
745 testcase("NFToken consecutive packing");
746
747 using namespace test::jtx;
748
749 Env env{*this, features};
750
751 // Eventually all of the NFTokens will be owned by buyer.
752 Account const buyer{"buyer"};
753 env.fund(XRP(10000), buyer);
754 env.close();
755
756 // Here are 33 seeds that produce identical low 32-bits in their
757 // corresponding AccountIDs.
759 "sp6JS7f14BuwFY8Mw56vZeiBuhePx", // 0. 0x115d0525
760 "sp6JS7f14BuwFY8Mw5BodF9tGuTUe", // 1. 0x115d0525
761 "sp6JS7f14BuwFY8Mw5EnhC1cg84J7", // 2. 0x115d0525
762 "sp6JS7f14BuwFY8Mw5P913Cunr2BK", // 3. 0x115d0525
763 "sp6JS7f14BuwFY8Mw5Pru7eLo1XzT", // 4. 0x115d0525
764 "sp6JS7f14BuwFY8Mw61SLUC8UX2m8", // 5. 0x115d0525
765 "sp6JS7f14BuwFY8Mw6AsBF9TpeMpq", // 6. 0x115d0525
766 "sp6JS7f14BuwFY8Mw84XqrBZkU2vE", // 7. 0x115d0525
767 "sp6JS7f14BuwFY8Mw89oSU6dBk3KB", // 8. 0x115d0525
768 "sp6JS7f14BuwFY8Mw89qUKCyDmyzj", // 9. 0x115d0525
769 "sp6JS7f14BuwFY8Mw8GfqQ9VRZ8tm", // 10. 0x115d0525
770 "sp6JS7f14BuwFY8Mw8LtW3VqrqMks", // 11. 0x115d0525
771 "sp6JS7f14BuwFY8Mw8ZrAkJc2sHew", // 12. 0x115d0525
772 "sp6JS7f14BuwFY8Mw8jpkYSNrD3ah", // 13. 0x115d0525
773 "sp6JS7f14BuwFY8MwF2mshd786m3V", // 14. 0x115d0525
774 "sp6JS7f14BuwFY8MwFHfXq9x5NbPY", // 15. 0x115d0525
775 "sp6JS7f14BuwFY8MwFrjWq5LAB8NT", // 16. 0x115d0525
776 "sp6JS7f14BuwFY8Mwj4asgSh6hQZd", // 17. 0x115d0525
777 "sp6JS7f14BuwFY8Mwj7ipFfqBSRrE", // 18. 0x115d0525
778 "sp6JS7f14BuwFY8MwjHqtcvGav8uW", // 19. 0x115d0525
779 "sp6JS7f14BuwFY8MwjLp4sk5fmzki", // 20. 0x115d0525
780 "sp6JS7f14BuwFY8MwjioHuYb3Ytkx", // 21. 0x115d0525
781 "sp6JS7f14BuwFY8MwkRjHPXWi7fGN", // 22. 0x115d0525
782 "sp6JS7f14BuwFY8MwkdVdPV3LjNN1", // 23. 0x115d0525
783 "sp6JS7f14BuwFY8MwkxUtVY5AXZFk", // 24. 0x115d0525
784 "sp6JS7f14BuwFY8Mwm4jQzdfTbY9F", // 25. 0x115d0525
785 "sp6JS7f14BuwFY8MwmCucYAqNp4iF", // 26. 0x115d0525
786 "sp6JS7f14BuwFY8Mwo2bgdFtxBzpF", // 27. 0x115d0525
787 "sp6JS7f14BuwFY8MwoGwD7v4U6qBh", // 28. 0x115d0525
788 "sp6JS7f14BuwFY8MwoUczqFADMoXi", // 29. 0x115d0525
789 "sp6JS7f14BuwFY8MwoY1xZeGd3gAr", // 30. 0x115d0525
790 "sp6JS7f14BuwFY8MwomVCbfkv4kYZ", // 31. 0x115d0525
791 "sp6JS7f14BuwFY8MwoqbrPSr4z13F", // 32. 0x115d0525
792 };
793
794 // Create accounts for all of the seeds and fund those accounts.
795 std::vector<Account> accounts;
796 accounts.reserve(kSeeds.size());
797 for (std::string_view const seed : kSeeds)
798 {
799 Account const& account =
800 accounts.emplace_back(Account::AcctStringType::Base58Seed, std::string(seed));
801 env.fund(XRP(10000), account);
802
803 // Do not close the ledger inside the loop. If accounts are
804 // initialized at different ledgers, they will have different
805 // account sequences. That would cause the accounts to have
806 // different NFTokenID sequence numbers.
807 }
808 env.close();
809
810 // All of the accounts create seven consecutive NFTs and and offer
811 // those NFTs to buyer.
812 std::array<std::vector<uint256>, 7> nftIDsByPage;
813 for (auto& vec : nftIDsByPage)
814 vec.reserve(accounts.size());
816 for (auto& vec : offers)
817 vec.reserve(accounts.size());
818 for (std::size_t i = 0; i < nftIDsByPage.size(); ++i)
819 {
820 for (Account const& account : accounts)
821 {
822 // Mint the NFT. Tweak the taxon so zero is always stored.
823 std::uint32_t const taxon = toUInt32(nft::cipheredTaxon(i, nft::toTaxon(0)));
824
825 uint256 const& nftID = nftIDsByPage[i].emplace_back(
826 token::getNextID(env, account, taxon, tfTransferable));
827 env(token::mint(account, taxon), Txflags(tfTransferable));
828 env.close();
829
830 // Create an offer to give the NFT to buyer for free.
831 offers[i].emplace_back(
832 keylet::nftokenOffer(account, SeqProxy::rawSequence(env.seq(account))).key);
833 env(token::createOffer(account, nftID, XRP(0)),
834 token::Destination(buyer),
835 Txflags(tfSellNFToken));
836 }
837 }
838 env.close();
839
840 // Verify that the low 96 bits of all generated NFTs of the same
841 // sequence is identical.
842 for (auto const& vec : nftIDsByPage)
843 {
844 uint256 const expectLowBits = vec.front() & nft::kPageMask;
845 for (uint256 const& nftID : vec)
846 {
847 BEAST_EXPECT(expectLowBits == (nftID & nft::kPageMask));
848 }
849 }
850
851 // Remove one NFT and offer from each of the vectors. These offers
852 // are the ones that will overflow the page.
853 std::vector<uint256> overflowNFTs;
854 overflowNFTs.reserve(nftIDsByPage.size());
855 std::vector<uint256> overflowOffers;
856 overflowOffers.reserve(nftIDsByPage.size());
857
858 for (std::size_t i = 0; i < nftIDsByPage.size(); ++i)
859 {
860 overflowNFTs.push_back(nftIDsByPage[i].back());
861 nftIDsByPage[i].pop_back();
862 BEAST_EXPECT(nftIDsByPage[i].size() == kSeeds.size() - 1);
863
864 overflowOffers.push_back(offers[i].back());
865 offers[i].pop_back();
866 BEAST_EXPECT(offers[i].size() == kSeeds.size() - 1);
867 }
868
869 // buyer accepts all of the offers that won't cause an overflow.
870 // Fill the center and outsides first to exercise different boundary
871 // cases.
872 for (int const i : std::initializer_list<int>{3, 6, 0, 1, 2, 5, 4})
873 {
874 for (uint256 const& offer : offers[i])
875 {
876 env(token::acceptSellOffer(buyer, offer));
877 env.close();
878 }
879 }
880
881 // buyer accepts the seven offers that would cause page overflows if
882 // the transaction succeeded.
883 for (uint256 const& offer : overflowOffers)
884 {
885 env(token::acceptSellOffer(buyer, offer), Ter(tecNO_SUITABLE_NFTOKEN_PAGE));
886 env.close();
887 }
888
889 // Verify that all expected NFTs are owned by buyer and findable in
890 // the ledger by having buyer create sell offers for all of their NFTs.
891 // Attempting to sell an offer that the ledger can't find generates
892 // a non-tesSUCCESS error code.
893 for (auto const& vec : nftIDsByPage)
894 {
895 for (uint256 const& nftID : vec)
896 {
897 env(token::createOffer(buyer, nftID, XRP(100)), Txflags(tfSellNFToken));
898 env.close();
899 }
900 }
901
902 // See what the account_objects command does with "nft_offer".
903 {
904 json::Value ownedNftOffers(json::ValueType::Array);
905 std::string marker;
906 do
907 {
908 json::Value buyerOffers = [&env, &buyer, &marker]() {
909 json::Value params;
910 params[jss::account] = buyer.human();
911 params[jss::type] = jss::nft_offer;
912
913 if (!marker.empty())
914 params[jss::marker] = marker;
915 return env.rpc("json", "account_objects", to_string(params));
916 }();
917
918 marker.clear();
919 if (buyerOffers.isMember(jss::result))
920 {
921 json::Value& result = buyerOffers[jss::result];
922
923 if (result.isMember(jss::marker))
924 marker = result[jss::marker].asString();
925
926 if (result.isMember(jss::account_objects))
927 {
928 json::Value& someOffers = result[jss::account_objects];
929 for (std::size_t i = 0; i < someOffers.size(); ++i)
930 ownedNftOffers.append(someOffers[i]);
931 }
932 }
933 } while (!marker.empty());
934
935 // Verify there are as many offers are there are NFTs.
936 {
937 std::size_t totalOwnedNFTs = 0;
938 for (auto const& vec : nftIDsByPage)
939 totalOwnedNFTs += vec.size();
940 BEAST_EXPECT(ownedNftOffers.size() == totalOwnedNFTs);
941 }
942
943 // Cancel all the offers.
944 {
945 std::vector<uint256> cancelOffers;
946 cancelOffers.reserve(ownedNftOffers.size());
947
948 for (auto const& offer : ownedNftOffers)
949 {
950 if (offer.isMember(jss::index))
951 {
952 uint256 offerIndex;
953 if (offerIndex.parseHex(offer[jss::index].asString()))
954 cancelOffers.push_back(offerIndex);
955 }
956 }
957 env(token::cancelOffer(buyer, cancelOffers));
958 env.close();
959 }
960
961 // account_objects should no longer return any "nft_offer"s.
962 json::Value remainingOffers = [&env, &buyer]() {
963 json::Value params;
964 params[jss::account] = buyer.human();
965 params[jss::type] = jss::nft_offer;
966
967 return env.rpc("json", "account_objects", to_string(params));
968 }();
969 BEAST_EXPECT(
970 remainingOffers.isMember(jss::result) &&
971 remainingOffers[jss::result].isMember(jss::account_objects) &&
972 remainingOffers[jss::result][jss::account_objects].size() == 0);
973 }
974
975 // Verify that the ledger reports all of the NFTs owned by buyer.
976 // Use the account_nfts rpc call to get the values.
978 std::string marker;
979 do
980 {
981 json::Value buyerNFTs = [&env, &buyer, &marker]() {
982 json::Value params;
983 params[jss::account] = buyer.human();
984 params[jss::type] = "state";
985
986 if (!marker.empty())
987 params[jss::marker] = marker;
988 return env.rpc("json", "account_nfts", to_string(params));
989 }();
990
991 marker.clear();
992 if (buyerNFTs.isMember(jss::result))
993 {
994 json::Value& result = buyerNFTs[jss::result];
995
996 if (result.isMember(jss::marker))
997 marker = result[jss::marker].asString();
998
999 if (result.isMember(jss::account_nfts))
1000 {
1001 json::Value& someNFTs = result[jss::account_nfts];
1002 for (std::size_t i = 0; i < someNFTs.size(); ++i)
1003 ownedNFTs.append(someNFTs[i]);
1004 }
1005 }
1006 } while (!marker.empty());
1007
1008 // Copy all of the nftIDs into a set to make validation easier.
1009 std::set<uint256> allNftIDs;
1010 for (auto& vec : nftIDsByPage)
1011 allNftIDs.insert(vec.begin(), vec.end());
1012
1013 BEAST_EXPECT(ownedNFTs.size() == allNftIDs.size());
1014
1015 for (json::Value const& ownedNFT : ownedNFTs)
1016 {
1017 if (ownedNFT.isMember(sfNFTokenID.jsonName))
1018 {
1019 uint256 ownedID;
1020 BEAST_EXPECT(ownedID.parseHex(ownedNFT[sfNFTokenID.jsonName].asString()));
1021 auto const foundIter = allNftIDs.find(ownedID);
1022
1023 // Assuming we find the NFT, erase it so we know it's been found
1024 // and can't be found again.
1025 if (BEAST_EXPECT(foundIter != allNftIDs.end()))
1026 allNftIDs.erase(foundIter);
1027 }
1028 }
1029
1030 // All NFTs should now be accounted for, so allNftIDs should be empty.
1031 BEAST_EXPECT(allNftIDs.empty());
1032 }
1033
1034 void
1036 {
1037 testConsecutiveNFTs(features);
1038 testLopsidedSplits(features);
1039 testNFTokenDir(features);
1040 testTooManyEquivalent(features);
1041 testConsecutivePacking(features);
1042 }
1043
1044public:
1045 void
1046 run() override
1047 {
1048 using namespace test::jtx;
1049 FeatureBitset const all{testableAmendments()};
1050
1051 testWithFeats(all);
1052 }
1053};
1054
1056
1057} // namespace xrpl
1058
1059// Seed that produces an account with the low-32 bits == 0xFFFFFFFF in
1060// case it is needed for future testing:
1061//
1062// sp6JS7f14BuwFY8MwFe95Vpi9Znjs
1063//
A testsuite class.
Definition suite.h:52
bool expect(Condition const &shouldBeTrue)
Evaluate a test condition.
Definition suite.h:235
TestcaseT testcase
Memberspace for declaring test cases.
Definition suite.h:155
Represents a JSON value.
Definition json_value.h:117
std::string toStyledString() const
bool isArray() const
Value & append(Value const &value)
Append value to array at the end.
UInt size() const
Number of values in array or object.
std::string asString() const
Returns the unquoted string value.
bool isMember(char const *key) const
Return true if the object has a member named key.
constexpr bool parseHex(std::string_view sv)
Parse a hex string into a base_uint.
Definition base_uint.h:525
void testLopsidedSplits(FeatureBitset features)
void run() override
Runs the suite.
void testWithFeats(FeatureBitset features)
static void printNFTPages(test::jtx::Env &env, Volume vol)
void testNFTokenDir(FeatureBitset features)
void testConsecutivePacking(FeatureBitset features)
void testTooManyEquivalent(FeatureBitset features)
void testConsecutiveNFTs(FeatureBitset features)
static constexpr SeqProxy rawSequence(std::uint32_t v)
Factory function to return a sequence-based SeqProxy.
Definition SeqProxy.h:62
A transaction testing environment.
Definition Env.h:161
json::Value rpc(unsigned apiVersion, std::unordered_map< std::string, std::string > const &headers, std::string const &cmd, Args &&... args)
Execute an RPC command.
Definition Env.h:1056
T clear(T... args)
T emplace_back(T... args)
T empty(T... args)
T end(T... args)
T endl(T... args)
T erase(T... args)
T find(T... args)
T front(T... args)
T insert(T... args)
unsigned int UInt
@ Array
array value (ordered list)
Definition json_value.h:28
Keylet nftokenOffer(AccountID const &owner, SeqProxy const &seq)
An offer from an account to buy or sell an NFT.
Definition Indexes.cpp:423
constexpr uint256 kPageMask(std::string_view("0000000000000000000000000000000000000000ffffffffffffffffffffffff"))
Taxon toTaxon(std::uint32_t i)
Definition nft.h:21
Taxon cipheredTaxon(std::uint32_t tokenSeq, Taxon taxon)
Definition nft.h:63
Use hash_* containers for keys that do not need a cryptographically secure hashing algorithm.
Definition algorithm.h:5
BEAST_DEFINE_TESTSUITE_PRIO(AccountSet, app, xrpl, 1)
std::string to_string(BaseUInt< Bits, Tag > const &a)
Definition base_uint.h:651
TERSubset< CanCvtToTER > TER
Definition TER.h:647
@ tecNO_SUITABLE_NFTOKEN_PAGE
Definition TER.h:324
BaseUInt< 256 > uint256
Definition base_uint.h:580
@ tesSUCCESS
Definition TER.h:245
T pop_back(T... args)
T push_back(T... args)
T reserve(T... args)
T reverse(T... args)
T size(T... args)
uint256 key
Definition Keylet.h:21