rippled
NFTokenDir_test.cpp
1 //------------------------------------------------------------------------------
2 /*
3  This file is part of rippled: https://github.com/ripple/rippled
4  Copyright (c) 2022 Ripple Labs Inc.
5 
6  Permission to use, copy, modify, and/or distribute this software for any
7  purpose with or without fee is hereby granted, provided that the above
8  copyright notice and this permission notice appear in all copies.
9 
10  THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
11  WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
12  MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
13  ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
14  WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
15  ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
16  OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
17 */
18 //==============================================================================
19 
20 #include <ripple/protocol/Feature.h>
21 #include <ripple/protocol/jss.h>
22 #include <ripple/protocol/nftPageMask.h>
23 #include <test/jtx.h>
24 
25 #include <initializer_list>
26 
27 namespace ripple {
28 
29 class NFTokenDir_test : public beast::unit_test::suite
30 {
31  // printNFTPages is a helper function that may be used for debugging.
32  //
33  // It uses the ledger RPC command to show the NFT pages in the ledger.
34  // This parameter controls how noisy the output is.
35  enum Volume : bool {
36  quiet = false,
37  noisy = true,
38  };
39 
40  void
42  {
43  Json::Value jvParams;
44  jvParams[jss::ledger_index] = "current";
45  jvParams[jss::binary] = false;
46  {
47  Json::Value jrr = env.rpc(
48  "json",
49  "ledger_data",
50  boost::lexical_cast<std::string>(jvParams));
51 
52  // Iterate the state and print all NFTokenPages.
53  if (!jrr.isMember(jss::result) ||
54  !jrr[jss::result].isMember(jss::state))
55  {
56  std::cout << "No ledger state found!" << std::endl;
57  return;
58  }
59  Json::Value& state = jrr[jss::result][jss::state];
60  if (!state.isArray())
61  {
62  std::cout << "Ledger state is not array!" << std::endl;
63  return;
64  }
65  for (Json::UInt i = 0; i < state.size(); ++i)
66  {
67  if (state[i].isMember(sfNFTokens.jsonName) &&
68  state[i][sfNFTokens.jsonName].isArray())
69  {
70  std::uint32_t tokenCount =
71  state[i][sfNFTokens.jsonName].size();
72  std::cout << tokenCount << " NFtokens in page "
73  << state[i][jss::index].asString() << std::endl;
74 
75  if (vol == noisy)
76  {
77  std::cout << state[i].toStyledString() << std::endl;
78  }
79  else
80  {
81  if (tokenCount > 0)
82  std::cout << "first: "
83  << state[i][sfNFTokens.jsonName][0u]
85  << std::endl;
86  if (tokenCount > 1)
87  std::cout
88  << "last: "
89  << state[i][sfNFTokens.jsonName][tokenCount - 1]
91  << std::endl;
92  }
93  }
94  }
95  }
96  }
97 
98  void
100  {
101  // All NFT IDs with the same low 96 bits must stay on the same NFT page.
102  testcase("Lopsided splits");
103 
104  using namespace test::jtx;
105 
106  // When a single NFT page exceeds 32 entries, the code is inclined
107  // to split that page into two equal pieces. That's fine, but
108  // the code also needs to keep NFTs with identical low 96-bits on
109  // the same page.
110  //
111  // Here we synthesize cases where there are several NFTs with
112  // identical 96-low-bits in the middle of a page. When that page
113  // is split because it overflows, we need to see that the NFTs
114  // with identical 96-low-bits are all kept on the same page.
115 
116  // Lambda that exercises the lopsided splits.
117  auto exerciseLopsided =
118  [this,
120  Env env{*this, features};
121 
122  // Eventually all of the NFTokens will be owned by buyer.
123  Account const buyer{"buyer"};
124  env.fund(XRP(10000), buyer);
125  env.close();
126 
127  // Create accounts for all of the seeds and fund those accounts.
128  std::vector<Account> accounts;
129  accounts.reserve(seeds.size());
130  for (std::string_view const& seed : seeds)
131  {
132  Account const& account = accounts.emplace_back(
133  Account::base58Seed, std::string(seed));
134  env.fund(XRP(10000), account);
135  env.close();
136  }
137 
138  // All of the accounts create one NFT and and offer that NFT to
139  // buyer.
140  std::vector<uint256> nftIDs;
141  std::vector<uint256> offers;
142  offers.reserve(accounts.size());
143  for (Account const& account : accounts)
144  {
145  // Mint the NFT.
146  uint256 const& nftID = nftIDs.emplace_back(
147  token::getNextID(env, account, 0, tfTransferable));
148  env(token::mint(account, 0), txflags(tfTransferable));
149  env.close();
150 
151  // Create an offer to give the NFT to buyer for free.
152  offers.emplace_back(
153  keylet::nftoffer(account, env.seq(account)).key);
154  env(token::createOffer(account, nftID, XRP(0)),
155  token::destination(buyer),
156  txflags((tfSellNFToken)));
157  }
158  env.close();
159 
160  // buyer accepts all of the offers.
161  for (uint256 const& offer : offers)
162  {
163  env(token::acceptSellOffer(buyer, offer));
164  env.close();
165  }
166 
167  // This can be a good time to look at the NFT pages.
168  // printNFTPages(env, noisy);
169 
170  // Verify that all NFTs are owned by buyer and findable in the
171  // ledger by having buyer create sell offers for all of their
172  // NFTs. Attempting to sell an offer that the ledger can't find
173  // generates a non-tesSUCCESS error code.
174  for (uint256 const& nftID : nftIDs)
175  {
176  uint256 const offerID =
177  keylet::nftoffer(buyer, env.seq(buyer)).key;
178  env(token::createOffer(buyer, nftID, XRP(100)),
179  txflags(tfSellNFToken));
180  env.close();
181 
182  env(token::cancelOffer(buyer, {offerID}));
183  }
184 
185  // Verify that all the NFTs are owned by buyer.
186  Json::Value buyerNFTs = [&env, &buyer]() {
187  Json::Value params;
188  params[jss::account] = buyer.human();
189  params[jss::type] = "state";
190  return env.rpc("json", "account_nfts", to_string(params));
191  }();
192 
193  BEAST_EXPECT(
194  buyerNFTs[jss::result][jss::account_nfts].size() ==
195  nftIDs.size());
196  for (Json::Value const& ownedNFT :
197  buyerNFTs[jss::result][jss::account_nfts])
198  {
199  uint256 ownedID;
200  BEAST_EXPECT(ownedID.parseHex(
201  ownedNFT[sfNFTokenID.jsonName].asString()));
202  auto const foundIter =
203  std::find(nftIDs.begin(), nftIDs.end(), ownedID);
204 
205  // Assuming we find the NFT, erase it so we know it's been
206  // found and can't be found again.
207  if (BEAST_EXPECT(foundIter != nftIDs.end()))
208  nftIDs.erase(foundIter);
209  }
210 
211  // All NFTs should now be accounted for, so nftIDs should be
212  // empty.
213  BEAST_EXPECT(nftIDs.empty());
214  };
215 
216  // These seeds cause a lopsided split where the new NFT is added
217  // to the upper page.
219  splitAndAddToHi{
220  "sp6JS7f14BuwFY8Mw5p3b8jjQBBTK", // 0. 0x1d2932ea
221  "sp6JS7f14BuwFY8Mw6F7X3EiGKazu", // 1. 0x1d2932ea
222  "sp6JS7f14BuwFY8Mw6FxjntJJfKXq", // 2. 0x1d2932ea
223  "sp6JS7f14BuwFY8Mw6eSF1ydEozJg", // 3. 0x1d2932ea
224  "sp6JS7f14BuwFY8Mw6koPB91um2ej", // 4. 0x1d2932ea
225  "sp6JS7f14BuwFY8Mw6m6D64iwquSe", // 5. 0x1d2932ea
226 
227  "sp6JS7f14BuwFY8Mw5rC43sN4adC2", // 6. 0x208dbc24
228  "sp6JS7f14BuwFY8Mw65L9DDQqgebz", // 7. 0x208dbc24
229  "sp6JS7f14BuwFY8Mw65nKvU8pPQNn", // 8. 0x208dbc24
230  "sp6JS7f14BuwFY8Mw6bxZLyTrdipw", // 9. 0x208dbc24
231  "sp6JS7f14BuwFY8Mw6d5abucntSoX", // 10. 0x208dbc24
232  "sp6JS7f14BuwFY8Mw6qXK5awrRRP8", // 11. 0x208dbc24
233 
234  // These eight need to be kept together by the implementation.
235  "sp6JS7f14BuwFY8Mw66EBtMxoMcCa", // 12. 0x309b67ed
236  "sp6JS7f14BuwFY8Mw66dGfE9jVfGv", // 13. 0x309b67ed
237  "sp6JS7f14BuwFY8Mw6APdZa7PH566", // 14. 0x309b67ed
238  "sp6JS7f14BuwFY8Mw6C3QX5CZyET5", // 15. 0x309b67ed
239  "sp6JS7f14BuwFY8Mw6CSysFf8GvaR", // 16. 0x309b67ed
240  "sp6JS7f14BuwFY8Mw6c7QSDmoAeRV", // 17. 0x309b67ed
241  "sp6JS7f14BuwFY8Mw6mvonveaZhW7", // 18. 0x309b67ed
242  "sp6JS7f14BuwFY8Mw6vtHHG7dYcXi", // 19. 0x309b67ed
243 
244  "sp6JS7f14BuwFY8Mw66yppUNxESaw", // 20. 0x40d4b96f
245  "sp6JS7f14BuwFY8Mw6ATYQvobXiDT", // 21. 0x40d4b96f
246  "sp6JS7f14BuwFY8Mw6bis8D1Wa9Uy", // 22. 0x40d4b96f
247  "sp6JS7f14BuwFY8Mw6cTiGCWA8Wfa", // 23. 0x40d4b96f
248  "sp6JS7f14BuwFY8Mw6eAy2fpXmyYf", // 24. 0x40d4b96f
249  "sp6JS7f14BuwFY8Mw6icn58TRs8YG", // 25. 0x40d4b96f
250 
251  "sp6JS7f14BuwFY8Mw68tj2eQEWoJt", // 26. 0x503b6ba9
252  "sp6JS7f14BuwFY8Mw6AjnAinNnMHT", // 27. 0x503b6ba9
253  "sp6JS7f14BuwFY8Mw6CKDUwB4LrhL", // 28. 0x503b6ba9
254  "sp6JS7f14BuwFY8Mw6d2yPszEFA6J", // 29. 0x503b6ba9
255  "sp6JS7f14BuwFY8Mw6jcBQBH3PfnB", // 30. 0x503b6ba9
256  "sp6JS7f14BuwFY8Mw6qxx19KSnN1w", // 31. 0x503b6ba9
257 
258  // Adding this NFT splits the page. It is added to the upper
259  // page.
260  "sp6JS7f14BuwFY8Mw6ut1hFrqWoY5", // 32. 0x503b6ba9
261  };
262 
263  // These seeds cause a lopsided split where the new NFT is added
264  // to the lower page.
266  splitAndAddToLo{
267  "sp6JS7f14BuwFY8Mw5p3b8jjQBBTK", // 0. 0x1d2932ea
268  "sp6JS7f14BuwFY8Mw6F7X3EiGKazu", // 1. 0x1d2932ea
269  "sp6JS7f14BuwFY8Mw6FxjntJJfKXq", // 2. 0x1d2932ea
270  "sp6JS7f14BuwFY8Mw6eSF1ydEozJg", // 3. 0x1d2932ea
271  "sp6JS7f14BuwFY8Mw6koPB91um2ej", // 4. 0x1d2932ea
272  "sp6JS7f14BuwFY8Mw6m6D64iwquSe", // 5. 0x1d2932ea
273 
274  "sp6JS7f14BuwFY8Mw5rC43sN4adC2", // 6. 0x208dbc24
275  "sp6JS7f14BuwFY8Mw65L9DDQqgebz", // 7. 0x208dbc24
276  "sp6JS7f14BuwFY8Mw65nKvU8pPQNn", // 8. 0x208dbc24
277  "sp6JS7f14BuwFY8Mw6bxZLyTrdipw", // 9. 0x208dbc24
278  "sp6JS7f14BuwFY8Mw6d5abucntSoX", // 10. 0x208dbc24
279  "sp6JS7f14BuwFY8Mw6qXK5awrRRP8", // 11. 0x208dbc24
280 
281  // These eight need to be kept together by the implementation.
282  "sp6JS7f14BuwFY8Mw66EBtMxoMcCa", // 12. 0x309b67ed
283  "sp6JS7f14BuwFY8Mw66dGfE9jVfGv", // 13. 0x309b67ed
284  "sp6JS7f14BuwFY8Mw6APdZa7PH566", // 14. 0x309b67ed
285  "sp6JS7f14BuwFY8Mw6C3QX5CZyET5", // 15. 0x309b67ed
286  "sp6JS7f14BuwFY8Mw6CSysFf8GvaR", // 16. 0x309b67ed
287  "sp6JS7f14BuwFY8Mw6c7QSDmoAeRV", // 17. 0x309b67ed
288  "sp6JS7f14BuwFY8Mw6mvonveaZhW7", // 18. 0x309b67ed
289  "sp6JS7f14BuwFY8Mw6vtHHG7dYcXi", // 19. 0x309b67ed
290 
291  "sp6JS7f14BuwFY8Mw66yppUNxESaw", // 20. 0x40d4b96f
292  "sp6JS7f14BuwFY8Mw6ATYQvobXiDT", // 21. 0x40d4b96f
293  "sp6JS7f14BuwFY8Mw6bis8D1Wa9Uy", // 22. 0x40d4b96f
294  "sp6JS7f14BuwFY8Mw6cTiGCWA8Wfa", // 23. 0x40d4b96f
295  "sp6JS7f14BuwFY8Mw6eAy2fpXmyYf", // 24. 0x40d4b96f
296  "sp6JS7f14BuwFY8Mw6icn58TRs8YG", // 25. 0x40d4b96f
297 
298  "sp6JS7f14BuwFY8Mw68tj2eQEWoJt", // 26. 0x503b6ba9
299  "sp6JS7f14BuwFY8Mw6AjnAinNnMHT", // 27. 0x503b6ba9
300  "sp6JS7f14BuwFY8Mw6CKDUwB4LrhL", // 28. 0x503b6ba9
301  "sp6JS7f14BuwFY8Mw6d2yPszEFA6J", // 29. 0x503b6ba9
302  "sp6JS7f14BuwFY8Mw6jcBQBH3PfnB", // 30. 0x503b6ba9
303  "sp6JS7f14BuwFY8Mw6qxx19KSnN1w", // 31. 0x503b6ba9
304 
305  // Adding this NFT splits the page. It is added to the lower
306  // page.
307  "sp6JS7f14BuwFY8Mw6xCigaMwC6Dp", // 32. 0x309b67ed
308  };
309 
310  // FUTURE TEST
311  // These seeds fill the last 17 entries of the initial page with
312  // equivalent NFTs. The split should keep these together.
313 
314  // FUTURE TEST
315  // These seeds fill the first entries of the initial page with
316  // equivalent NFTs. The split should keep these together.
317 
318  // Run the test cases.
319  exerciseLopsided(splitAndAddToHi);
320  exerciseLopsided(splitAndAddToLo);
321  }
322 
323  void
325  {
326  // Exercise the case where 33 NFTs with identical sort
327  // characteristics are owned by the same account.
328  testcase("NFToken too many same");
329 
330  using namespace test::jtx;
331 
332  Env env{*this, features};
333 
334  // Eventually all of the NFTokens will be owned by buyer.
335  Account const buyer{"buyer"};
336  env.fund(XRP(10000), buyer);
337  env.close();
338 
339  // Here are 33 seeds that produce identical low 32-bits in their
340  // corresponding AccountIDs.
341  //
342  // NOTE: We've not yet identified 33 AccountIDs that meet the
343  // requirements. At the moment 12 is the best we can do. We'll fill
344  // in the full count when they are available.
346  "sp6JS7f14BuwFY8Mw5G5vCrbxB3TZ",
347  "sp6JS7f14BuwFY8Mw5H6qyXhorcip",
348  "sp6JS7f14BuwFY8Mw5suWxsBQRqLx",
349  "sp6JS7f14BuwFY8Mw66gtwamvGgSg",
350  "sp6JS7f14BuwFY8Mw66iNV4PPcmyt",
351  "sp6JS7f14BuwFY8Mw68Qz2P58ybfE",
352  "sp6JS7f14BuwFY8Mw6AYtLXKzi2Bo",
353  "sp6JS7f14BuwFY8Mw6boCES4j62P2",
354  "sp6JS7f14BuwFY8Mw6kv7QDDv7wjw",
355  "sp6JS7f14BuwFY8Mw6mHXMvpBjjwg",
356  "sp6JS7f14BuwFY8Mw6qfGbznyYvVp",
357  "sp6JS7f14BuwFY8Mw6zg6qHKDfSoU",
358  };
359 
360  // Create accounts for all of the seeds and fund those accounts.
361  std::vector<Account> accounts;
362  accounts.reserve(seeds.size());
363  for (std::string_view const& seed : seeds)
364  {
365  Account const& account =
366  accounts.emplace_back(Account::base58Seed, std::string(seed));
367  env.fund(XRP(10000), account);
368  env.close();
369  }
370 
371  // All of the accounts create one NFT and and offer that NFT to buyer.
372  std::vector<uint256> nftIDs;
373  std::vector<uint256> offers;
374  offers.reserve(accounts.size());
375  for (Account const& account : accounts)
376  {
377  // Mint the NFT.
378  uint256 const& nftID = nftIDs.emplace_back(
379  token::getNextID(env, account, 0, tfTransferable));
380  env(token::mint(account, 0), txflags(tfTransferable));
381  env.close();
382 
383  // Create an offer to give the NFT to buyer for free.
384  offers.emplace_back(
385  keylet::nftoffer(account, env.seq(account)).key);
386  env(token::createOffer(account, nftID, XRP(0)),
387  token::destination(buyer),
388  txflags((tfSellNFToken)));
389  }
390  env.close();
391 
392  // Verify that the low 96 bits of all generated NFTs is identical.
393  uint256 const expectLowBits = nftIDs.front() & nft::pageMask;
394  for (uint256 const& nftID : nftIDs)
395  {
396  BEAST_EXPECT(expectLowBits == (nftID & nft::pageMask));
397  }
398 
399  // buyer accepts all of the offers.
400  for (uint256 const& offer : offers)
401  {
402  env(token::acceptSellOffer(buyer, offer));
403  env.close();
404  }
405 
406  // Verify that all NFTs are owned by buyer and findable in the
407  // ledger by having buyer create sell offers for all of their NFTs.
408  // Attempting to sell an offer that the ledger can't find generates
409  // a non-tesSUCCESS error code.
410  for (uint256 const& nftID : nftIDs)
411  {
412  uint256 const offerID = keylet::nftoffer(buyer, env.seq(buyer)).key;
413  env(token::createOffer(buyer, nftID, XRP(100)),
414  txflags(tfSellNFToken));
415  env.close();
416 
417  env(token::cancelOffer(buyer, {offerID}));
418  }
419 
420  // Verify that all the NFTs are owned by buyer.
421  Json::Value buyerNFTs = [&env, &buyer]() {
422  Json::Value params;
423  params[jss::account] = buyer.human();
424  params[jss::type] = "state";
425  return env.rpc("json", "account_nfts", to_string(params));
426  }();
427 
428  BEAST_EXPECT(
429  buyerNFTs[jss::result][jss::account_nfts].size() == nftIDs.size());
430  for (Json::Value const& ownedNFT :
431  buyerNFTs[jss::result][jss::account_nfts])
432  {
433  uint256 ownedID;
434  BEAST_EXPECT(
435  ownedID.parseHex(ownedNFT[sfNFTokenID.jsonName].asString()));
436  auto const foundIter =
437  std::find(nftIDs.begin(), nftIDs.end(), ownedID);
438 
439  // Assuming we find the NFT, erase it so we know it's been found
440  // and can't be found again.
441  if (BEAST_EXPECT(foundIter != nftIDs.end()))
442  nftIDs.erase(foundIter);
443  }
444 
445  // All NFTs should now be accounted for, so nftIDs should be empty.
446  BEAST_EXPECT(nftIDs.empty());
447  }
448 
449  void
451  {
452  testLopsidedSplits(features);
453  testTooManyEquivalent(features);
454  }
455 
456 public:
457  void
458  run() override
459  {
460  using namespace test::jtx;
461  auto const sa = supported_amendments();
462  testWithFeats(sa);
463  }
464 };
465 
466 BEAST_DEFINE_TESTSUITE_PRIO(NFTokenDir, tx, ripple, 1);
467 
468 } // namespace ripple
ripple::tfTransferable
constexpr const std::uint32_t tfTransferable
Definition: TxFlags.h:121
ripple::NFTokenDir_test::testWithFeats
void testWithFeats(FeatureBitset features)
Definition: NFTokenDir_test.cpp:450
std::string
STL class.
std::string_view
STL class.
ripple::sfNFTokenID
const SF_UINT256 sfNFTokenID
std::vector::reserve
T reserve(T... args)
Json::UInt
unsigned int UInt
Definition: json_forwards.h:27
std::vector
STL class.
std::find
T find(T... args)
std::vector::size
T size(T... args)
ripple::keylet::nftoffer
Keylet nftoffer(AccountID const &owner, std::uint32_t seq)
An offer from an account to buy or sell an NFT.
Definition: Indexes.cpp:355
ripple::NFTokenDir_test::testTooManyEquivalent
void testTooManyEquivalent(FeatureBitset features)
Definition: NFTokenDir_test.cpp:324
Json::Value::toStyledString
std::string toStyledString() const
Definition: json_value.cpp:1039
ripple::BEAST_DEFINE_TESTSUITE_PRIO
BEAST_DEFINE_TESTSUITE_PRIO(NFToken, tx, ripple, 2)
ripple::SField::jsonName
const Json::StaticString jsonName
Definition: SField.h:136
std::vector::front
T front(T... args)
ripple::nft::pageMask
constexpr uint256 pageMask(std::string_view("0000000000000000000000000000000000000000ffffffffffffffffffffffff"))
ripple::NFTokenDir_test::testLopsidedSplits
void testLopsidedSplits(FeatureBitset features)
Definition: NFTokenDir_test.cpp:99
std::cout
ripple::Keylet::key
uint256 key
Definition: Keylet.h:40
ripple::base_uint< 256 >
ripple::NFTokenDir_test::run
void run() override
Definition: NFTokenDir_test.cpp:458
ripple::NFTokenDir_test::printNFTPages
void printNFTPages(test::jtx::Env &env, Volume vol)
Definition: NFTokenDir_test.cpp:41
Json::Value::size
UInt size() const
Number of values in array or object.
Definition: json_value.cpp:706
ripple::tfSellNFToken
constexpr const std::uint32_t tfSellNFToken
Definition: TxFlags.h:127
ripple::NFTokenDir_test::quiet
@ quiet
Definition: NFTokenDir_test.cpp:36
Json::Value::isMember
bool isMember(const char *key) const
Return true if the object has a member named key.
Definition: json_value.cpp:932
std::uint32_t
ripple::NFTokenDir_test::noisy
@ noisy
Definition: NFTokenDir_test.cpp:37
Json::Value::isArray
bool isArray() const
Definition: json_value.cpp:1015
ripple::sfNFTokens
const SField sfNFTokens
std::vector::emplace_back
T emplace_back(T... args)
ripple
Use hash_* containers for keys that do not need a cryptographically secure hashing algorithm.
Definition: RCLCensorshipDetector.h:29
std::endl
T endl(T... args)
ripple::NFTokenDir_test::Volume
Volume
Definition: NFTokenDir_test.cpp:35
ripple::FeatureBitset
Definition: Feature.h:113
ripple::to_string
std::string to_string(Manifest const &m)
Format the specified manifest to a string for debugging purposes.
Definition: app/misc/impl/Manifest.cpp:41
ripple::base_uint::parseHex
constexpr bool parseHex(std::string_view sv)
Parse a hex string into a base_uint.
Definition: base_uint.h:489
ripple::NFTokenDir_test
Definition: NFTokenDir_test.cpp:29
ripple::test::jtx::Env
A transaction testing environment.
Definition: Env.h:116
ripple::test::jtx::Env::rpc
Json::Value rpc(std::unordered_map< std::string, std::string > const &headers, std::string const &cmd, Args &&... args)
Execute an RPC command.
Definition: Env.h:684
Json::Value
Represents a JSON value.
Definition: json_value.h:145
initializer_list
Json::Value::asString
std::string asString() const
Returns the unquoted string value.
Definition: json_value.cpp:469