xrpld
Loading...
Searching...
No Matches
Batch_test.cpp
1#include <test/jtx/Account.h>
2#include <test/jtx/Env.h>
3#include <test/jtx/TestHelpers.h>
4#include <test/jtx/acctdelete.h>
5#include <test/jtx/amount.h>
6#include <test/jtx/balance.h> // IWYU pragma: keep
7#include <test/jtx/batch.h>
8#include <test/jtx/check.h>
9#include <test/jtx/delegate.h>
10#include <test/jtx/envconfig.h>
11#include <test/jtx/fee.h>
12#include <test/jtx/flags.h>
13#include <test/jtx/mpt.h>
14#include <test/jtx/multisign.h>
15#include <test/jtx/noop.h>
16#include <test/jtx/offer.h>
17#include <test/jtx/pay.h>
18#include <test/jtx/regkey.h>
19#include <test/jtx/seq.h>
20#include <test/jtx/sig.h>
21#include <test/jtx/tags.h>
22#include <test/jtx/ter.h>
23#include <test/jtx/ticket.h>
24#include <test/jtx/trust.h>
25#include <test/jtx/txflags.h>
26#include <test/jtx/utility.h>
27#include <test/jtx/vault.h>
28
29#include <xrpld/app/misc/Transaction.h>
30#include <xrpld/app/misc/TxQ.h>
31
32#include <xrpl/basics/base_uint.h>
33#include <xrpl/basics/strHex.h>
34#include <xrpl/beast/unit_test/suite.h>
35#include <xrpl/beast/utility/Journal.h>
36#include <xrpl/config/BasicConfig.h>
37#include <xrpl/config/Constants.h>
38#include <xrpl/core/HashRouter.h>
39#include <xrpl/json/json_value.h>
40#include <xrpl/json/to_string.h>
41#include <xrpl/ledger/ApplyView.h>
42#include <xrpl/ledger/OpenView.h>
43#include <xrpl/protocol/AccountID.h>
44#include <xrpl/protocol/Batch.h>
45#include <xrpl/protocol/Feature.h>
46#include <xrpl/protocol/Indexes.h>
47#include <xrpl/protocol/Issue.h>
48#include <xrpl/protocol/KeyType.h>
49#include <xrpl/protocol/LedgerFormats.h>
50#include <xrpl/protocol/Protocol.h>
51#include <xrpl/protocol/SField.h>
52#include <xrpl/protocol/STParsedJSON.h>
53#include <xrpl/protocol/STTx.h>
54#include <xrpl/protocol/SecretKey.h>
55#include <xrpl/protocol/Serializer.h>
56#include <xrpl/protocol/SystemParameters.h>
57#include <xrpl/protocol/TER.h>
58#include <xrpl/protocol/TxFlags.h>
59#include <xrpl/protocol/TxFormats.h>
60#include <xrpl/protocol/Units.h>
61#include <xrpl/protocol/XRPAmount.h>
62#include <xrpl/protocol/jss.h>
63#include <xrpl/server/NetworkOPs.h>
64#include <xrpl/tx/apply.h>
65#include <xrpl/tx/transactors/system/Batch.h>
66
67#include <algorithm>
68#include <cstdint>
69#include <map>
70#include <memory>
71#include <optional>
72#include <sstream>
73#include <string>
74#include <utility>
75#include <vector>
76
77namespace xrpl::test {
78
80{
89
95
96 static json::Value
97 getTxByIndex(json::Value const& jrr, int const index)
98 {
99 for (auto const& txn : jrr[jss::result][jss::ledger][jss::transactions])
100 {
101 if (txn[jss::metaData][sfTransactionIndex.jsonName] == index)
102 return txn;
103 }
104 return {};
105 }
106
107 static json::Value
109 {
110 json::Value params;
111 params[jss::ledger_index] = env.closed()->seq();
112 params[jss::transactions] = true;
113 params[jss::expand] = true;
114 return env.rpc("json", "ledger", to_string(params));
115 }
116
117 void
118 validateInnerTxn(jtx::Env& env, std::string const& batchID, TestLedgerData const& ledgerResult)
119 {
120 json::Value const jrr = env.rpc("tx", ledgerResult.txHash)[jss::result];
121 BEAST_EXPECT(jrr[sfTransactionType.jsonName] == ledgerResult.txType);
122 BEAST_EXPECT(jrr[jss::meta][sfTransactionResult.jsonName] == ledgerResult.result);
123 BEAST_EXPECT(jrr[jss::meta][sfParentBatchID.jsonName] == batchID);
124 }
125
126 void
128 {
129 auto const jrr = getLastLedger(env);
130 auto const transactions = jrr[jss::result][jss::ledger][jss::transactions];
131 BEAST_EXPECT(transactions.size() == ledgerResults.size());
132 for (TestLedgerData const& ledgerResult : ledgerResults)
133 {
134 auto const txn = getTxByIndex(jrr, ledgerResult.index);
135 BEAST_EXPECT(txn[jss::hash].asString() == ledgerResult.txHash);
136 BEAST_EXPECT(txn.isMember(jss::metaData));
137 json::Value const meta = txn[jss::metaData];
138 BEAST_EXPECT(txn[sfTransactionType.jsonName] == ledgerResult.txType);
139 BEAST_EXPECT(meta[sfTransactionResult.jsonName] == ledgerResult.result);
140 if (ledgerResult.batchID)
141 validateInnerTxn(env, *ledgerResult.batchID, ledgerResult);
142 }
143 }
144
145 template <typename... Args>
147 submitBatch(jtx::Env& env, TER const& result, Args&&... args)
148 {
149 auto batchTxn = env.jt(std::forward<Args>(args)...);
150 env(batchTxn, jtx::Ter(result));
151
152 auto const ids = batchTxn.stx->getBatchTransactionIDs();
154 txIDs.reserve(ids.size());
155 for (auto const& id : ids)
156 txIDs.push_back(strHex(id));
157 TxID const batchID = batchTxn.stx->getTransactionID();
158 return std::make_pair(txIDs, strHex(batchID));
159 }
160
161 static uint256
162 getCheckIndex(AccountID const& account, std::uint32_t uSequence)
163 {
164 return keylet::check(account, uSequence).key;
165 }
166
170 std::map<std::string, std::string> extraVoting = {})
171 {
172 auto p = test::jtx::envconfig();
173 auto& section = p->section(Sections::kTransactionQueue);
174 section.set(Keys::kLedgersInQueue, "2");
175 section.set(Keys::kMinimumQueueSize, "2");
176 section.set(Keys::kMinLedgersToComputeSizeLimit, "3");
177 section.set(Keys::kMaxLedgerCountsToStore, "100");
178 section.set(Keys::kRetrySequencePercent, "25");
180
181 for (auto const& [k, v] : extraTxQ)
182 section.set(k, v);
183
184 return p;
185 }
186
187 static auto
188 openLedgerFee(jtx::Env& env, XRPAmount const& batchFee)
189 {
190 using namespace jtx;
191
192 auto const& view = *env.current();
193 auto metrics = env.app().getTxQ().getMetrics(view);
194 return toDrops(metrics.openLedgerFeeLevel, batchFee) + 1;
195 }
196
197 void
199 {
200 using namespace test::jtx;
201 using namespace std::literals;
202
203 bool const withInnerSigFix = features[fixBatchInnerSigs];
204
205 for (bool const withBatch : {true, false})
206 {
207 testcase << "enabled: Batch " << (withBatch ? "enabled" : "disabled")
208 << ", Inner Sig Fix: " << (withInnerSigFix ? "enabled" : "disabled");
209
210 auto const amend = withBatch ? features : features - featureBatch;
211
212 test::jtx::Env env{*this, amend};
213
214 auto const alice = Account("alice");
215 auto const bob = Account("bob");
216 auto const carol = Account("carol");
217 env.fund(XRP(10000), alice, bob, carol);
218 env.close();
219
220 // ttBatch
221 {
222 auto const seq = env.seq(alice);
223 auto const batchFee = batch::calcBatchFee(env, 0, 2);
224 auto const txResult = withBatch ? Ter(tesSUCCESS) : Ter(temDISABLED);
225 env(batch::outer(alice, seq, batchFee, tfAllOrNothing),
226 batch::Inner(pay(alice, bob, XRP(1)), seq + 1),
227 batch::Inner(pay(alice, bob, XRP(1)), seq + 2),
228 txResult);
229 env.close();
230 }
231
232 // tfInnerBatchTxn
233 // If the feature is disabled, the transaction fails with
234 // temINVALID_FLAG. If the feature is enabled, the transaction fails
235 // early in checkValidity()
236 {
237 auto const txResult = withBatch ? Ter(telENV_RPC_FAILED) : Ter(temINVALID_FLAG);
238 env(pay(alice, bob, XRP(1)), Txflags(tfInnerBatchTxn), txResult);
239 env.close();
240 }
241
242 env.close();
243 }
244 }
245
246 void
248 {
249 testcase("preflight");
250
251 using namespace test::jtx;
252 using namespace std::literals;
253
254 //----------------------------------------------------------------------
255 // preflight
256
257 test::jtx::Env env{*this, features};
258
259 auto const alice = Account("alice");
260 auto const bob = Account("bob");
261 auto const carol = Account("carol");
262 env.fund(XRP(10000), alice, bob, carol);
263 env.close();
264
265 // temBAD_FEE: preflight1
266 {
267 env(batch::outer(alice, env.seq(alice), XRP(-1), tfAllOrNothing), Ter(temBAD_FEE));
268 env.close();
269 }
270
271 // DEFENSIVE: temINVALID_FLAG: Batch: inner batch flag.
272 // ACTUAL: telENV_RPC_FAILED: checkValidity()
273 {
274 auto const seq = env.seq(alice);
275 auto const batchFee = batch::calcBatchFee(env, 0, 0);
276 env(batch::outer(alice, seq, batchFee, tfInnerBatchTxn), Ter(telENV_RPC_FAILED));
277 env.close();
278 }
279
280 // temINVALID_FLAG: Batch: invalid flags.
281 {
282 auto const seq = env.seq(alice);
283 auto const batchFee = batch::calcBatchFee(env, 0, 0);
284 env(batch::outer(alice, seq, batchFee, tfDisallowXRP), Ter(temINVALID_FLAG));
285 env.close();
286 }
287
288 // temINVALID_FLAG: Batch: too many flags.
289 {
290 auto const seq = env.seq(alice);
291 auto const batchFee = batch::calcBatchFee(env, 0, 0);
292 env(batch::outer(alice, seq, batchFee, tfAllOrNothing),
293 Txflags(tfAllOrNothing | tfOnlyOne),
295 env.close();
296 }
297
298 // temARRAY_EMPTY: Batch: txns array must have at least 2 entries.
299 {
300 auto const seq = env.seq(alice);
301 auto const batchFee = batch::calcBatchFee(env, 0, 0);
302 env(batch::outer(alice, seq, batchFee, tfAllOrNothing), Ter(temARRAY_EMPTY));
303 env.close();
304 }
305
306 // temARRAY_EMPTY: Batch: txns array must have at least 2 entries.
307 {
308 auto const seq = env.seq(alice);
309 auto const batchFee = batch::calcBatchFee(env, 0, 0);
310 env(batch::outer(alice, seq, batchFee, tfAllOrNothing),
311 batch::Inner(pay(alice, bob, XRP(1)), seq + 1),
313 env.close();
314 }
315
316 // DEFENSIVE: temARRAY_TOO_LARGE: Batch: txns array exceeds 8 entries.
317 // ACTUAL: telENV_RPC_FAILED: isRawTransactionOkay()
318 {
319 auto const seq = env.seq(alice);
320 auto const batchFee = batch::calcBatchFee(env, 0, 9);
321 env(batch::outer(alice, seq, batchFee, tfAllOrNothing),
322 batch::Inner(pay(alice, bob, XRP(1)), seq + 1),
323 batch::Inner(pay(alice, bob, XRP(1)), seq + 2),
324 batch::Inner(pay(alice, bob, XRP(1)), seq + 3),
325 batch::Inner(pay(alice, bob, XRP(1)), seq + 4),
326 batch::Inner(pay(alice, bob, XRP(1)), seq + 5),
327 batch::Inner(pay(alice, bob, XRP(1)), seq + 6),
328 batch::Inner(pay(alice, bob, XRP(1)), seq + 7),
329 batch::Inner(pay(alice, bob, XRP(1)), seq + 8),
330 batch::Inner(pay(alice, bob, XRP(1)), seq + 9),
332 env.close();
333 }
334
335 // temREDUNDANT: Batch: duplicate Txn found.
336 {
337 auto const batchFee = batch::calcBatchFee(env, 1, 2);
338 auto const seq = env.seq(alice);
339 auto jt = env.jtnofill(
340 batch::outer(alice, env.seq(alice), batchFee, tfAllOrNothing),
341 batch::Inner(pay(alice, bob, XRP(10)), seq + 1),
342 batch::Inner(pay(alice, bob, XRP(10)), seq + 1));
343
344 env(jt.jv, batch::Sig(bob), Ter(temREDUNDANT));
345 env.close();
346 }
347
348 // DEFENSIVE: temINVALID: Batch: batch cannot have inner batch txn.
349 // ACTUAL: telENV_RPC_FAILED: isRawTransactionOkay()
350 {
351 auto const seq = env.seq(alice);
352 auto const batchFee = batch::calcBatchFee(env, 0, 2);
353 env(batch::outer(alice, seq, batchFee, tfAllOrNothing),
354 batch::Inner(batch::outer(alice, seq, batchFee, tfAllOrNothing), seq),
355 batch::Inner(pay(alice, bob, XRP(1)), seq + 2),
357 env.close();
358 }
359
360 // temINVALID_FLAG: Batch: inner txn must have the
361 // tfInnerBatchTxn flag.
362 {
363 auto const batchFee = batch::calcBatchFee(env, 1, 2);
364 auto const seq = env.seq(alice);
365 auto tx1 = batch::Inner(pay(alice, bob, XRP(10)), seq + 1);
366 tx1[jss::Flags] = 0;
367 auto jt = env.jtnofill(
368 batch::outer(alice, seq, batchFee, tfAllOrNothing),
369 tx1,
370 batch::Inner(pay(alice, bob, XRP(10)), seq + 2));
371
372 env(jt.jv, batch::Sig(bob), Ter(temINVALID_FLAG));
373 env.close();
374 }
375
376 // temBAD_SIGNATURE: Batch: inner txn cannot include TxnSignature.
377 {
378 auto const seq = env.seq(alice);
379 auto const batchFee = batch::calcBatchFee(env, 0, 2);
380 auto jt = env.jt(pay(alice, bob, XRP(1)));
381 env(batch::outer(alice, seq, batchFee, tfAllOrNothing),
382 batch::Inner(jt.jv, seq + 1),
383 batch::Inner(pay(alice, bob, XRP(1)), seq + 2),
385 env.close();
386 }
387
388 // temBAD_SIGNER: Batch: inner txn cannot include Signers.
389 {
390 auto const seq = env.seq(alice);
391 auto const batchFee = batch::calcBatchFee(env, 0, 2);
392 auto tx1 = pay(alice, bob, XRP(1));
393 tx1[sfSigners.jsonName] = json::ValueType::Array;
394 tx1[sfSigners.jsonName][0U][sfSigner.jsonName] = json::ValueType::Object;
395 tx1[sfSigners.jsonName][0U][sfSigner.jsonName][sfAccount.jsonName] = alice.human();
396 tx1[sfSigners.jsonName][0U][sfSigner.jsonName][sfSigningPubKey.jsonName] =
397 strHex(alice.pk());
398 tx1[sfSigners.jsonName][0U][sfSigner.jsonName][sfTxnSignature.jsonName] = "DEADBEEF";
399 env(batch::outer(alice, seq, batchFee, tfAllOrNothing),
400 batch::Inner(tx1, seq + 1),
401 batch::Inner(pay(alice, bob, XRP(1)), seq + 2),
403 env.close();
404 }
405
406 // temBAD_REGKEY: Batch: inner txn must include empty
407 // SigningPubKey.
408 {
409 auto const seq = env.seq(alice);
410 auto const batchFee = batch::calcBatchFee(env, 0, 2);
411 auto tx1 = batch::Inner(pay(alice, bob, XRP(1)), seq + 1);
412 tx1[jss::SigningPubKey] = strHex(alice.pk());
413 auto jt = env.jtnofill(
414 batch::outer(alice, seq, batchFee, tfAllOrNothing),
415 tx1,
416 batch::Inner(pay(alice, bob, XRP(1)), seq + 2));
417
418 env(jt.jv, Ter(temBAD_REGKEY));
419 env.close();
420 }
421
422 // temINVALID_INNER_BATCH: Batch: inner txn preflight failed.
423 {
424 auto const seq = env.seq(alice);
425 auto const batchFee = batch::calcBatchFee(env, 0, 2);
426 env(batch::outer(alice, seq, batchFee, tfAllOrNothing),
427 batch::Inner(pay(alice, bob, XRP(1)), seq + 1),
428 // amount can't be negative
429 batch::Inner(pay(alice, bob, XRP(-1)), seq + 2),
431 env.close();
432 }
433
434 // temBAD_FEE: Batch: inner txn must have a fee of 0.
435 {
436 auto const seq = env.seq(alice);
437 auto const batchFee = batch::calcBatchFee(env, 0, 2);
438 auto tx1 = batch::Inner(pay(alice, bob, XRP(1)), seq + 1);
439 tx1[jss::Fee] = to_string(env.current()->fees().base);
440 env(batch::outer(alice, seq, batchFee, tfAllOrNothing),
441 tx1,
442 batch::Inner(pay(alice, bob, XRP(2)), seq + 2),
443 Ter(temBAD_FEE));
444 env.close();
445 }
446
447 // temBAD_FEE: Inner txn with negative fee
448 {
449 auto const seq = env.seq(alice);
450 auto const batchFee = batch::calcBatchFee(env, 0, 2);
451 auto tx1 = batch::Inner(pay(alice, bob, XRP(1)), seq + 1);
452 tx1[jss::Fee] = "-1";
453 env(batch::outer(alice, seq, batchFee, tfAllOrNothing),
454 tx1,
455 batch::Inner(pay(alice, bob, XRP(2)), seq + 2),
456 Ter(temBAD_FEE));
457 env.close();
458 }
459
460 // temBAD_FEE: Inner txn with non-integer fee
461 {
462 auto const seq = env.seq(alice);
463 auto const batchFee = batch::calcBatchFee(env, 0, 2);
464 auto tx1 = batch::Inner(pay(alice, bob, XRP(1)), seq + 1);
465 tx1[jss::Fee] = "1.5";
466 env.setParseFailureExpected(true);
467 try
468 {
469 env(batch::outer(alice, seq, batchFee, tfAllOrNothing),
470 tx1,
471 batch::Inner(pay(alice, bob, XRP(2)), seq + 2));
472 fail("Expected parse_error for fractional fee");
473 }
474 catch (jtx::ParseError const&)
475 {
476 BEAST_EXPECT(true);
477 }
478 env.setParseFailureExpected(false);
479 }
480
481 // temSEQ_AND_TICKET: Batch: inner txn cannot have both Sequence
482 // and TicketSequence.
483 {
484 auto const seq = env.seq(alice);
485 auto const batchFee = batch::calcBatchFee(env, 0, 2);
486 auto tx1 = batch::Inner(pay(alice, bob, XRP(1)), 0, 1);
487 tx1[jss::Sequence] = seq + 1;
488 env(batch::outer(alice, seq, batchFee, tfAllOrNothing),
489 tx1,
490 batch::Inner(pay(alice, bob, XRP(2)), seq + 2),
492 env.close();
493 }
494
495 // temSEQ_AND_TICKET: Batch: inner txn must have either Sequence or
496 // TicketSequence.
497 {
498 auto const seq = env.seq(alice);
499 auto const batchFee = batch::calcBatchFee(env, 0, 2);
500 env(batch::outer(alice, seq, batchFee, tfAllOrNothing),
501 batch::Inner(pay(alice, bob, XRP(1)), 0),
502 batch::Inner(pay(alice, bob, XRP(2)), seq + 2),
504 env.close();
505 }
506
507 // temREDUNDANT: Batch: duplicate sequence found:
508 {
509 auto const seq = env.seq(alice);
510 auto const batchFee = batch::calcBatchFee(env, 0, 2);
511 env(batch::outer(alice, seq, batchFee, tfAllOrNothing),
512 batch::Inner(pay(alice, bob, XRP(1)), seq + 1),
513 batch::Inner(pay(alice, bob, XRP(2)), seq + 1),
515 env.close();
516 }
517
518 // temREDUNDANT: Batch: duplicate ticket found:
519 {
520 auto const seq = env.seq(alice);
521 auto const batchFee = batch::calcBatchFee(env, 0, 2);
522 env(batch::outer(alice, seq, batchFee, tfAllOrNothing),
523 batch::Inner(pay(alice, bob, XRP(1)), 0, seq + 1),
524 batch::Inner(pay(alice, bob, XRP(2)), 0, seq + 1),
526 env.close();
527 }
528
529 // temREDUNDANT: Batch: duplicate ticket & sequence found:
530 {
531 auto const seq = env.seq(alice);
532 auto const batchFee = batch::calcBatchFee(env, 0, 2);
533 env(batch::outer(alice, seq, batchFee, tfAllOrNothing),
534 batch::Inner(pay(alice, bob, XRP(1)), 0, seq + 1),
535 batch::Inner(pay(alice, bob, XRP(2)), seq + 1),
537 env.close();
538 }
539
540 // DEFENSIVE: temARRAY_TOO_LARGE: Batch: signers array exceeds 8
541 // entries.
542 // ACTUAL: telENV_RPC_FAILED: isRawTransactionOkay()
543 {
544 auto const seq = env.seq(alice);
545 auto const batchFee = batch::calcBatchFee(env, 9, 2);
546 env(batch::outer(alice, seq, batchFee, tfAllOrNothing),
547 batch::Inner(pay(alice, bob, XRP(10)), seq + 1),
548 batch::Inner(pay(alice, bob, XRP(5)), seq + 2),
549 batch::Sig(bob, carol, alice, bob, carol, alice, bob, carol, alice, alice),
551 env.close();
552 }
553
554 // temBAD_SIGNER: Batch: signer cannot be the outer account
555 {
556 auto const seq = env.seq(alice);
557 auto const batchFee = batch::calcBatchFee(env, 2, 2);
558 env(batch::outer(alice, seq, batchFee, tfAllOrNothing),
559 batch::Inner(pay(alice, bob, XRP(10)), seq + 1),
560 batch::Inner(pay(bob, alice, XRP(5)), env.seq(bob)),
561 batch::Sig(alice, bob),
563 env.close();
564 }
565
566 // temREDUNDANT: Batch: duplicate signer found
567 {
568 auto const seq = env.seq(alice);
569 auto const batchFee = batch::calcBatchFee(env, 2, 2);
570 env(batch::outer(alice, seq, batchFee, tfAllOrNothing),
571 batch::Inner(pay(alice, bob, XRP(10)), seq + 1),
572 batch::Inner(pay(bob, alice, XRP(5)), env.seq(bob)),
573 batch::Sig(bob, bob),
575 env.close();
576 }
577
578 // temBAD_SIGNER: Batch: no account signature for inner txn.
579 // Note: Extra signature by bob
580 {
581 auto const seq = env.seq(alice);
582 auto const batchFee = batch::calcBatchFee(env, 1, 2);
583 env(batch::outer(alice, seq, batchFee, tfAllOrNothing),
584 batch::Inner(pay(alice, bob, XRP(10)), seq + 1),
585 batch::Inner(pay(alice, bob, XRP(5)), seq + 2),
586 batch::Sig(bob),
588 env.close();
589 }
590
591 // temBAD_SIGNER: Batch: no account signature for inner txn.
592 {
593 auto const seq = env.seq(alice);
594 auto const batchFee = batch::calcBatchFee(env, 1, 2);
595 env(batch::outer(alice, seq, batchFee, tfAllOrNothing),
596 batch::Inner(pay(alice, bob, XRP(10)), seq + 1),
597 batch::Inner(pay(bob, alice, XRP(5)), env.seq(bob)),
598 batch::Sig(carol),
600 env.close();
601 }
602
603 // temBAD_SIGNATURE: Batch: invalid batch txn signature.
604 {
605 auto const seq = env.seq(alice);
606 auto const bobSeq = env.seq(bob);
607 auto const batchFee = batch::calcBatchFee(env, 1, 2);
608 auto jt = env.jtnofill(
609 batch::outer(alice, env.seq(alice), batchFee, tfAllOrNothing),
610 batch::Inner(pay(alice, bob, XRP(10)), seq + 1),
611 batch::Inner(pay(bob, alice, XRP(5)), bobSeq));
612
613 Serializer msg;
614 serializeBatch(msg, tfAllOrNothing, jt.stx->getBatchTransactionIDs());
615 auto const sig = xrpl::sign(bob.pk(), bob.sk(), msg.slice());
616 jt.jv[sfBatchSigners.jsonName][0u][sfBatchSigner.jsonName][sfAccount.jsonName] =
617 bob.human();
618 jt.jv[sfBatchSigners.jsonName][0u][sfBatchSigner.jsonName][sfSigningPubKey.jsonName] =
619 strHex(alice.pk());
620 jt.jv[sfBatchSigners.jsonName][0u][sfBatchSigner.jsonName][sfTxnSignature.jsonName] =
621 strHex(Slice{sig.data(), sig.size()});
622
623 env(jt.jv, Ter(temBAD_SIGNATURE));
624 env.close();
625 }
626
627 // temBAD_SIGNER: Batch: invalid batch signers.
628 {
629 auto const seq = env.seq(alice);
630 auto const batchFee = batch::calcBatchFee(env, 2, 2);
631 env(batch::outer(alice, seq, batchFee, tfAllOrNothing),
632 batch::Inner(pay(alice, bob, XRP(10)), seq + 1),
633 batch::Inner(pay(bob, alice, XRP(5)), env.seq(bob)),
634 batch::Inner(pay(carol, alice, XRP(5)), env.seq(carol)),
635 batch::Sig(bob),
637 env.close();
638 }
639 }
640
641 void
643 {
644 testcase("preclaim");
645
646 using namespace test::jtx;
647 using namespace std::literals;
648
649 //----------------------------------------------------------------------
650 // preclaim
651
652 test::jtx::Env env{*this, features};
653
654 auto const alice = Account("alice");
655 auto const bob = Account("bob");
656 auto const carol = Account("carol");
657 auto const dave = Account("dave");
658 auto const elsa = Account("elsa");
659 auto const frank = Account("frank");
660 auto const phantom = Account("phantom");
661 env.memoize(phantom);
662
663 env.fund(XRP(10000), alice, bob, carol, dave, elsa, frank);
664 env.close();
665
666 //----------------------------------------------------------------------
667 // checkSign.checkSingleSign
668
669 // tefBAD_AUTH: Bob is not authorized to sign for Alice
670 {
671 auto const seq = env.seq(alice);
672 auto const batchFee = batch::calcBatchFee(env, 3, 2);
673 env(batch::outer(alice, seq, batchFee, tfAllOrNothing),
674 batch::Inner(pay(alice, bob, XRP(10)), seq + 1),
675 batch::Inner(pay(alice, bob, XRP(20)), seq + 2),
676 Sig(bob),
678 env.close();
679 }
680
681 //----------------------------------------------------------------------
682 // checkBatchSign.checkMultiSign
683
684 // tefNOT_MULTI_SIGNING: SignersList not enabled
685 {
686 auto const seq = env.seq(alice);
687 auto const batchFee = batch::calcBatchFee(env, 3, 2);
688 env(batch::outer(alice, seq, batchFee, tfAllOrNothing),
689 batch::Inner(pay(alice, bob, XRP(10)), seq + 1),
690 batch::Inner(pay(bob, alice, XRP(5)), env.seq(bob)),
691 batch::Msig(bob, {dave, carol}),
693 env.close();
694 }
695
696 env(signers(alice, 2, {{bob, 1}, {carol, 1}}));
697 env.close();
698
699 env(signers(bob, 2, {{carol, 1}, {dave, 1}, {elsa, 1}}));
700 env.close();
701
702 // tefBAD_SIGNATURE: Account not in SignersList
703 {
704 auto const seq = env.seq(alice);
705 auto const batchFee = batch::calcBatchFee(env, 3, 2);
706 env(batch::outer(alice, seq, batchFee, tfAllOrNothing),
707 batch::Inner(pay(alice, bob, XRP(10)), seq + 1),
708 batch::Inner(pay(bob, alice, XRP(5)), env.seq(bob)),
709 batch::Msig(bob, {carol, frank}),
711 env.close();
712 }
713
714 // tefBAD_SIGNATURE: Wrong publicKey type
715 {
716 auto const seq = env.seq(alice);
717 auto const batchFee = batch::calcBatchFee(env, 3, 2);
718 env(batch::outer(alice, seq, batchFee, tfAllOrNothing),
719 batch::Inner(pay(alice, bob, XRP(10)), seq + 1),
720 batch::Inner(pay(bob, alice, XRP(5)), env.seq(bob)),
721 batch::Msig(bob, {carol, Account("dave", KeyType::Ed25519)}),
723 env.close();
724 }
725
726 // tefMASTER_DISABLED: Master key disabled
727 {
728 env(regkey(elsa, frank));
729 env(fset(elsa, asfDisableMaster), Sig(elsa));
730 auto const seq = env.seq(alice);
731 auto const batchFee = batch::calcBatchFee(env, 3, 2);
732 env(batch::outer(alice, seq, batchFee, tfAllOrNothing),
733 batch::Inner(pay(alice, bob, XRP(10)), seq + 1),
734 batch::Inner(pay(bob, alice, XRP(5)), env.seq(bob)),
735 batch::Msig(bob, {carol, elsa}),
737 env.close();
738 }
739
740 // tefBAD_SIGNATURE: Signer does not exist
741 {
742 auto const seq = env.seq(alice);
743 auto const batchFee = batch::calcBatchFee(env, 3, 2);
744 env(batch::outer(alice, seq, batchFee, tfAllOrNothing),
745 batch::Inner(pay(alice, bob, XRP(10)), seq + 1),
746 batch::Inner(pay(bob, alice, XRP(5)), env.seq(bob)),
747 batch::Msig(bob, {carol, phantom}),
749 env.close();
750 }
751
752 // tefBAD_SIGNATURE: Signer has not enabled RegularKey
753 {
754 auto const seq = env.seq(alice);
755 auto const batchFee = batch::calcBatchFee(env, 3, 2);
756 Account const davo{"davo", KeyType::Ed25519};
757 env(batch::outer(alice, seq, batchFee, tfAllOrNothing),
758 batch::Inner(pay(alice, bob, XRP(10)), seq + 1),
759 batch::Inner(pay(bob, alice, XRP(5)), env.seq(bob)),
760 batch::Msig(bob, {carol, Reg{dave, davo}}),
762 env.close();
763 }
764
765 // tefBAD_SIGNATURE: Wrong RegularKey Set
766 {
767 env(regkey(dave, frank));
768 auto const seq = env.seq(alice);
769 auto const batchFee = batch::calcBatchFee(env, 3, 2);
770 Account const davo{"davo", KeyType::Ed25519};
771 env(batch::outer(alice, seq, batchFee, tfAllOrNothing),
772 batch::Inner(pay(alice, bob, XRP(10)), seq + 1),
773 batch::Inner(pay(bob, alice, XRP(5)), env.seq(bob)),
774 batch::Msig(bob, {carol, Reg{dave, davo}}),
775 Ter(tefBAD_SIGNATURE));
776 env.close();
777 }
778
779 // tefBAD_QUORUM
780 {
781 auto const seq = env.seq(alice);
782 auto const batchFee = batch::calcBatchFee(env, 2, 2);
783 env(batch::outer(alice, seq, batchFee, tfAllOrNothing),
784 batch::Inner(pay(alice, bob, XRP(10)), seq + 1),
785 batch::Inner(pay(bob, alice, XRP(5)), env.seq(bob)),
786 batch::Msig(bob, {carol}),
788 env.close();
789 }
790
791 // tesSUCCESS: BatchSigners.Signers
792 {
793 auto const seq = env.seq(alice);
794 auto const batchFee = batch::calcBatchFee(env, 3, 2);
795 env(batch::outer(alice, seq, batchFee, tfAllOrNothing),
796 batch::Inner(pay(alice, bob, XRP(10)), seq + 1),
797 batch::Inner(pay(bob, alice, XRP(5)), env.seq(bob)),
798 batch::Msig(bob, {carol, dave}),
799 Ter(tesSUCCESS));
800 env.close();
801 }
802
803 // tesSUCCESS: Multisign + BatchSigners.Signers
804 {
805 auto const seq = env.seq(alice);
806 auto const batchFee = batch::calcBatchFee(env, 4, 2);
807 env(batch::outer(alice, seq, batchFee, tfAllOrNothing),
808 batch::Inner(pay(alice, bob, XRP(10)), seq + 1),
809 batch::Inner(pay(bob, alice, XRP(5)), env.seq(bob)),
810 batch::Msig(bob, {carol, dave}),
811 Msig(bob, carol),
812 Ter(tesSUCCESS));
813 env.close();
814 }
815
816 //----------------------------------------------------------------------
817 // checkBatchSign.checkSingleSign
818
819 // tefBAD_AUTH: Inner Account is not signer
820 {
821 auto const ledSeq = env.current()->seq();
822 auto const seq = env.seq(alice);
823 auto const batchFee = batch::calcBatchFee(env, 1, 2);
824 env(batch::outer(alice, seq, batchFee, tfAllOrNothing),
825 batch::Inner(pay(alice, phantom, XRP(1000)), seq + 1),
826 batch::Inner(noop(phantom), ledSeq),
827 batch::Sig(Reg{phantom, carol}),
829 env.close();
830 }
831
832 // tefBAD_AUTH: Account is not signer
833 {
834 auto const ledSeq = env.current()->seq();
835 auto const seq = env.seq(alice);
836 auto const batchFee = batch::calcBatchFee(env, 1, 2);
837 env(batch::outer(alice, seq, batchFee, tfAllOrNothing),
838 batch::Inner(pay(alice, bob, XRP(1000)), seq + 1),
839 batch::Inner(noop(bob), ledSeq),
840 batch::Sig(Reg{bob, carol}),
842 env.close();
843 }
844
845 // tesSUCCESS: Signed With Regular Key
846 {
847 env(regkey(bob, carol));
848 auto const seq = env.seq(alice);
849 auto const batchFee = batch::calcBatchFee(env, 1, 2);
850 env(batch::outer(alice, seq, batchFee, tfAllOrNothing),
851 batch::Inner(pay(alice, bob, XRP(1)), seq + 1),
852 batch::Inner(pay(bob, alice, XRP(2)), env.seq(bob)),
853 batch::Sig(Reg{bob, carol}),
854 Ter(tesSUCCESS));
855 env.close();
856 }
857
858 // tesSUCCESS: Signed With Master Key
859 {
860 auto const seq = env.seq(alice);
861 auto const batchFee = batch::calcBatchFee(env, 1, 2);
862 env(batch::outer(alice, seq, batchFee, tfAllOrNothing),
863 batch::Inner(pay(alice, bob, XRP(1)), seq + 1),
864 batch::Inner(pay(bob, alice, XRP(2)), env.seq(bob)),
865 batch::Sig(bob),
866 Ter(tesSUCCESS));
867 env.close();
868 }
869
870 // tefMASTER_DISABLED: Signed With Master Key Disabled
871 {
872 env(regkey(bob, carol));
873 env(fset(bob, asfDisableMaster), Sig(bob));
874 auto const seq = env.seq(alice);
875 auto const batchFee = batch::calcBatchFee(env, 1, 2);
876 env(batch::outer(alice, seq, batchFee, tfAllOrNothing),
877 batch::Inner(pay(alice, bob, XRP(1)), seq + 1),
878 batch::Inner(pay(bob, alice, XRP(2)), env.seq(bob)),
879 batch::Sig(bob),
881 env.close();
882 }
883 }
884
885 void
887 {
888 testcase("bad raw txn");
889
890 using namespace test::jtx;
891 using namespace std::literals;
892
893 test::jtx::Env env{*this, features};
894
895 auto const alice = Account("alice");
896 auto const bob = Account("bob");
897
898 env.fund(XRP(10000), alice, bob);
899
900 // Invalid: sfTransactionType
901 {
902 auto const batchFee = batch::calcBatchFee(env, 1, 2);
903 auto const seq = env.seq(alice);
904 auto tx1 = batch::Inner(pay(alice, bob, XRP(10)), seq + 1);
905 tx1.removeMember(jss::TransactionType);
906 auto jt = env.jtnofill(
907 batch::outer(alice, seq, batchFee, tfAllOrNothing),
908 tx1,
909 batch::Inner(pay(alice, bob, XRP(10)), seq + 2));
910
911 env(jt.jv, batch::Sig(bob), Ter(telENV_RPC_FAILED));
912 env.close();
913 }
914
915 // Invalid: sfAccount
916 {
917 auto const batchFee = batch::calcBatchFee(env, 1, 2);
918 auto const seq = env.seq(alice);
919 auto tx1 = batch::Inner(pay(alice, bob, XRP(10)), seq + 1);
920 tx1.removeMember(jss::Account);
921 auto jt = env.jtnofill(
922 batch::outer(alice, seq, batchFee, tfAllOrNothing),
923 tx1,
924 batch::Inner(pay(alice, bob, XRP(10)), seq + 2));
925
926 env(jt.jv, batch::Sig(bob), Ter(telENV_RPC_FAILED));
927 env.close();
928 }
929
930 // Invalid: sfSequence
931 {
932 auto const batchFee = batch::calcBatchFee(env, 1, 2);
933 auto const seq = env.seq(alice);
934 auto tx1 = batch::Inner(pay(alice, bob, XRP(10)), seq + 1);
935 tx1.removeMember(jss::Sequence);
936 auto jt = env.jtnofill(
937 batch::outer(alice, seq, batchFee, tfAllOrNothing),
938 tx1,
939 batch::Inner(pay(alice, bob, XRP(10)), seq + 2));
940
941 env(jt.jv, batch::Sig(bob), Ter(telENV_RPC_FAILED));
942 env.close();
943 }
944
945 // Invalid: sfFee
946 {
947 auto const batchFee = batch::calcBatchFee(env, 1, 2);
948 auto const seq = env.seq(alice);
949 auto tx1 = batch::Inner(pay(alice, bob, XRP(10)), seq + 1);
950 tx1.removeMember(jss::Fee);
951 auto jt = env.jtnofill(
952 batch::outer(alice, seq, batchFee, tfAllOrNothing),
953 tx1,
954 batch::Inner(pay(alice, bob, XRP(10)), seq + 2));
955
956 env(jt.jv, batch::Sig(bob), Ter(telENV_RPC_FAILED));
957 env.close();
958 }
959
960 // Invalid: sfSigningPubKey
961 {
962 auto const batchFee = batch::calcBatchFee(env, 1, 2);
963 auto const seq = env.seq(alice);
964 auto tx1 = batch::Inner(pay(alice, bob, XRP(10)), seq + 1);
965 tx1.removeMember(jss::SigningPubKey);
966 auto jt = env.jtnofill(
967 batch::outer(alice, seq, batchFee, tfAllOrNothing),
968 tx1,
969 batch::Inner(pay(alice, bob, XRP(10)), seq + 2));
970
971 env(jt.jv, batch::Sig(bob), Ter(telENV_RPC_FAILED));
972 env.close();
973 }
974 }
975
976 void
978 {
979 testcase("bad sequence");
980
981 using namespace test::jtx;
982 using namespace std::literals;
983
984 test::jtx::Env env{*this, features};
985
986 auto const alice = Account("alice");
987 auto const bob = Account("bob");
988 auto const gw = Account("gw");
989 auto const usd = gw["USD"];
990
991 env.fund(XRP(10000), alice, bob, gw);
992 env.close();
993 env.trust(usd(1000), alice, bob);
994 env(pay(gw, alice, usd(100)));
995 env(pay(gw, bob, usd(100)));
996 env.close();
997
998 env(noop(bob), Ter(tesSUCCESS));
999 env.close();
1000
1001 // Invalid: Alice Sequence is a past sequence
1002 {
1003 auto const preAliceSeq = env.seq(alice);
1004 auto const preAlice = env.balance(alice);
1005 auto const preAliceUSD = env.balance(alice, usd.issue());
1006 auto const preBobSeq = env.seq(bob);
1007 auto const preBob = env.balance(bob);
1008 auto const preBobUSD = env.balance(bob, usd.issue());
1009
1010 auto const batchFee = batch::calcBatchFee(env, 1, 2);
1011 auto const [txIDs, batchID] = submitBatch(
1012 env,
1013 tesSUCCESS,
1014 batch::outer(alice, preAliceSeq, batchFee, tfAllOrNothing),
1015 batch::Inner(pay(alice, bob, XRP(10)), preAliceSeq - 10),
1016 batch::Inner(pay(bob, alice, XRP(5)), preBobSeq),
1017 batch::Sig(bob));
1018
1019 env.close();
1020 {
1021 std::vector<TestLedgerData> const testCases = {
1022 {.index = 0,
1023 .txType = "Batch",
1024 .result = "tesSUCCESS",
1025 .txHash = batchID,
1026 .batchID = std::nullopt},
1027 };
1028 validateClosedLedger(env, testCases);
1029 }
1030
1031 env.close();
1032 {
1033 // next ledger is empty
1034 std::vector<TestLedgerData> const testCases = {};
1035 validateClosedLedger(env, testCases);
1036 }
1037
1038 // Alice pays fee & Bob should not be affected.
1039 BEAST_EXPECT(env.seq(alice) == preAliceSeq + 1);
1040 BEAST_EXPECT(env.balance(alice) == preAlice - batchFee);
1041 BEAST_EXPECT(env.balance(alice, usd.issue()) == preAliceUSD);
1042 BEAST_EXPECT(env.seq(bob) == preBobSeq);
1043 BEAST_EXPECT(env.balance(bob) == preBob);
1044 BEAST_EXPECT(env.balance(bob, usd.issue()) == preBobUSD);
1045 }
1046
1047 // Invalid: Alice Sequence is a future sequence
1048 {
1049 auto const preAliceSeq = env.seq(alice);
1050 auto const preAlice = env.balance(alice);
1051 auto const preAliceUSD = env.balance(alice, usd.issue());
1052 auto const preBobSeq = env.seq(bob);
1053 auto const preBob = env.balance(bob);
1054 auto const preBobUSD = env.balance(bob, usd.issue());
1055
1056 auto const batchFee = batch::calcBatchFee(env, 1, 2);
1057 auto const [txIDs, batchID] = submitBatch(
1058 env,
1059 tesSUCCESS,
1060 batch::outer(alice, preAliceSeq, batchFee, tfAllOrNothing),
1061 batch::Inner(pay(alice, bob, XRP(10)), preAliceSeq + 10),
1062 batch::Inner(pay(bob, alice, XRP(5)), preBobSeq),
1063 batch::Sig(bob));
1064
1065 env.close();
1066 {
1067 std::vector<TestLedgerData> const testCases = {
1068 {.index = 0,
1069 .txType = "Batch",
1070 .result = "tesSUCCESS",
1071 .txHash = batchID,
1072 .batchID = std::nullopt},
1073 };
1074 validateClosedLedger(env, testCases);
1075 }
1076
1077 env.close();
1078 {
1079 // next ledger is empty
1080 std::vector<TestLedgerData> const testCases = {};
1081 validateClosedLedger(env, testCases);
1082 }
1083
1084 // Alice pays fee & Bob should not be affected.
1085 BEAST_EXPECT(env.seq(alice) == preAliceSeq + 1);
1086 BEAST_EXPECT(env.balance(alice) == preAlice - batchFee);
1087 BEAST_EXPECT(env.balance(alice, usd.issue()) == preAliceUSD);
1088 BEAST_EXPECT(env.seq(bob) == preBobSeq);
1089 BEAST_EXPECT(env.balance(bob) == preBob);
1090 BEAST_EXPECT(env.balance(bob, usd.issue()) == preBobUSD);
1091 }
1092
1093 // Invalid: Bob Sequence is a past sequence
1094 {
1095 auto const preAliceSeq = env.seq(alice);
1096 auto const preAlice = env.balance(alice);
1097 auto const preAliceUSD = env.balance(alice, usd.issue());
1098 auto const preBobSeq = env.seq(bob);
1099 auto const preBob = env.balance(bob);
1100 auto const preBobUSD = env.balance(bob, usd.issue());
1101
1102 auto const batchFee = batch::calcBatchFee(env, 1, 2);
1103 auto const [txIDs, batchID] = submitBatch(
1104 env,
1105 tesSUCCESS,
1106 batch::outer(alice, preAliceSeq, batchFee, tfAllOrNothing),
1107 batch::Inner(pay(alice, bob, XRP(10)), preAliceSeq + 1),
1108 batch::Inner(pay(bob, alice, XRP(5)), preBobSeq - 10),
1109 batch::Sig(bob));
1110
1111 env.close();
1112 {
1113 std::vector<TestLedgerData> const testCases = {
1114 {.index = 0,
1115 .txType = "Batch",
1116 .result = "tesSUCCESS",
1117 .txHash = batchID,
1118 .batchID = std::nullopt},
1119 };
1120 validateClosedLedger(env, testCases);
1121 }
1122
1123 env.close();
1124 {
1125 // next ledger is empty
1126 std::vector<TestLedgerData> const testCases = {};
1127 validateClosedLedger(env, testCases);
1128 }
1129
1130 // Alice pays fee & Bob should not be affected.
1131 BEAST_EXPECT(env.seq(alice) == preAliceSeq + 1);
1132 BEAST_EXPECT(env.balance(alice) == preAlice - batchFee);
1133 BEAST_EXPECT(env.balance(alice, usd.issue()) == preAliceUSD);
1134 BEAST_EXPECT(env.seq(bob) == preBobSeq);
1135 BEAST_EXPECT(env.balance(bob) == preBob);
1136 BEAST_EXPECT(env.balance(bob, usd.issue()) == preBobUSD);
1137 }
1138
1139 // Invalid: Bob Sequence is a future sequence
1140 {
1141 auto const preAliceSeq = env.seq(alice);
1142 auto const preAlice = env.balance(alice);
1143 auto const preAliceUSD = env.balance(alice, usd.issue());
1144 auto const preBobSeq = env.seq(bob);
1145 auto const preBob = env.balance(bob);
1146 auto const preBobUSD = env.balance(bob, usd.issue());
1147
1148 auto const batchFee = batch::calcBatchFee(env, 1, 2);
1149 auto const [txIDs, batchID] = submitBatch(
1150 env,
1151 tesSUCCESS,
1152 batch::outer(alice, preAliceSeq, batchFee, tfAllOrNothing),
1153 batch::Inner(pay(alice, bob, XRP(10)), preAliceSeq + 1),
1154 batch::Inner(pay(bob, alice, XRP(5)), preBobSeq + 10),
1155 batch::Sig(bob));
1156
1157 env.close();
1158 {
1159 std::vector<TestLedgerData> const testCases = {
1160 {.index = 0,
1161 .txType = "Batch",
1162 .result = "tesSUCCESS",
1163 .txHash = batchID,
1164 .batchID = std::nullopt},
1165 };
1166 validateClosedLedger(env, testCases);
1167 }
1168
1169 env.close();
1170 {
1171 // next ledger is empty
1172 std::vector<TestLedgerData> const testCases = {};
1173 validateClosedLedger(env, testCases);
1174 }
1175
1176 // Alice pays fee & Bob should not be affected.
1177 BEAST_EXPECT(env.seq(alice) == preAliceSeq + 1);
1178 BEAST_EXPECT(env.balance(alice) == preAlice - batchFee);
1179 BEAST_EXPECT(env.balance(alice, usd.issue()) == preAliceUSD);
1180 BEAST_EXPECT(env.seq(bob) == preBobSeq);
1181 BEAST_EXPECT(env.balance(bob) == preBob);
1182 BEAST_EXPECT(env.balance(bob, usd.issue()) == preBobUSD);
1183 }
1184
1185 // Invalid: Outer and Inner Sequence are the same
1186 {
1187 auto const preAliceSeq = env.seq(alice);
1188 auto const preAlice = env.balance(alice);
1189 auto const preAliceUSD = env.balance(alice, usd.issue());
1190 auto const preBobSeq = env.seq(bob);
1191 auto const preBob = env.balance(bob);
1192 auto const preBobUSD = env.balance(bob, usd.issue());
1193
1194 auto const batchFee = batch::calcBatchFee(env, 1, 2);
1195 auto const [txIDs, batchID] = submitBatch(
1196 env,
1197 tesSUCCESS,
1198 batch::outer(alice, preAliceSeq, batchFee, tfAllOrNothing),
1199 batch::Inner(pay(alice, bob, XRP(10)), preAliceSeq),
1200 batch::Inner(pay(bob, alice, XRP(5)), preBobSeq),
1201 batch::Sig(bob));
1202
1203 env.close();
1204 {
1205 std::vector<TestLedgerData> const testCases = {
1206 {.index = 0,
1207 .txType = "Batch",
1208 .result = "tesSUCCESS",
1209 .txHash = batchID,
1210 .batchID = std::nullopt},
1211 };
1212 validateClosedLedger(env, testCases);
1213 }
1214
1215 env.close();
1216 {
1217 // next ledger is empty
1218 std::vector<TestLedgerData> const testCases = {};
1219 validateClosedLedger(env, testCases);
1220 }
1221
1222 // Alice pays fee & Bob should not be affected.
1223 BEAST_EXPECT(env.seq(alice) == preAliceSeq + 1);
1224 BEAST_EXPECT(env.balance(alice) == preAlice - batchFee);
1225 BEAST_EXPECT(env.balance(alice, usd.issue()) == preAliceUSD);
1226 BEAST_EXPECT(env.seq(bob) == preBobSeq);
1227 BEAST_EXPECT(env.balance(bob) == preBob);
1228 BEAST_EXPECT(env.balance(bob, usd.issue()) == preBobUSD);
1229 }
1230 }
1231
1232 void
1234 {
1235 testcase("bad outer fee");
1236
1237 using namespace test::jtx;
1238 using namespace std::literals;
1239
1240 // Bad Fee Without Signer
1241 {
1242 test::jtx::Env env{*this, features};
1243
1244 auto const alice = Account("alice");
1245 auto const bob = Account("bob");
1246 env.fund(XRP(10000), alice, bob);
1247 env.close();
1248
1249 env(noop(bob), Ter(tesSUCCESS));
1250 env.close();
1251
1252 // Bad Fee: Should be batch::calcBatchFee(env, 0, 2)
1253 auto const batchFee = batch::calcBatchFee(env, 0, 1);
1254 auto const aliceSeq = env.seq(alice);
1255 env(batch::outer(alice, aliceSeq, batchFee, tfAllOrNothing),
1256 batch::Inner(pay(alice, bob, XRP(10)), aliceSeq + 1),
1257 batch::Inner(pay(alice, bob, XRP(15)), aliceSeq + 2),
1259 env.close();
1260 }
1261
1262 // Bad Fee With MultiSign
1263 {
1264 test::jtx::Env env{*this, features};
1265
1266 auto const alice = Account("alice");
1267 auto const bob = Account("bob");
1268 auto const carol = Account("carol");
1269 env.fund(XRP(10000), alice, bob, carol);
1270 env.close();
1271
1272 env(noop(bob), Ter(tesSUCCESS));
1273 env.close();
1274
1275 env(signers(alice, 2, {{bob, 1}, {carol, 1}}));
1276 env.close();
1277
1278 // Bad Fee: Should be batch::calcBatchFee(env, 2, 2)
1279 auto const batchFee = batch::calcBatchFee(env, 1, 2);
1280 auto const aliceSeq = env.seq(alice);
1281 env(batch::outer(alice, aliceSeq, batchFee, tfAllOrNothing),
1282 batch::Inner(pay(alice, bob, XRP(10)), aliceSeq + 1),
1283 batch::Inner(pay(alice, bob, XRP(15)), aliceSeq + 2),
1284 Msig(bob, carol),
1286 env.close();
1287 }
1288
1289 // Bad Fee With MultiSign + BatchSigners
1290 {
1291 test::jtx::Env env{*this, features};
1292
1293 auto const alice = Account("alice");
1294 auto const bob = Account("bob");
1295 auto const carol = Account("carol");
1296 env.fund(XRP(10000), alice, bob, carol);
1297 env.close();
1298
1299 env(noop(bob), Ter(tesSUCCESS));
1300 env.close();
1301
1302 env(signers(alice, 2, {{bob, 1}, {carol, 1}}));
1303 env.close();
1304
1305 // Bad Fee: Should be batch::calcBatchFee(env, 3, 2)
1306 auto const batchFee = batch::calcBatchFee(env, 2, 2);
1307 auto const aliceSeq = env.seq(alice);
1308 auto const bobSeq = env.seq(bob);
1309 env(batch::outer(alice, aliceSeq, batchFee, tfAllOrNothing),
1310 batch::Inner(pay(alice, bob, XRP(10)), aliceSeq + 1),
1311 batch::Inner(pay(bob, alice, XRP(5)), bobSeq),
1312 batch::Sig(bob),
1313 Msig(bob, carol),
1315 env.close();
1316 }
1317
1318 // Bad Fee With MultiSign + BatchSigners.Signers
1319 {
1320 test::jtx::Env env{*this, features};
1321
1322 auto const alice = Account("alice");
1323 auto const bob = Account("bob");
1324 auto const carol = Account("carol");
1325 env.fund(XRP(10000), alice, bob, carol);
1326 env.close();
1327
1328 env(noop(bob), Ter(tesSUCCESS));
1329 env.close();
1330
1331 env(signers(alice, 2, {{bob, 1}, {carol, 1}}));
1332 env.close();
1333
1334 env(signers(bob, 2, {{alice, 1}, {carol, 1}}));
1335 env.close();
1336
1337 // Bad Fee: Should be batch::calcBatchFee(env, 4, 2)
1338 auto const batchFee = batch::calcBatchFee(env, 3, 2);
1339 auto const aliceSeq = env.seq(alice);
1340 auto const bobSeq = env.seq(bob);
1341 env(batch::outer(alice, aliceSeq, batchFee, tfAllOrNothing),
1342 batch::Inner(pay(alice, bob, XRP(10)), aliceSeq + 1),
1343 batch::Inner(pay(bob, alice, XRP(5)), bobSeq),
1344 batch::Msig(bob, {alice, carol}),
1345 Msig(bob, carol),
1347 env.close();
1348 }
1349
1350 // Bad Fee With BatchSigners
1351 {
1352 test::jtx::Env env{*this, features};
1353
1354 auto const alice = Account("alice");
1355 auto const bob = Account("bob");
1356 env.fund(XRP(10000), alice, bob);
1357 env.close();
1358
1359 env(noop(bob), Ter(tesSUCCESS));
1360 env.close();
1361
1362 // Bad Fee: Should be batch::calcBatchFee(env, 1, 2)
1363 auto const batchFee = batch::calcBatchFee(env, 0, 2);
1364 auto const aliceSeq = env.seq(alice);
1365 auto const bobSeq = env.seq(bob);
1366 env(batch::outer(alice, aliceSeq, batchFee, tfAllOrNothing),
1367 batch::Inner(pay(alice, bob, XRP(10)), aliceSeq + 1),
1368 batch::Inner(pay(bob, alice, XRP(5)), bobSeq),
1369 batch::Sig(bob),
1371 env.close();
1372 }
1373
1374 // Bad Fee Dynamic Fee Calculation
1375 {
1376 test::jtx::Env env{*this, features};
1377
1378 auto const alice = Account("alice");
1379 auto const bob = Account("bob");
1380 auto const gw = Account("gw");
1381 auto const usd = gw["USD"];
1382
1383 env.fund(XRP(10000), alice, bob, gw);
1384 env.close();
1385 auto const ammCreate = [&alice](STAmount const& amount, STAmount const& amount2) {
1386 json::Value jv;
1387 jv[jss::Account] = alice.human();
1388 jv[jss::Amount] = amount.getJson(JsonOptions::Values::None);
1389 jv[jss::Amount2] = amount2.getJson(JsonOptions::Values::None);
1390 jv[jss::TradingFee] = 0;
1391 jv[jss::TransactionType] = jss::AMMCreate;
1392 return jv;
1393 };
1394
1395 auto const batchFee = batch::calcBatchFee(env, 0, 2);
1396 auto const seq = env.seq(alice);
1397 env(batch::outer(alice, seq, batchFee, tfAllOrNothing),
1398 batch::Inner(ammCreate(XRP(10), usd(10)), seq + 1),
1399 batch::Inner(pay(alice, bob, XRP(10)), seq + 2),
1401 env.close();
1402 }
1403 }
1404
1405 void
1407 {
1408 testcase("calculate base fee");
1409
1410 using namespace test::jtx;
1411 using namespace std::literals;
1412
1413 // telENV_RPC_FAILED: Batch: txns array exceeds 8 entries.
1414 {
1415 test::jtx::Env env{*this, features};
1416
1417 auto const alice = Account("alice");
1418 auto const bob = Account("bob");
1419 env.fund(XRP(10000), alice, bob);
1420 env.close();
1421
1422 auto const batchFee = batch::calcBatchFee(env, 0, 9);
1423 auto const aliceSeq = env.seq(alice);
1424 env(batch::outer(alice, aliceSeq, batchFee, tfAllOrNothing),
1425 batch::Inner(pay(alice, bob, XRP(1)), aliceSeq),
1426 batch::Inner(pay(alice, bob, XRP(1)), aliceSeq),
1427 batch::Inner(pay(alice, bob, XRP(1)), aliceSeq),
1428 batch::Inner(pay(alice, bob, XRP(1)), aliceSeq),
1429 batch::Inner(pay(alice, bob, XRP(1)), aliceSeq),
1430 batch::Inner(pay(alice, bob, XRP(1)), aliceSeq),
1431 batch::Inner(pay(alice, bob, XRP(1)), aliceSeq),
1432 batch::Inner(pay(alice, bob, XRP(1)), aliceSeq),
1433 batch::Inner(pay(alice, bob, XRP(1)), aliceSeq),
1435 env.close();
1436 }
1437
1438 // temARRAY_TOO_LARGE: Batch: txns array exceeds 8 entries.
1439 {
1440 test::jtx::Env env{*this, features};
1441
1442 auto const alice = Account("alice");
1443 auto const bob = Account("bob");
1444 env.fund(XRP(10000), alice, bob);
1445 env.close();
1446
1447 auto const batchFee = batch::calcBatchFee(env, 0, 9);
1448 auto const aliceSeq = env.seq(alice);
1449 auto jt = env.jtnofill(
1450 batch::outer(alice, aliceSeq, batchFee, tfAllOrNothing),
1451 batch::Inner(pay(alice, bob, XRP(1)), aliceSeq),
1452 batch::Inner(pay(alice, bob, XRP(1)), aliceSeq),
1453 batch::Inner(pay(alice, bob, XRP(1)), aliceSeq),
1454 batch::Inner(pay(alice, bob, XRP(1)), aliceSeq),
1455 batch::Inner(pay(alice, bob, XRP(1)), aliceSeq),
1456 batch::Inner(pay(alice, bob, XRP(1)), aliceSeq),
1457 batch::Inner(pay(alice, bob, XRP(1)), aliceSeq),
1458 batch::Inner(pay(alice, bob, XRP(1)), aliceSeq),
1459 batch::Inner(pay(alice, bob, XRP(1)), aliceSeq));
1460
1461 env.app().getOpenLedger().modify([&](OpenView& view, beast::Journal j) {
1462 auto const result = xrpl::apply(env.app(), view, *jt.stx, TapNone, j);
1463 BEAST_EXPECT(!result.applied && result.ter == temARRAY_TOO_LARGE);
1464 return result.applied;
1465 });
1466 }
1467
1468 // telENV_RPC_FAILED: Batch: signers array exceeds 8 entries.
1469 {
1470 test::jtx::Env env{*this, features};
1471
1472 auto const alice = Account("alice");
1473 auto const bob = Account("bob");
1474 env.fund(XRP(10000), alice, bob);
1475 env.close();
1476
1477 auto const aliceSeq = env.seq(alice);
1478 auto const batchFee = batch::calcBatchFee(env, 9, 2);
1479 env(batch::outer(alice, aliceSeq, batchFee, tfAllOrNothing),
1480 batch::Inner(pay(alice, bob, XRP(10)), aliceSeq + 1),
1481 batch::Inner(pay(alice, bob, XRP(5)), aliceSeq + 2),
1482 batch::Sig(bob, bob, bob, bob, bob, bob, bob, bob, bob, bob),
1484 env.close();
1485 }
1486
1487 // temARRAY_TOO_LARGE: Batch: signers array exceeds 8 entries.
1488 {
1489 test::jtx::Env env{*this, features};
1490
1491 auto const alice = Account("alice");
1492 auto const bob = Account("bob");
1493 env.fund(XRP(10000), alice, bob);
1494 env.close();
1495
1496 auto const batchFee = batch::calcBatchFee(env, 0, 9);
1497 auto const aliceSeq = env.seq(alice);
1498 auto jt = env.jtnofill(
1499 batch::outer(alice, aliceSeq, batchFee, tfAllOrNothing),
1500 batch::Inner(pay(alice, bob, XRP(10)), aliceSeq + 1),
1501 batch::Inner(pay(alice, bob, XRP(5)), aliceSeq + 2),
1502 batch::Sig(bob, bob, bob, bob, bob, bob, bob, bob, bob, bob));
1503
1504 env.app().getOpenLedger().modify([&](OpenView& view, beast::Journal j) {
1505 auto const result = xrpl::apply(env.app(), view, *jt.stx, TapNone, j);
1506 BEAST_EXPECT(!result.applied && result.ter == temARRAY_TOO_LARGE);
1507 return result.applied;
1508 });
1509 }
1510 }
1511
1512 void
1514 {
1515 testcase("all or nothing");
1516
1517 using namespace test::jtx;
1518 using namespace std::literals;
1519
1520 test::jtx::Env env{*this, features};
1521
1522 auto const alice = Account("alice");
1523 auto const bob = Account("bob");
1524 auto const gw = Account("gw");
1525 auto const usd = gw["USD"];
1526 env.fund(XRP(10000), alice, bob, gw);
1527 env.close();
1528
1529 // all
1530 {
1531 auto const preAlice = env.balance(alice);
1532 auto const preBob = env.balance(bob);
1533
1534 auto const batchFee = batch::calcBatchFee(env, 0, 2);
1535 auto const seq = env.seq(alice);
1536 auto const [txIDs, batchID] = submitBatch(
1537 env,
1538 tesSUCCESS,
1539 batch::outer(alice, seq, batchFee, tfAllOrNothing),
1540 batch::Inner(pay(alice, bob, XRP(1)), seq + 1),
1541 batch::Inner(pay(alice, bob, XRP(2)), seq + 2));
1542 env.close();
1543
1544 std::vector<TestLedgerData> const testCases = {
1545 {.index = 0,
1546 .txType = "Batch",
1547 .result = "tesSUCCESS",
1548 .txHash = batchID,
1549 .batchID = std::nullopt},
1550 {.index = 1,
1551 .txType = "Payment",
1552 .result = "tesSUCCESS",
1553 .txHash = txIDs[0],
1554 .batchID = batchID},
1555 {.index = 2,
1556 .txType = "Payment",
1557 .result = "tesSUCCESS",
1558 .txHash = txIDs[1],
1559 .batchID = batchID},
1560 };
1561 validateClosedLedger(env, testCases);
1562
1563 // Alice consumes sequences (# of txns)
1564 BEAST_EXPECT(env.seq(alice) == seq + 3);
1565
1566 // Alice pays XRP & Fee; Bob receives XRP
1567 BEAST_EXPECT(env.balance(alice) == preAlice - XRP(3) - batchFee);
1568 BEAST_EXPECT(env.balance(bob) == preBob + XRP(3));
1569 }
1570
1571 // tec failure
1572 {
1573 auto const preAlice = env.balance(alice);
1574 auto const preBob = env.balance(bob);
1575
1576 auto const batchFee = batch::calcBatchFee(env, 0, 2);
1577 auto const seq = env.seq(alice);
1578
1579 auto const [txIDs, batchID] = submitBatch(
1580 env,
1581 tesSUCCESS,
1582 batch::outer(alice, seq, batchFee, tfAllOrNothing),
1583 batch::Inner(pay(alice, bob, XRP(1)), seq + 1),
1584 // tecUNFUNDED_PAYMENT: alice does not have enough XRP
1585 batch::Inner(pay(alice, bob, XRP(9999)), seq + 2));
1586 env.close();
1587
1588 std::vector<TestLedgerData> const testCases = {
1589 {.index = 0,
1590 .txType = "Batch",
1591 .result = "tesSUCCESS",
1592 .txHash = batchID,
1593 .batchID = std::nullopt},
1594 };
1595 validateClosedLedger(env, testCases);
1596
1597 // Alice consumes sequence
1598 BEAST_EXPECT(env.seq(alice) == seq + 1);
1599
1600 // Alice pays Fee; Bob should not be affected
1601 BEAST_EXPECT(env.balance(alice) == preAlice - batchFee);
1602 BEAST_EXPECT(env.balance(bob) == preBob);
1603 }
1604
1605 // tef failure
1606 {
1607 auto const preAlice = env.balance(alice);
1608 auto const preBob = env.balance(bob);
1609
1610 auto const batchFee = batch::calcBatchFee(env, 0, 2);
1611 auto const seq = env.seq(alice);
1612 auto const [txIDs, batchID] = submitBatch(
1613 env,
1614 tesSUCCESS,
1615 batch::outer(alice, seq, batchFee, tfAllOrNothing),
1616 batch::Inner(pay(alice, bob, XRP(1)), seq + 1),
1617 // tefNO_AUTH_REQUIRED: trustline auth is not required
1618 batch::Inner(trust(alice, usd(1000), tfSetfAuth), seq + 2));
1619 env.close();
1620
1621 std::vector<TestLedgerData> const testCases = {
1622 {.index = 0,
1623 .txType = "Batch",
1624 .result = "tesSUCCESS",
1625 .txHash = batchID,
1626 .batchID = std::nullopt},
1627 };
1628 validateClosedLedger(env, testCases);
1629
1630 // Alice consumes sequence
1631 BEAST_EXPECT(env.seq(alice) == seq + 1);
1632
1633 // Alice pays Fee; Bob should not be affected
1634 BEAST_EXPECT(env.balance(alice) == preAlice - batchFee);
1635 BEAST_EXPECT(env.balance(bob) == preBob);
1636 }
1637
1638 // ter failure
1639 {
1640 auto const preAlice = env.balance(alice);
1641 auto const preBob = env.balance(bob);
1642
1643 auto const batchFee = batch::calcBatchFee(env, 0, 2);
1644 auto const seq = env.seq(alice);
1645 auto const [txIDs, batchID] = submitBatch(
1646 env,
1647 tesSUCCESS,
1648 batch::outer(alice, seq, batchFee, tfAllOrNothing),
1649 batch::Inner(pay(alice, bob, XRP(1)), seq + 1),
1650 // terPRE_TICKET: ticket does not exist
1651 batch::Inner(trust(alice, usd(1000), tfSetfAuth), 0, seq + 2));
1652 env.close();
1653
1654 std::vector<TestLedgerData> const testCases = {
1655 {.index = 0,
1656 .txType = "Batch",
1657 .result = "tesSUCCESS",
1658 .txHash = batchID,
1659 .batchID = std::nullopt},
1660 };
1661 validateClosedLedger(env, testCases);
1662
1663 // Alice consumes sequence
1664 BEAST_EXPECT(env.seq(alice) == seq + 1);
1665
1666 // Alice pays Fee; Bob should not be affected
1667 BEAST_EXPECT(env.balance(alice) == preAlice - batchFee);
1668 BEAST_EXPECT(env.balance(bob) == preBob);
1669 }
1670 }
1671
1672 void
1674 {
1675 testcase("only one");
1676
1677 using namespace test::jtx;
1678 using namespace std::literals;
1679
1680 test::jtx::Env env{*this, features};
1681
1682 auto const alice = Account("alice");
1683 auto const bob = Account("bob");
1684 auto const carol = Account("carol");
1685 auto const dave = Account("dave");
1686 auto const gw = Account("gw");
1687 auto const usd = gw["USD"];
1688 env.fund(XRP(10000), alice, bob, carol, dave, gw);
1689 env.close();
1690
1691 // all transactions fail
1692 {
1693 auto const preAlice = env.balance(alice);
1694 auto const preBob = env.balance(bob);
1695
1696 auto const batchFee = batch::calcBatchFee(env, 0, 3);
1697 auto const seq = env.seq(alice);
1698 auto const [txIDs, batchID] = submitBatch(
1699 env,
1700 tesSUCCESS,
1701 batch::outer(alice, seq, batchFee, tfOnlyOne),
1702 // tecUNFUNDED_PAYMENT: alice does not have enough XRP
1703 batch::Inner(pay(alice, bob, XRP(9999)), seq + 1),
1704 // tecUNFUNDED_PAYMENT: alice does not have enough XRP
1705 batch::Inner(pay(alice, bob, XRP(9999)), seq + 2),
1706 // tecUNFUNDED_PAYMENT: alice does not have enough XRP
1707 batch::Inner(pay(alice, bob, XRP(9999)), seq + 3));
1708 env.close();
1709
1710 std::vector<TestLedgerData> const testCases = {
1711 {.index = 0,
1712 .txType = "Batch",
1713 .result = "tesSUCCESS",
1714 .txHash = batchID,
1715 .batchID = std::nullopt},
1716 {.index = 1,
1717 .txType = "Payment",
1718 .result = "tecUNFUNDED_PAYMENT",
1719 .txHash = txIDs[0],
1720 .batchID = batchID},
1721 {.index = 2,
1722 .txType = "Payment",
1723 .result = "tecUNFUNDED_PAYMENT",
1724 .txHash = txIDs[1],
1725 .batchID = batchID},
1726 {.index = 3,
1727 .txType = "Payment",
1728 .result = "tecUNFUNDED_PAYMENT",
1729 .txHash = txIDs[2],
1730 .batchID = batchID},
1731 };
1732 validateClosedLedger(env, testCases);
1733
1734 // Alice consumes sequences (# of txns)
1735 BEAST_EXPECT(env.seq(alice) == seq + 4);
1736
1737 // Alice pays XRP & Fee; Bob receives XRP
1738 BEAST_EXPECT(env.balance(alice) == preAlice - batchFee);
1739 BEAST_EXPECT(env.balance(bob) == preBob);
1740 }
1741
1742 // first transaction fails
1743 {
1744 auto const preAlice = env.balance(alice);
1745 auto const preBob = env.balance(bob);
1746
1747 auto const batchFee = batch::calcBatchFee(env, 0, 3);
1748 auto const seq = env.seq(alice);
1749 auto const [txIDs, batchID] = submitBatch(
1750 env,
1751 tesSUCCESS,
1752 batch::outer(alice, seq, batchFee, tfOnlyOne),
1753 // tecUNFUNDED_PAYMENT: alice does not have enough XRP
1754 batch::Inner(pay(alice, bob, XRP(9999)), seq + 1),
1755 batch::Inner(pay(alice, bob, XRP(1)), seq + 2),
1756 batch::Inner(pay(alice, bob, XRP(2)), seq + 3));
1757 env.close();
1758
1759 std::vector<TestLedgerData> const testCases = {
1760 {.index = 0,
1761 .txType = "Batch",
1762 .result = "tesSUCCESS",
1763 .txHash = batchID,
1764 .batchID = std::nullopt},
1765 {.index = 1,
1766 .txType = "Payment",
1767 .result = "tecUNFUNDED_PAYMENT",
1768 .txHash = txIDs[0],
1769 .batchID = batchID},
1770 {.index = 2,
1771 .txType = "Payment",
1772 .result = "tesSUCCESS",
1773 .txHash = txIDs[1],
1774 .batchID = batchID},
1775 };
1776 validateClosedLedger(env, testCases);
1777
1778 // Alice consumes sequences (# of txns)
1779 BEAST_EXPECT(env.seq(alice) == seq + 3);
1780
1781 // Alice pays XRP & Fee; Bob receives XRP
1782 BEAST_EXPECT(env.balance(alice) == preAlice - XRP(1) - batchFee);
1783 BEAST_EXPECT(env.balance(bob) == preBob + XRP(1));
1784 }
1785
1786 // tec failure
1787 {
1788 auto const preAlice = env.balance(alice);
1789 auto const preBob = env.balance(bob);
1790
1791 auto const batchFee = batch::calcBatchFee(env, 0, 3);
1792 auto const seq = env.seq(alice);
1793 auto const [txIDs, batchID] = submitBatch(
1794 env,
1795 tesSUCCESS,
1796 batch::outer(alice, seq, batchFee, tfOnlyOne),
1797 batch::Inner(pay(alice, bob, XRP(1)), seq + 1),
1798 // tecUNFUNDED_PAYMENT: alice does not have enough XRP
1799 batch::Inner(pay(alice, bob, XRP(9999)), seq + 2),
1800 batch::Inner(pay(alice, bob, XRP(2)), seq + 3));
1801 env.close();
1802
1803 std::vector<TestLedgerData> const testCases = {
1804 {.index = 0,
1805 .txType = "Batch",
1806 .result = "tesSUCCESS",
1807 .txHash = batchID,
1808 .batchID = std::nullopt},
1809 {.index = 1,
1810 .txType = "Payment",
1811 .result = "tesSUCCESS",
1812 .txHash = txIDs[0],
1813 .batchID = batchID},
1814 };
1815 validateClosedLedger(env, testCases);
1816
1817 // Alice consumes sequences (# of txns)
1818 BEAST_EXPECT(env.seq(alice) == seq + 2);
1819
1820 // Alice pays XRP & Fee; Bob receives XRP
1821 BEAST_EXPECT(env.balance(alice) == preAlice - XRP(1) - batchFee);
1822 BEAST_EXPECT(env.balance(bob) == preBob + XRP(1));
1823 }
1824
1825 // tef failure
1826 {
1827 auto const preAlice = env.balance(alice);
1828 auto const preBob = env.balance(bob);
1829
1830 auto const batchFee = batch::calcBatchFee(env, 0, 3);
1831 auto const seq = env.seq(alice);
1832 auto const [txIDs, batchID] = submitBatch(
1833 env,
1834 tesSUCCESS,
1835 batch::outer(alice, seq, batchFee, tfOnlyOne),
1836 // tefNO_AUTH_REQUIRED: trustline auth is not required
1837 batch::Inner(trust(alice, usd(1000), tfSetfAuth), seq + 1),
1838 batch::Inner(pay(alice, bob, XRP(1)), seq + 1),
1839 batch::Inner(pay(alice, bob, XRP(2)), seq + 3));
1840 env.close();
1841
1842 std::vector<TestLedgerData> const testCases = {
1843 {.index = 0,
1844 .txType = "Batch",
1845 .result = "tesSUCCESS",
1846 .txHash = batchID,
1847 .batchID = std::nullopt},
1848 {.index = 1,
1849 .txType = "Payment",
1850 .result = "tesSUCCESS",
1851 .txHash = txIDs[1],
1852 .batchID = batchID},
1853 };
1854 validateClosedLedger(env, testCases);
1855
1856 // Alice consumes sequences (# of txns)
1857 BEAST_EXPECT(env.seq(alice) == seq + 2);
1858
1859 // Alice pays XRP & Fee; Bob receives XRP
1860 BEAST_EXPECT(env.balance(alice) == preAlice - batchFee - XRP(1));
1861 BEAST_EXPECT(env.balance(bob) == preBob + XRP(1));
1862 }
1863
1864 // ter failure
1865 {
1866 auto const preAlice = env.balance(alice);
1867 auto const preBob = env.balance(bob);
1868
1869 auto const batchFee = batch::calcBatchFee(env, 0, 3);
1870 auto const seq = env.seq(alice);
1871 auto const [txIDs, batchID] = submitBatch(
1872 env,
1873 tesSUCCESS,
1874 batch::outer(alice, seq, batchFee, tfOnlyOne),
1875 // terPRE_TICKET: ticket does not exist
1876 batch::Inner(trust(alice, usd(1000), tfSetfAuth), 0, seq + 1),
1877 batch::Inner(pay(alice, bob, XRP(1)), seq + 1),
1878 batch::Inner(pay(alice, bob, XRP(2)), seq + 3));
1879 env.close();
1880
1881 std::vector<TestLedgerData> const testCases = {
1882 {.index = 0,
1883 .txType = "Batch",
1884 .result = "tesSUCCESS",
1885 .txHash = batchID,
1886 .batchID = std::nullopt},
1887 {.index = 1,
1888 .txType = "Payment",
1889 .result = "tesSUCCESS",
1890 .txHash = txIDs[1],
1891 .batchID = batchID},
1892 };
1893 validateClosedLedger(env, testCases);
1894
1895 // Alice consumes sequences (# of txns)
1896 BEAST_EXPECT(env.seq(alice) == seq + 2);
1897
1898 // Alice pays XRP & Fee; Bob receives XRP
1899 BEAST_EXPECT(env.balance(alice) == preAlice - batchFee - XRP(1));
1900 BEAST_EXPECT(env.balance(bob) == preBob + XRP(1));
1901 }
1902
1903 // tec (tecKILLED) error
1904 {
1905 auto const preAlice = env.balance(alice);
1906 auto const preBob = env.balance(bob);
1907 auto const preCarol = env.balance(carol);
1908 auto const seq = env.seq(alice);
1909 auto const batchFee = batch::calcBatchFee(env, 0, 6);
1910
1911 auto const [txIDs, batchID] = submitBatch(
1912 env,
1913 tesSUCCESS,
1914 batch::outer(alice, seq, batchFee, tfOnlyOne),
1916 offer(alice, alice["USD"](100), XRP(100), tfImmediateOrCancel), seq + 1),
1918 offer(alice, alice["USD"](100), XRP(100), tfImmediateOrCancel), seq + 2),
1920 offer(alice, alice["USD"](100), XRP(100), tfImmediateOrCancel), seq + 3),
1921 batch::Inner(pay(alice, bob, XRP(100)), seq + 4),
1922 batch::Inner(pay(alice, carol, XRP(100)), seq + 5),
1923 batch::Inner(pay(alice, dave, XRP(100)), seq + 6));
1924 env.close();
1925
1926 std::vector<TestLedgerData> const testCases = {
1927 {.index = 0,
1928 .txType = "Batch",
1929 .result = "tesSUCCESS",
1930 .txHash = batchID,
1931 .batchID = std::nullopt},
1932 {.index = 1,
1933 .txType = "OfferCreate",
1934 .result = "tecKILLED",
1935 .txHash = txIDs[0],
1936 .batchID = batchID},
1937 {.index = 2,
1938 .txType = "OfferCreate",
1939 .result = "tecKILLED",
1940 .txHash = txIDs[1],
1941 .batchID = batchID},
1942 {.index = 3,
1943 .txType = "OfferCreate",
1944 .result = "tecKILLED",
1945 .txHash = txIDs[2],
1946 .batchID = batchID},
1947 {.index = 4,
1948 .txType = "Payment",
1949 .result = "tesSUCCESS",
1950 .txHash = txIDs[3],
1951 .batchID = batchID},
1952 };
1953 validateClosedLedger(env, testCases);
1954
1955 BEAST_EXPECT(env.balance(alice) == preAlice - XRP(100) - batchFee);
1956 BEAST_EXPECT(env.balance(bob) == preBob + XRP(100));
1957 BEAST_EXPECT(env.balance(carol) == preCarol);
1958 }
1959 }
1960
1961 void
1963 {
1964 testcase("until failure");
1965
1966 using namespace test::jtx;
1967 using namespace std::literals;
1968
1969 test::jtx::Env env{*this, features};
1970
1971 auto const alice = Account("alice");
1972 auto const bob = Account("bob");
1973 auto const carol = Account("carol");
1974 auto const dave = Account("dave");
1975 auto const gw = Account("gw");
1976 auto const usd = gw["USD"];
1977 env.fund(XRP(10000), alice, bob, carol, dave, gw);
1978 env.close();
1979
1980 // first transaction fails
1981 {
1982 auto const preAlice = env.balance(alice);
1983 auto const preBob = env.balance(bob);
1984
1985 auto const batchFee = batch::calcBatchFee(env, 0, 4);
1986 auto const seq = env.seq(alice);
1987 auto const [txIDs, batchID] = submitBatch(
1988 env,
1989 tesSUCCESS,
1990 batch::outer(alice, seq, batchFee, tfUntilFailure),
1991 // tecUNFUNDED_PAYMENT: alice does not have enough XRP
1992 batch::Inner(pay(alice, bob, XRP(9999)), seq + 1),
1993 batch::Inner(pay(alice, bob, XRP(1)), seq + 2),
1994 batch::Inner(pay(alice, bob, XRP(2)), seq + 3),
1995 batch::Inner(pay(alice, bob, XRP(3)), seq + 4));
1996 env.close();
1997
1998 std::vector<TestLedgerData> const testCases = {
1999 {.index = 0,
2000 .txType = "Batch",
2001 .result = "tesSUCCESS",
2002 .txHash = batchID,
2003 .batchID = std::nullopt},
2004 {.index = 1,
2005 .txType = "Payment",
2006 .result = "tecUNFUNDED_PAYMENT",
2007 .txHash = txIDs[0],
2008 .batchID = batchID},
2009 };
2010 validateClosedLedger(env, testCases);
2011
2012 // Alice consumes sequences (# of txns)
2013 BEAST_EXPECT(env.seq(alice) == seq + 2);
2014
2015 // Alice pays XRP & Fee; Bob receives XRP
2016 BEAST_EXPECT(env.balance(alice) == preAlice - batchFee);
2017 BEAST_EXPECT(env.balance(bob) == preBob);
2018 }
2019
2020 // all transactions succeed
2021 {
2022 auto const preAlice = env.balance(alice);
2023 auto const preBob = env.balance(bob);
2024
2025 auto const batchFee = batch::calcBatchFee(env, 0, 4);
2026 auto const seq = env.seq(alice);
2027 auto const [txIDs, batchID] = submitBatch(
2028 env,
2029 tesSUCCESS,
2030 batch::outer(alice, seq, batchFee, tfUntilFailure),
2031 batch::Inner(pay(alice, bob, XRP(1)), seq + 1),
2032 batch::Inner(pay(alice, bob, XRP(2)), seq + 2),
2033 batch::Inner(pay(alice, bob, XRP(3)), seq + 3),
2034 batch::Inner(pay(alice, bob, XRP(4)), seq + 4));
2035 env.close();
2036
2037 std::vector<TestLedgerData> const testCases = {
2038 {.index = 0,
2039 .txType = "Batch",
2040 .result = "tesSUCCESS",
2041 .txHash = batchID,
2042 .batchID = std::nullopt},
2043 {.index = 1,
2044 .txType = "Payment",
2045 .result = "tesSUCCESS",
2046 .txHash = txIDs[0],
2047 .batchID = batchID},
2048 {.index = 2,
2049 .txType = "Payment",
2050 .result = "tesSUCCESS",
2051 .txHash = txIDs[1],
2052 .batchID = batchID},
2053 {.index = 3,
2054 .txType = "Payment",
2055 .result = "tesSUCCESS",
2056 .txHash = txIDs[2],
2057 .batchID = batchID},
2058 {.index = 4,
2059 .txType = "Payment",
2060 .result = "tesSUCCESS",
2061 .txHash = txIDs[3],
2062 .batchID = batchID},
2063 };
2064 validateClosedLedger(env, testCases);
2065
2066 // Alice consumes sequences (# of txns)
2067 BEAST_EXPECT(env.seq(alice) == seq + 5);
2068
2069 // Alice pays XRP & Fee; Bob receives XRP
2070 BEAST_EXPECT(env.balance(alice) == preAlice - XRP(10) - batchFee);
2071 BEAST_EXPECT(env.balance(bob) == preBob + XRP(10));
2072 }
2073
2074 // tec error
2075 {
2076 auto const preAlice = env.balance(alice);
2077 auto const preBob = env.balance(bob);
2078
2079 auto const batchFee = batch::calcBatchFee(env, 0, 4);
2080 auto const seq = env.seq(alice);
2081 auto const [txIDs, batchID] = submitBatch(
2082 env,
2083 tesSUCCESS,
2084 batch::outer(alice, seq, batchFee, tfUntilFailure),
2085 batch::Inner(pay(alice, bob, XRP(1)), seq + 1),
2086 batch::Inner(pay(alice, bob, XRP(2)), seq + 2),
2087 // tecUNFUNDED_PAYMENT: alice does not have enough XRP
2088 batch::Inner(pay(alice, bob, XRP(9999)), seq + 3),
2089 batch::Inner(pay(alice, bob, XRP(3)), seq + 4));
2090 env.close();
2091
2092 std::vector<TestLedgerData> const testCases = {
2093 {.index = 0,
2094 .txType = "Batch",
2095 .result = "tesSUCCESS",
2096 .txHash = batchID,
2097 .batchID = std::nullopt},
2098 {.index = 1,
2099 .txType = "Payment",
2100 .result = "tesSUCCESS",
2101 .txHash = txIDs[0],
2102 .batchID = batchID},
2103 {.index = 2,
2104 .txType = "Payment",
2105 .result = "tesSUCCESS",
2106 .txHash = txIDs[1],
2107 .batchID = batchID},
2108 {.index = 3,
2109 .txType = "Payment",
2110 .result = "tecUNFUNDED_PAYMENT",
2111 .txHash = txIDs[2],
2112 .batchID = batchID},
2113 };
2114 validateClosedLedger(env, testCases);
2115
2116 // Alice consumes sequences (# of txns)
2117 BEAST_EXPECT(env.seq(alice) == seq + 4);
2118
2119 // Alice pays XRP & Fee; Bob receives XRP
2120 BEAST_EXPECT(env.balance(alice) == preAlice - XRP(3) - batchFee);
2121 BEAST_EXPECT(env.balance(bob) == preBob + XRP(3));
2122 }
2123
2124 // tef error
2125 {
2126 auto const preAlice = env.balance(alice);
2127 auto const preBob = env.balance(bob);
2128
2129 auto const batchFee = batch::calcBatchFee(env, 0, 4);
2130 auto const seq = env.seq(alice);
2131 auto const [txIDs, batchID] = submitBatch(
2132 env,
2133 tesSUCCESS,
2134 batch::outer(alice, seq, batchFee, tfUntilFailure),
2135 batch::Inner(pay(alice, bob, XRP(1)), seq + 1),
2136 batch::Inner(pay(alice, bob, XRP(2)), seq + 2),
2137 // tefNO_AUTH_REQUIRED: trustline auth is not required
2138 batch::Inner(trust(alice, usd(1000), tfSetfAuth), seq + 3),
2139 batch::Inner(pay(alice, bob, XRP(3)), seq + 4));
2140 env.close();
2141
2142 std::vector<TestLedgerData> const testCases = {
2143 {.index = 0,
2144 .txType = "Batch",
2145 .result = "tesSUCCESS",
2146 .txHash = batchID,
2147 .batchID = std::nullopt},
2148 {.index = 1,
2149 .txType = "Payment",
2150 .result = "tesSUCCESS",
2151 .txHash = txIDs[0],
2152 .batchID = batchID},
2153 {.index = 2,
2154 .txType = "Payment",
2155 .result = "tesSUCCESS",
2156 .txHash = txIDs[1],
2157 .batchID = batchID},
2158 };
2159 validateClosedLedger(env, testCases);
2160
2161 // Alice consumes sequences (# of txns)
2162 BEAST_EXPECT(env.seq(alice) == seq + 3);
2163
2164 // Alice pays XRP & Fee; Bob receives XRP
2165 BEAST_EXPECT(env.balance(alice) == preAlice - XRP(3) - batchFee);
2166 BEAST_EXPECT(env.balance(bob) == preBob + XRP(3));
2167 }
2168
2169 // ter error
2170 {
2171 auto const preAlice = env.balance(alice);
2172 auto const preBob = env.balance(bob);
2173
2174 auto const batchFee = batch::calcBatchFee(env, 0, 4);
2175 auto const seq = env.seq(alice);
2176 auto const [txIDs, batchID] = submitBatch(
2177 env,
2178 tesSUCCESS,
2179 batch::outer(alice, seq, batchFee, tfUntilFailure),
2180 batch::Inner(pay(alice, bob, XRP(1)), seq + 1),
2181 batch::Inner(pay(alice, bob, XRP(2)), seq + 2),
2182 // terPRE_TICKET: ticket does not exist
2183 batch::Inner(trust(alice, usd(1000), tfSetfAuth), 0, seq + 3),
2184 batch::Inner(pay(alice, bob, XRP(3)), seq + 4));
2185 env.close();
2186
2187 std::vector<TestLedgerData> const testCases = {
2188 {.index = 0,
2189 .txType = "Batch",
2190 .result = "tesSUCCESS",
2191 .txHash = batchID,
2192 .batchID = std::nullopt},
2193 {.index = 1,
2194 .txType = "Payment",
2195 .result = "tesSUCCESS",
2196 .txHash = txIDs[0],
2197 .batchID = batchID},
2198 {.index = 2,
2199 .txType = "Payment",
2200 .result = "tesSUCCESS",
2201 .txHash = txIDs[1],
2202 .batchID = batchID},
2203 };
2204 validateClosedLedger(env, testCases);
2205
2206 // Alice consumes sequences (# of txns)
2207 BEAST_EXPECT(env.seq(alice) == seq + 3);
2208
2209 // Alice pays XRP & Fee; Bob receives XRP
2210 BEAST_EXPECT(env.balance(alice) == preAlice - XRP(3) - batchFee);
2211 BEAST_EXPECT(env.balance(bob) == preBob + XRP(3));
2212 }
2213
2214 // tec (tecKILLED) error
2215 {
2216 auto const preAlice = env.balance(alice);
2217 auto const preBob = env.balance(bob);
2218 auto const preCarol = env.balance(carol);
2219 auto const seq = env.seq(alice);
2220 auto const batchFee = batch::calcBatchFee(env, 0, 4);
2221 auto const [txIDs, batchID] = submitBatch(
2222 env,
2223 tesSUCCESS,
2224 batch::outer(alice, seq, batchFee, tfUntilFailure),
2225 batch::Inner(pay(alice, bob, XRP(100)), seq + 1),
2226 batch::Inner(pay(alice, carol, XRP(100)), seq + 2),
2228 offer(alice, alice["USD"](100), XRP(100), tfImmediateOrCancel), seq + 3),
2229 batch::Inner(pay(alice, dave, XRP(100)), seq + 4));
2230 env.close();
2231
2232 std::vector<TestLedgerData> const testCases = {
2233 {.index = 0,
2234 .txType = "Batch",
2235 .result = "tesSUCCESS",
2236 .txHash = batchID,
2237 .batchID = std::nullopt},
2238 {.index = 1,
2239 .txType = "Payment",
2240 .result = "tesSUCCESS",
2241 .txHash = txIDs[0],
2242 .batchID = batchID},
2243 {.index = 2,
2244 .txType = "Payment",
2245 .result = "tesSUCCESS",
2246 .txHash = txIDs[1],
2247 .batchID = batchID},
2248 {.index = 3,
2249 .txType = "OfferCreate",
2250 .result = "tecKILLED",
2251 .txHash = txIDs[2],
2252 .batchID = batchID},
2253 };
2254 validateClosedLedger(env, testCases);
2255
2256 BEAST_EXPECT(env.balance(alice) == preAlice - XRP(200) - batchFee);
2257 BEAST_EXPECT(env.balance(bob) == preBob + XRP(100));
2258 BEAST_EXPECT(env.balance(carol) == preCarol + XRP(100));
2259 }
2260 }
2261
2262 void
2264 {
2265 testcase("independent");
2266
2267 using namespace test::jtx;
2268 using namespace std::literals;
2269
2270 test::jtx::Env env{*this, features};
2271
2272 auto const alice = Account("alice");
2273 auto const bob = Account("bob");
2274 auto const carol = Account("carol");
2275 auto const gw = Account("gw");
2276 auto const usd = gw["USD"];
2277 env.fund(XRP(10000), alice, bob, carol, gw);
2278 env.close();
2279
2280 // multiple transactions fail
2281 {
2282 auto const preAlice = env.balance(alice);
2283 auto const preBob = env.balance(bob);
2284
2285 auto const batchFee = batch::calcBatchFee(env, 0, 4);
2286 auto const seq = env.seq(alice);
2287 auto const [txIDs, batchID] = submitBatch(
2288 env,
2289 tesSUCCESS,
2290 batch::outer(alice, seq, batchFee, tfIndependent),
2291 batch::Inner(pay(alice, bob, XRP(1)), seq + 1),
2292 // tecUNFUNDED_PAYMENT: alice does not have enough XRP
2293 batch::Inner(pay(alice, bob, XRP(9999)), seq + 2),
2294 // tecUNFUNDED_PAYMENT: alice does not have enough XRP
2295 batch::Inner(pay(alice, bob, XRP(9999)), seq + 3),
2296 batch::Inner(pay(alice, bob, XRP(3)), seq + 4));
2297 env.close();
2298
2299 std::vector<TestLedgerData> const testCases = {
2300 {.index = 0,
2301 .txType = "Batch",
2302 .result = "tesSUCCESS",
2303 .txHash = batchID,
2304 .batchID = std::nullopt},
2305 {.index = 1,
2306 .txType = "Payment",
2307 .result = "tesSUCCESS",
2308 .txHash = txIDs[0],
2309 .batchID = batchID},
2310 {.index = 2,
2311 .txType = "Payment",
2312 .result = "tecUNFUNDED_PAYMENT",
2313 .txHash = txIDs[1],
2314 .batchID = batchID},
2315 {.index = 3,
2316 .txType = "Payment",
2317 .result = "tecUNFUNDED_PAYMENT",
2318 .txHash = txIDs[2],
2319 .batchID = batchID},
2320 {.index = 4,
2321 .txType = "Payment",
2322 .result = "tesSUCCESS",
2323 .txHash = txIDs[3],
2324 .batchID = batchID},
2325 };
2326 validateClosedLedger(env, testCases);
2327
2328 // Alice consumes sequences (# of txns)
2329 BEAST_EXPECT(env.seq(alice) == seq + 5);
2330
2331 // Alice pays XRP & Fee; Bob receives XRP
2332 BEAST_EXPECT(env.balance(alice) == preAlice - XRP(4) - batchFee);
2333 BEAST_EXPECT(env.balance(bob) == preBob + XRP(4));
2334 }
2335
2336 // tec error
2337 {
2338 auto const preAlice = env.balance(alice);
2339 auto const preBob = env.balance(bob);
2340
2341 auto const batchFee = batch::calcBatchFee(env, 0, 4);
2342 auto const seq = env.seq(alice);
2343 auto const [txIDs, batchID] = submitBatch(
2344 env,
2345 tesSUCCESS,
2346 batch::outer(alice, seq, batchFee, tfIndependent),
2347 batch::Inner(pay(alice, bob, XRP(1)), seq + 1),
2348 batch::Inner(pay(alice, bob, XRP(2)), seq + 2),
2349 // tecUNFUNDED_PAYMENT: alice does not have enough XRP
2350 batch::Inner(pay(alice, bob, XRP(9999)), seq + 3),
2351 batch::Inner(pay(alice, bob, XRP(3)), seq + 4));
2352 env.close();
2353
2354 std::vector<TestLedgerData> const testCases = {
2355 {.index = 0,
2356 .txType = "Batch",
2357 .result = "tesSUCCESS",
2358 .txHash = batchID,
2359 .batchID = std::nullopt},
2360 {.index = 1,
2361 .txType = "Payment",
2362 .result = "tesSUCCESS",
2363 .txHash = txIDs[0],
2364 .batchID = batchID},
2365 {.index = 2,
2366 .txType = "Payment",
2367 .result = "tesSUCCESS",
2368 .txHash = txIDs[1],
2369 .batchID = batchID},
2370 {.index = 3,
2371 .txType = "Payment",
2372 .result = "tecUNFUNDED_PAYMENT",
2373 .txHash = txIDs[2],
2374 .batchID = batchID},
2375 {.index = 4,
2376 .txType = "Payment",
2377 .result = "tesSUCCESS",
2378 .txHash = txIDs[3],
2379 .batchID = batchID},
2380 };
2381 validateClosedLedger(env, testCases);
2382
2383 // Alice consumes sequences (# of txns)
2384 BEAST_EXPECT(env.seq(alice) == seq + 5);
2385
2386 // Alice pays XRP & Fee; Bob receives XRP
2387 BEAST_EXPECT(env.balance(alice) == preAlice - XRP(6) - batchFee);
2388 BEAST_EXPECT(env.balance(bob) == preBob + XRP(6));
2389 }
2390
2391 // tef error
2392 {
2393 auto const preAlice = env.balance(alice);
2394 auto const preBob = env.balance(bob);
2395
2396 auto const batchFee = batch::calcBatchFee(env, 0, 4);
2397 auto const seq = env.seq(alice);
2398 auto const [txIDs, batchID] = submitBatch(
2399 env,
2400 tesSUCCESS,
2401 batch::outer(alice, seq, batchFee, tfIndependent),
2402 batch::Inner(pay(alice, bob, XRP(1)), seq + 1),
2403 batch::Inner(pay(alice, bob, XRP(2)), seq + 2),
2404 // tefNO_AUTH_REQUIRED: trustline auth is not required
2405 batch::Inner(trust(alice, usd(1000), tfSetfAuth), seq + 3),
2406 batch::Inner(pay(alice, bob, XRP(3)), seq + 3));
2407 env.close();
2408
2409 std::vector<TestLedgerData> const testCases = {
2410 {.index = 0,
2411 .txType = "Batch",
2412 .result = "tesSUCCESS",
2413 .txHash = batchID,
2414 .batchID = std::nullopt},
2415 {.index = 1,
2416 .txType = "Payment",
2417 .result = "tesSUCCESS",
2418 .txHash = txIDs[0],
2419 .batchID = batchID},
2420 {.index = 2,
2421 .txType = "Payment",
2422 .result = "tesSUCCESS",
2423 .txHash = txIDs[1],
2424 .batchID = batchID},
2425 {.index = 3,
2426 .txType = "Payment",
2427 .result = "tesSUCCESS",
2428 .txHash = txIDs[3],
2429 .batchID = batchID},
2430 };
2431 validateClosedLedger(env, testCases);
2432
2433 // Alice consumes sequences (# of txns)
2434 BEAST_EXPECT(env.seq(alice) == seq + 4);
2435
2436 // Alice pays XRP & Fee; Bob receives XRP
2437 BEAST_EXPECT(env.balance(alice) == preAlice - batchFee - XRP(6));
2438 BEAST_EXPECT(env.balance(bob) == preBob + XRP(6));
2439 }
2440
2441 // ter error
2442 {
2443 auto const preAlice = env.balance(alice);
2444 auto const preBob = env.balance(bob);
2445
2446 auto const batchFee = batch::calcBatchFee(env, 0, 4);
2447 auto const seq = env.seq(alice);
2448 auto const [txIDs, batchID] = submitBatch(
2449 env,
2450 tesSUCCESS,
2451 batch::outer(alice, seq, batchFee, tfIndependent),
2452 batch::Inner(pay(alice, bob, XRP(1)), seq + 1),
2453 batch::Inner(pay(alice, bob, XRP(2)), seq + 2),
2454 // terPRE_TICKET: ticket does not exist
2455 batch::Inner(trust(alice, usd(1000), tfSetfAuth), 0, seq + 3),
2456 batch::Inner(pay(alice, bob, XRP(3)), seq + 3));
2457 env.close();
2458
2459 std::vector<TestLedgerData> const testCases = {
2460 {.index = 0,
2461 .txType = "Batch",
2462 .result = "tesSUCCESS",
2463 .txHash = batchID,
2464 .batchID = std::nullopt},
2465 {.index = 1,
2466 .txType = "Payment",
2467 .result = "tesSUCCESS",
2468 .txHash = txIDs[0],
2469 .batchID = batchID},
2470 {.index = 2,
2471 .txType = "Payment",
2472 .result = "tesSUCCESS",
2473 .txHash = txIDs[1],
2474 .batchID = batchID},
2475 {.index = 3,
2476 .txType = "Payment",
2477 .result = "tesSUCCESS",
2478 .txHash = txIDs[3],
2479 .batchID = batchID},
2480 };
2481 validateClosedLedger(env, testCases);
2482
2483 // Alice consumes sequences (# of txns)
2484 BEAST_EXPECT(env.seq(alice) == seq + 4);
2485
2486 // Alice pays XRP & Fee; Bob receives XRP
2487 BEAST_EXPECT(env.balance(alice) == preAlice - batchFee - XRP(6));
2488 BEAST_EXPECT(env.balance(bob) == preBob + XRP(6));
2489 }
2490
2491 // tec (tecKILLED) error
2492 {
2493 auto const preAlice = env.balance(alice);
2494 auto const preBob = env.balance(bob);
2495 auto const preCarol = env.balance(carol);
2496 auto const seq = env.seq(alice);
2497 auto const batchFee = batch::calcBatchFee(env, 0, 3);
2498 auto const [txIDs, batchID] = submitBatch(
2499 env,
2500 tesSUCCESS,
2501 batch::outer(alice, seq, batchFee, tfIndependent),
2502 batch::Inner(pay(alice, bob, XRP(100)), seq + 1),
2503 batch::Inner(pay(alice, carol, XRP(100)), seq + 2),
2505 offer(alice, alice["USD"](100), XRP(100), tfImmediateOrCancel), seq + 3));
2506 env.close();
2507
2508 std::vector<TestLedgerData> const testCases = {
2509 {.index = 0,
2510 .txType = "Batch",
2511 .result = "tesSUCCESS",
2512 .txHash = batchID,
2513 .batchID = std::nullopt},
2514 {.index = 1,
2515 .txType = "Payment",
2516 .result = "tesSUCCESS",
2517 .txHash = txIDs[0],
2518 .batchID = batchID},
2519 {.index = 2,
2520 .txType = "Payment",
2521 .result = "tesSUCCESS",
2522 .txHash = txIDs[1],
2523 .batchID = batchID},
2524 {.index = 3,
2525 .txType = "OfferCreate",
2526 .result = "tecKILLED",
2527 .txHash = txIDs[2],
2528 .batchID = batchID},
2529 };
2530 validateClosedLedger(env, testCases);
2531
2532 BEAST_EXPECT(env.balance(alice) == preAlice - XRP(200) - batchFee);
2533 BEAST_EXPECT(env.balance(bob) == preBob + XRP(100));
2534 BEAST_EXPECT(env.balance(carol) == preCarol + XRP(100));
2535 }
2536 }
2537
2538 void
2539 doTestInnerSubmitRPC(FeatureBitset features, bool withBatch)
2540 {
2541 bool const withInnerSigFix = features[fixBatchInnerSigs];
2542
2543 std::string const testName = [&]() {
2545 ss << "inner submit rpc: batch " << (withBatch ? "enabled" : "disabled")
2546 << ", inner sig fix: " << (withInnerSigFix ? "enabled" : "disabled") << ": ";
2547 return ss.str();
2548 }();
2549
2550 auto const amend = withBatch ? features : features - featureBatch;
2551
2552 using namespace test::jtx;
2553 using namespace std::literals;
2554
2555 test::jtx::Env env{*this, amend};
2556 if (!BEAST_EXPECT(amend[featureBatch] == withBatch))
2557 return;
2558
2559 auto const alice = Account("alice");
2560 auto const bob = Account("bob");
2561
2562 env.fund(XRP(10000), alice, bob);
2563 env.close();
2564
2565 auto submitAndValidate = [&](std::string caseName,
2566 Slice const& slice,
2567 int line,
2568 std::optional<std::string> expectedEnabled = std::nullopt,
2569 std::optional<std::string> expectedDisabled = std::nullopt,
2570 bool expectInvalidFlag = false) {
2571 testcase << testName << caseName
2572 << (expectInvalidFlag ? " - Expected to reach tx engine!" : "");
2573 auto const jrr = env.rpc("submit", strHex(slice))[jss::result];
2574 auto const expected = withBatch
2575 ? expectedEnabled.value_or(
2576 "fails local checks: Malformed: Invalid inner batch "
2577 "transaction.")
2578 : expectedDisabled.value_or("fails local checks: Empty SigningPubKey.");
2579 if (expectInvalidFlag)
2580 {
2581 expect(
2582 jrr[jss::status] == "success" && jrr[jss::engine_result] == "temINVALID_FLAG",
2583 pretty(jrr),
2584 __FILE__,
2585 line);
2586 }
2587 else
2588 {
2589 expect(
2590 jrr[jss::status] == "error" && jrr[jss::error] == "invalidTransaction" &&
2591 jrr[jss::error_exception] == expected,
2592 pretty(jrr),
2593 __FILE__,
2594 line);
2595 }
2596 env.close();
2597 };
2598
2599 // Invalid RPC Submission: TxnSignature
2600 // + has `TxnSignature` field
2601 // - has no `SigningPubKey` field
2602 // - has no `Signers` field
2603 // + has `tfInnerBatchTxn` flag
2604 {
2605 auto txn = batch::Inner(pay(alice, bob, XRP(1)), env.seq(alice));
2606 txn[sfTxnSignature] = "DEADBEEF";
2607 STParsedJSONObject parsed("test", txn.getTxn());
2608 Serializer s;
2609 parsed.object->add(s); // NOLINT(bugprone-unchecked-optional-access)
2610 submitAndValidate("TxnSignature set", s.slice(), __LINE__);
2611 }
2612
2613 // Invalid RPC Submission: SigningPubKey
2614 // - has no `TxnSignature` field
2615 // + has `SigningPubKey` field
2616 // - has no `Signers` field
2617 // + has `tfInnerBatchTxn` flag
2618 {
2619 auto txn = batch::Inner(pay(alice, bob, XRP(1)), env.seq(alice));
2620 txn[sfSigningPubKey] = strHex(alice.pk());
2621 STParsedJSONObject parsed("test", txn.getTxn());
2622 Serializer s;
2623 parsed.object->add(s); // NOLINT(bugprone-unchecked-optional-access)
2624 submitAndValidate(
2625 "SigningPubKey set",
2626 s.slice(),
2627 __LINE__,
2628 std::nullopt,
2629 "fails local checks: Invalid signature.");
2630 }
2631
2632 // Invalid RPC Submission: Signers
2633 // - has no `TxnSignature` field
2634 // + has empty `SigningPubKey` field
2635 // + has `Signers` field
2636 // + has `tfInnerBatchTxn` flag
2637 {
2638 auto txn = batch::Inner(pay(alice, bob, XRP(1)), env.seq(alice));
2639 txn[sfSigners] = json::ValueType::Array;
2640 STParsedJSONObject parsed("test", txn.getTxn());
2641 Serializer s;
2642 parsed.object->add(s); // NOLINT(bugprone-unchecked-optional-access)
2643 submitAndValidate(
2644 "Signers set",
2645 s.slice(),
2646 __LINE__,
2647 std::nullopt,
2648 "fails local checks: Invalid Signers array size.");
2649 }
2650
2651 {
2652 // Fully signed inner batch transaction
2653 auto const txn = batch::Inner(pay(alice, bob, XRP(1)), env.seq(alice));
2654 auto const jt = env.jt(txn.getTxn());
2655
2656 STParsedJSONObject parsed("test", jt.jv);
2657 Serializer s;
2658 parsed.object->add(s); // NOLINT(bugprone-unchecked-optional-access)
2659 submitAndValidate(
2660 "Fully signed", s.slice(), __LINE__, std::nullopt, std::nullopt, !withBatch);
2661 }
2662
2663 // Invalid RPC Submission: tfInnerBatchTxn
2664 // - has no `TxnSignature` field
2665 // + has empty `SigningPubKey` field
2666 // - has no `Signers` field
2667 // + has `tfInnerBatchTxn` flag
2668 {
2669 auto txn = batch::Inner(pay(alice, bob, XRP(1)), env.seq(alice));
2670 STParsedJSONObject parsed("test", txn.getTxn());
2671 Serializer s;
2672 parsed.object->add(s); // NOLINT(bugprone-unchecked-optional-access)
2673 submitAndValidate(
2674 "No signing fields set",
2675 s.slice(),
2676 __LINE__,
2677 "fails local checks: Empty SigningPubKey.",
2678 "fails local checks: Empty SigningPubKey.",
2679 withBatch && !withInnerSigFix);
2680 }
2681
2682 // Invalid RPC Submission: tfInnerBatchTxn pseudo-transaction
2683 // - has no `TxnSignature` field
2684 // + has empty `SigningPubKey` field
2685 // - has no `Signers` field
2686 // + has `tfInnerBatchTxn` flag
2687 {
2688 STTx const amendTx(ttAMENDMENT, [seq = env.closed()->header().seq + 1](auto& obj) {
2689 obj.setAccountID(sfAccount, AccountID());
2690 obj.setFieldH256(sfAmendment, fixBatchInnerSigs);
2691 obj.setFieldU32(sfLedgerSequence, seq);
2692 obj.setFieldU32(sfFlags, tfInnerBatchTxn);
2693 });
2694 auto txn = batch::Inner(amendTx.getJson(JsonOptions::Values::None), env.seq(alice));
2695 STParsedJSONObject parsed("test", txn.getTxn());
2696 Serializer s;
2697 parsed.object->add(s); // NOLINT(bugprone-unchecked-optional-access)
2698 submitAndValidate(
2699 "Pseudo-transaction",
2700 s.slice(),
2701 __LINE__,
2702 withInnerSigFix ? "fails local checks: Empty SigningPubKey."
2703 : "fails local checks: Cannot submit pseudo transactions.",
2704 "fails local checks: Empty SigningPubKey.");
2705 }
2706 }
2707
2708 void
2710 {
2711 for (bool const withBatch : {true, false})
2712 {
2713 doTestInnerSubmitRPC(features, withBatch);
2714 }
2715 }
2716
2717 void
2719 {
2720 testcase("account activation");
2721
2722 using namespace test::jtx;
2723 using namespace std::literals;
2724
2725 test::jtx::Env env{*this, features};
2726
2727 auto const alice = Account("alice");
2728 auto const bob = Account("bob");
2729 env.fund(XRP(10000), alice);
2730 env.close();
2731 env.memoize(bob);
2732
2733 auto const preAlice = env.balance(alice);
2734 auto const ledSeq = env.current()->seq();
2735 auto const seq = env.seq(alice);
2736 auto const batchFee = batch::calcBatchFee(env, 1, 2);
2737 auto const [txIDs, batchID] = submitBatch(
2738 env,
2739 tesSUCCESS,
2740 batch::outer(alice, seq, batchFee, tfAllOrNothing),
2741 batch::Inner(pay(alice, bob, XRP(1000)), seq + 1),
2742 batch::Inner(fset(bob, asfAllowTrustLineClawback), ledSeq),
2743 batch::Sig(bob));
2744 env.close();
2745
2746 std::vector<TestLedgerData> const testCases = {
2747 {.index = 0,
2748 .txType = "Batch",
2749 .result = "tesSUCCESS",
2750 .txHash = batchID,
2751 .batchID = std::nullopt},
2752 {.index = 1,
2753 .txType = "Payment",
2754 .result = "tesSUCCESS",
2755 .txHash = txIDs[0],
2756 .batchID = batchID},
2757 {.index = 2,
2758 .txType = "AccountSet",
2759 .result = "tesSUCCESS",
2760 .txHash = txIDs[1],
2761 .batchID = batchID},
2762 };
2763 validateClosedLedger(env, testCases);
2764
2765 // Alice consumes sequences (# of txns)
2766 BEAST_EXPECT(env.seq(alice) == seq + 2);
2767
2768 // Bob consumes sequences (# of txns)
2769 BEAST_EXPECT(env.seq(bob) == ledSeq + 1);
2770
2771 // Alice pays XRP & Fee; Bob receives XRP
2772 BEAST_EXPECT(env.balance(alice) == preAlice - XRP(1000) - batchFee);
2773 BEAST_EXPECT(env.balance(bob) == XRP(1000));
2774 }
2775
2776 void
2778 {
2779 testcase("account set");
2780
2781 using namespace test::jtx;
2782 using namespace std::literals;
2783
2784 test::jtx::Env env{*this, features};
2785
2786 auto const alice = Account("alice");
2787 auto const bob = Account("bob");
2788 env.fund(XRP(10000), alice, bob);
2789 env.close();
2790
2791 auto const preAlice = env.balance(alice);
2792 auto const preBob = env.balance(bob);
2793
2794 auto const seq = env.seq(alice);
2795 auto const batchFee = batch::calcBatchFee(env, 0, 2);
2796 auto tx1 = batch::Inner(noop(alice), seq + 1);
2797 std::string domain = "example.com";
2798 tx1[sfDomain] = strHex(domain);
2799 auto const [txIDs, batchID] = submitBatch(
2800 env,
2801 tesSUCCESS,
2802 batch::outer(alice, seq, batchFee, tfAllOrNothing),
2803 tx1,
2804 batch::Inner(pay(alice, bob, XRP(1)), seq + 2));
2805 env.close();
2806
2807 std::vector<TestLedgerData> const testCases = {
2808 {.index = 0,
2809 .txType = "Batch",
2810 .result = "tesSUCCESS",
2811 .txHash = batchID,
2812 .batchID = std::nullopt},
2813 {.index = 1,
2814 .txType = "AccountSet",
2815 .result = "tesSUCCESS",
2816 .txHash = txIDs[0],
2817 .batchID = batchID},
2818 {.index = 2,
2819 .txType = "Payment",
2820 .result = "tesSUCCESS",
2821 .txHash = txIDs[1],
2822 .batchID = batchID},
2823 };
2824 validateClosedLedger(env, testCases);
2825
2826 auto const sle = env.le(keylet::account(alice));
2827 BEAST_EXPECT(sle);
2828 BEAST_EXPECT(sle->getFieldVL(sfDomain) == Blob(domain.begin(), domain.end()));
2829
2830 // Alice consumes sequences (# of txns)
2831 BEAST_EXPECT(env.seq(alice) == seq + 3);
2832
2833 // Alice pays XRP & Fee; Bob receives XRP
2834 BEAST_EXPECT(env.balance(alice) == preAlice - XRP(1) - batchFee);
2835 BEAST_EXPECT(env.balance(bob) == preBob + XRP(1));
2836 }
2837
2838 void
2840 {
2841 testcase("account delete");
2842
2843 using namespace test::jtx;
2844 using namespace std::literals;
2845
2846 // tfIndependent: account delete success
2847 {
2848 test::jtx::Env env{*this, features};
2849
2850 auto const alice = Account("alice");
2851 auto const bob = Account("bob");
2852 env.fund(XRP(10000), alice, bob);
2853 env.close();
2854
2855 incLgrSeqForAccDel(env, alice);
2856 for (int i = 0; i < 5; ++i)
2857 env.close();
2858
2859 auto const preAlice = env.balance(alice);
2860 auto const preBob = env.balance(bob);
2861
2862 auto const seq = env.seq(alice);
2863 auto const batchFee = batch::calcBatchFee(env, 0, 2) + env.current()->fees().increment;
2864 auto const [txIDs, batchID] = submitBatch(
2865 env,
2866 tesSUCCESS,
2867 batch::outer(alice, seq, batchFee, tfIndependent),
2868 batch::Inner(pay(alice, bob, XRP(1)), seq + 1),
2869 batch::Inner(acctdelete(alice, bob), seq + 2),
2870 // terNO_ACCOUNT: alice does not exist
2871 batch::Inner(pay(alice, bob, XRP(2)), seq + 3));
2872 env.close();
2873
2874 std::vector<TestLedgerData> const testCases = {
2875 {.index = 0,
2876 .txType = "Batch",
2877 .result = "tesSUCCESS",
2878 .txHash = batchID,
2879 .batchID = std::nullopt},
2880 {.index = 1,
2881 .txType = "Payment",
2882 .result = "tesSUCCESS",
2883 .txHash = txIDs[0],
2884 .batchID = batchID},
2885 {.index = 2,
2886 .txType = "AccountDelete",
2887 .result = "tesSUCCESS",
2888 .txHash = txIDs[1],
2889 .batchID = batchID},
2890 };
2891 validateClosedLedger(env, testCases);
2892
2893 // Alice does not exist; Bob receives Alice's XRP
2894 BEAST_EXPECT(!env.le(keylet::account(alice)));
2895 BEAST_EXPECT(env.balance(bob) == preBob + (preAlice - batchFee));
2896 }
2897
2898 // tfIndependent: account delete fails
2899 {
2900 test::jtx::Env env{*this, features};
2901
2902 auto const alice = Account("alice");
2903 auto const bob = Account("bob");
2904 env.fund(XRP(10000), alice, bob);
2905 env.close();
2906
2907 incLgrSeqForAccDel(env, alice);
2908 for (int i = 0; i < 5; ++i)
2909 env.close();
2910
2911 auto const preAlice = env.balance(alice);
2912 auto const preBob = env.balance(bob);
2913
2914 env.trust(bob["USD"](1000), alice);
2915 env.close();
2916
2917 auto const seq = env.seq(alice);
2918 auto const batchFee = batch::calcBatchFee(env, 0, 2) + env.current()->fees().increment;
2919 auto const [txIDs, batchID] = submitBatch(
2920 env,
2921 tesSUCCESS,
2922 batch::outer(alice, seq, batchFee, tfIndependent),
2923 batch::Inner(pay(alice, bob, XRP(1)), seq + 1),
2924 // tecHAS_OBLIGATIONS: alice has obligations
2925 batch::Inner(acctdelete(alice, bob), seq + 2),
2926 batch::Inner(pay(alice, bob, XRP(2)), seq + 3));
2927 env.close();
2928
2929 std::vector<TestLedgerData> const testCases = {
2930 {.index = 0,
2931 .txType = "Batch",
2932 .result = "tesSUCCESS",
2933 .txHash = batchID,
2934 .batchID = std::nullopt},
2935 {.index = 1,
2936 .txType = "Payment",
2937 .result = "tesSUCCESS",
2938 .txHash = txIDs[0],
2939 .batchID = batchID},
2940 {.index = 2,
2941 .txType = "AccountDelete",
2942 .result = "tecHAS_OBLIGATIONS",
2943 .txHash = txIDs[1],
2944 .batchID = batchID},
2945 {.index = 3,
2946 .txType = "Payment",
2947 .result = "tesSUCCESS",
2948 .txHash = txIDs[2],
2949 .batchID = batchID},
2950 };
2951 validateClosedLedger(env, testCases);
2952
2953 // Alice does not exist; Bob receives XRP
2954 BEAST_EXPECT(env.le(keylet::account(alice)));
2955 BEAST_EXPECT(env.balance(bob) == preBob + XRP(3));
2956 }
2957
2958 // tfAllOrNothing: account delete fails
2959 {
2960 test::jtx::Env env{*this, features};
2961
2962 auto const alice = Account("alice");
2963 auto const bob = Account("bob");
2964 env.fund(XRP(10000), alice, bob);
2965 env.close();
2966
2967 incLgrSeqForAccDel(env, alice);
2968 for (int i = 0; i < 5; ++i)
2969 env.close();
2970
2971 auto const preAlice = env.balance(alice);
2972 auto const preBob = env.balance(bob);
2973
2974 auto const seq = env.seq(alice);
2975 auto const batchFee = batch::calcBatchFee(env, 0, 2) + env.current()->fees().increment;
2976 auto const [txIDs, batchID] = submitBatch(
2977 env,
2978 tesSUCCESS,
2979 batch::outer(alice, seq, batchFee, tfAllOrNothing),
2980 batch::Inner(pay(alice, bob, XRP(1)), seq + 1),
2981 batch::Inner(acctdelete(alice, bob), seq + 2),
2982 // terNO_ACCOUNT: alice does not exist
2983 batch::Inner(pay(alice, bob, XRP(2)), seq + 3));
2984 env.close();
2985
2986 std::vector<TestLedgerData> const testCases = {
2987 {.index = 0,
2988 .txType = "Batch",
2989 .result = "tesSUCCESS",
2990 .txHash = batchID,
2991 .batchID = std::nullopt},
2992 };
2993 validateClosedLedger(env, testCases);
2994
2995 // Alice still exists; Bob is unchanged
2996 BEAST_EXPECT(env.le(keylet::account(alice)));
2997 BEAST_EXPECT(env.balance(bob) == preBob);
2998 }
2999 }
3000
3001 void
3003 {
3004 testcase("loan");
3005
3006 bool const lendingBatchEnabled = !std::ranges::any_of(
3008 [](auto const& disabled) { return disabled == ttLOAN_BROKER_SET; });
3009
3010 using namespace test::jtx;
3011
3012 test::jtx::Env env{*this, features};
3013
3014 Account const issuer{"issuer"};
3015 // For simplicity, lender will be the sole actor for the vault &
3016 // brokers.
3017 Account const lender{"lender"};
3018 // Borrower only wants to borrow
3019 Account const borrower{"borrower"};
3020
3021 // Fund the accounts and trust lines with the same amount so that tests
3022 // can use the same values regardless of the asset.
3023 env.fund(XRP(100'000), issuer, noripple(lender, borrower));
3024 env.close();
3025
3026 // Just use an XRP asset
3027 PrettyAsset const asset{xrpIssue(), 1'000'000};
3028
3029 Vault const vault{env};
3030
3031 auto const deposit = asset(50'000);
3032 auto const debtMaximumValue = asset(25'000).value();
3033 auto const coverDepositValue = asset(1000).value();
3034
3035 auto [tx, vaultKeylet] = vault.create({.owner = lender, .asset = asset});
3036 env(tx);
3037 env.close();
3038 BEAST_EXPECT(env.le(vaultKeylet));
3039
3040 env(vault.deposit({.depositor = lender, .id = vaultKeylet.key, .amount = deposit}));
3041 env.close();
3042
3043 auto const brokerKeylet = keylet::loanBroker(lender.id(), env.seq(lender));
3044
3045 {
3046 using namespace loanBroker;
3047 env(set(lender, vaultKeylet.key),
3048 kManagementFeeRate(TenthBips16(100)),
3049 kDebtMaximum(debtMaximumValue),
3050 kCoverRateMinimum(TenthBips32(percentageToTenthBips(10))),
3051 kCoverRateLiquidation(TenthBips32(percentageToTenthBips(25))));
3052
3053 env(coverDeposit(lender, brokerKeylet.key, coverDepositValue));
3054
3055 env.close();
3056 }
3057
3058 {
3059 using namespace loan;
3060 using namespace std::chrono_literals;
3061
3062 auto const lenderSeq = env.seq(lender);
3063 auto const batchFee = batch::calcBatchFee(env, 0, 2);
3064
3065 auto const loanKeylet = keylet::loan(brokerKeylet.key, 1);
3066 {
3067 auto const [txIDs, batchID] = submitBatch(
3068 env,
3069 lendingBatchEnabled ? temBAD_SIGNATURE : temINVALID_INNER_BATCH,
3070 batch::outer(lender, lenderSeq, batchFee, tfAllOrNothing),
3072 env.json(
3073 set(lender, brokerKeylet.key, asset(1000).value()),
3074 // Not allowed to include the counterparty signature
3075 Sig(sfCounterpartySignature, borrower),
3076 Sig(kNone),
3077 Fee(kNone),
3078 Seq(kNone)),
3079 lenderSeq + 1),
3081 pay(lender, loanKeylet.key, STAmount{asset, asset(500).value()}),
3082 lenderSeq + 2));
3083 }
3084 {
3085 auto const [txIDs, batchID] = submitBatch(
3086 env,
3088 batch::outer(lender, lenderSeq, batchFee, tfAllOrNothing),
3090 env.json(
3091 set(lender, brokerKeylet.key, asset(1000).value()),
3092 // Counterparty must be set
3093 Sig(kNone),
3094 Fee(kNone),
3095 Seq(kNone)),
3096 lenderSeq + 1),
3098 pay(lender, loanKeylet.key, STAmount{asset, asset(500).value()}),
3099 lenderSeq + 2));
3100 }
3101 {
3102 auto const [txIDs, batchID] = submitBatch(
3103 env,
3104 lendingBatchEnabled ? temBAD_SIGNER : temINVALID_INNER_BATCH,
3105 batch::outer(lender, lenderSeq, batchFee, tfAllOrNothing),
3107 env.json(
3108 set(lender, brokerKeylet.key, asset(1000).value()),
3109 // Counterparty must sign the outer transaction
3110 kCounterparty(borrower.id()),
3111 Sig(kNone),
3112 Fee(kNone),
3113 Seq(kNone)),
3114 lenderSeq + 1),
3116 pay(lender, loanKeylet.key, STAmount{asset, asset(500).value()}),
3117 lenderSeq + 2));
3118 }
3119 {
3120 // LoanSet normally charges at least 2x base fee, but since the
3121 // signature check is done by the batch, it only charges the
3122 // base fee.
3123 auto const batchFee = batch::calcBatchFee(env, 1, 2);
3124 auto const [txIDs, batchID] = submitBatch(
3125 env,
3126 lendingBatchEnabled ? TER(tesSUCCESS) : TER(temINVALID_INNER_BATCH),
3127 batch::outer(lender, lenderSeq, batchFee, tfAllOrNothing),
3129 env.json(
3130 set(lender, brokerKeylet.key, asset(1000).value()),
3131 kCounterparty(borrower.id()),
3132 Sig(kNone),
3133 Fee(kNone),
3134 Seq(kNone)),
3135 lenderSeq + 1),
3137 pay(
3138 // However, this inner transaction will fail,
3139 // because the lender is not allowed to draw the
3140 // transaction
3141 lender,
3142 loanKeylet.key,
3143 STAmount{asset, asset(500).value()}),
3144 lenderSeq + 2),
3145 batch::Sig(borrower));
3146 }
3147 env.close();
3148 BEAST_EXPECT(env.le(brokerKeylet));
3149 BEAST_EXPECT(!env.le(loanKeylet));
3150 {
3151 // LoanSet normally charges at least 2x base fee, but since the
3152 // signature check is done by the batch, it only charges the
3153 // base fee.
3154 auto const lenderSeq = env.seq(lender);
3155 auto const batchFee = batch::calcBatchFee(env, 1, 2);
3156 auto const [txIDs, batchID] = submitBatch(
3157 env,
3158 lendingBatchEnabled ? TER(tesSUCCESS) : TER(temINVALID_INNER_BATCH),
3159 batch::outer(lender, lenderSeq, batchFee, tfAllOrNothing),
3161 env.json(
3162 set(lender, brokerKeylet.key, asset(1000).value()),
3163 kCounterparty(borrower.id()),
3164 Sig(kNone),
3165 Fee(kNone),
3166 Seq(kNone)),
3167 lenderSeq + 1),
3168 batch::Inner(manage(lender, loanKeylet.key, tfLoanImpair), lenderSeq + 2),
3169 batch::Sig(borrower));
3170 }
3171 env.close();
3172 BEAST_EXPECT(env.le(brokerKeylet));
3173 if (auto const sleLoan = env.le(loanKeylet);
3174 lendingBatchEnabled ? BEAST_EXPECT(sleLoan) : !BEAST_EXPECT(!sleLoan))
3175 {
3176 BEAST_EXPECT(sleLoan->isFlag(lsfLoanImpaired));
3177 }
3178 }
3179 }
3180
3181 void
3183 {
3184 testcase("object create w/ sequence");
3185
3186 using namespace test::jtx;
3187 using namespace std::literals;
3188
3189 test::jtx::Env env{*this, features};
3190
3191 auto const alice = Account("alice");
3192 auto const bob = Account("bob");
3193 auto const gw = Account("gw");
3194 auto const usd = gw["USD"];
3195
3196 env.fund(XRP(10000), alice, bob, gw);
3197 env.close();
3198
3199 env.trust(usd(1000), alice, bob);
3200 env(pay(gw, alice, usd(100)));
3201 env(pay(gw, bob, usd(100)));
3202 env.close();
3203
3204 // success
3205 {
3206 auto const aliceSeq = env.seq(alice);
3207 auto const bobSeq = env.seq(bob);
3208 auto const preAlice = env.balance(alice);
3209 auto const preBob = env.balance(bob);
3210 auto const preAliceUSD = env.balance(alice, usd.issue());
3211 auto const preBobUSD = env.balance(bob, usd.issue());
3212
3213 auto const batchFee = batch::calcBatchFee(env, 1, 2);
3214 uint256 const chkID{getCheckIndex(bob, env.seq(bob))};
3215 auto const [txIDs, batchID] = submitBatch(
3216 env,
3217 tesSUCCESS,
3218 batch::outer(alice, aliceSeq, batchFee, tfAllOrNothing),
3219 batch::Inner(check::create(bob, alice, usd(10)), bobSeq),
3220 batch::Inner(check::cash(alice, chkID, usd(10)), aliceSeq + 1),
3221 batch::Sig(bob));
3222 env.close();
3223
3224 std::vector<TestLedgerData> const testCases = {
3225 {.index = 0,
3226 .txType = "Batch",
3227 .result = "tesSUCCESS",
3228 .txHash = batchID,
3229 .batchID = std::nullopt},
3230 {.index = 1,
3231 .txType = "CheckCreate",
3232 .result = "tesSUCCESS",
3233 .txHash = txIDs[0],
3234 .batchID = batchID},
3235 {.index = 2,
3236 .txType = "CheckCash",
3237 .result = "tesSUCCESS",
3238 .txHash = txIDs[1],
3239 .batchID = batchID},
3240 };
3241 validateClosedLedger(env, testCases);
3242
3243 // Alice consumes sequences (# of txns)
3244 BEAST_EXPECT(env.seq(alice) == aliceSeq + 2);
3245
3246 // Alice consumes sequences (# of txns)
3247 BEAST_EXPECT(env.seq(bob) == bobSeq + 1);
3248
3249 // Alice pays Fee; Bob XRP Unchanged
3250 BEAST_EXPECT(env.balance(alice) == preAlice - batchFee);
3251 BEAST_EXPECT(env.balance(bob) == preBob);
3252
3253 // Alice pays USD & Bob receives USD
3254 BEAST_EXPECT(env.balance(alice, usd.issue()) == preAliceUSD + usd(10));
3255 BEAST_EXPECT(env.balance(bob, usd.issue()) == preBobUSD - usd(10));
3256 }
3257
3258 // failure
3259 {
3260 env(fset(alice, asfRequireDest));
3261 env.close();
3262
3263 auto const aliceSeq = env.seq(alice);
3264 auto const bobSeq = env.seq(bob);
3265 auto const preAlice = env.balance(alice);
3266 auto const preBob = env.balance(bob);
3267 auto const preAliceUSD = env.balance(alice, usd.issue());
3268 auto const preBobUSD = env.balance(bob, usd.issue());
3269
3270 auto const batchFee = batch::calcBatchFee(env, 1, 2);
3271 uint256 const chkID{getCheckIndex(bob, env.seq(bob))};
3272 auto const [txIDs, batchID] = submitBatch(
3273 env,
3274 tesSUCCESS,
3275 batch::outer(alice, aliceSeq, batchFee, tfIndependent),
3276 // tecDST_TAG_NEEDED - alice has enabled asfRequireDest
3277 batch::Inner(check::create(bob, alice, usd(10)), bobSeq),
3278 batch::Inner(check::cash(alice, chkID, usd(10)), aliceSeq + 1),
3279 batch::Sig(bob));
3280 env.close();
3281
3282 std::vector<TestLedgerData> const testCases = {
3283 {.index = 0,
3284 .txType = "Batch",
3285 .result = "tesSUCCESS",
3286 .txHash = batchID,
3287 .batchID = std::nullopt},
3288 {.index = 1,
3289 .txType = "CheckCreate",
3290 .result = "tecDST_TAG_NEEDED",
3291 .txHash = txIDs[0],
3292 .batchID = batchID},
3293 {.index = 2,
3294 .txType = "CheckCash",
3295 .result = "tecNO_ENTRY",
3296 .txHash = txIDs[1],
3297 .batchID = batchID},
3298 };
3299 validateClosedLedger(env, testCases);
3300
3301 // Alice consumes sequences (# of txns)
3302 BEAST_EXPECT(env.seq(alice) == aliceSeq + 2);
3303
3304 // Bob consumes sequences (# of txns)
3305 BEAST_EXPECT(env.seq(bob) == bobSeq + 1);
3306
3307 // Alice pays Fee; Bob XRP Unchanged
3308 BEAST_EXPECT(env.balance(alice) == preAlice - batchFee);
3309 BEAST_EXPECT(env.balance(bob) == preBob);
3310
3311 // Alice pays USD & Bob receives USD
3312 BEAST_EXPECT(env.balance(alice, usd.issue()) == preAliceUSD);
3313 BEAST_EXPECT(env.balance(bob, usd.issue()) == preBobUSD);
3314 }
3315 }
3316
3317 void
3319 {
3320 testcase("object create w/ ticket");
3321
3322 using namespace test::jtx;
3323 using namespace std::literals;
3324
3325 test::jtx::Env env{*this, features};
3326
3327 auto const alice = Account("alice");
3328 auto const bob = Account("bob");
3329 auto const gw = Account("gw");
3330 auto const usd = gw["USD"];
3331
3332 env.fund(XRP(10000), alice, bob, gw);
3333 env.close();
3334
3335 env.trust(usd(1000), alice, bob);
3336 env(pay(gw, alice, usd(100)));
3337 env(pay(gw, bob, usd(100)));
3338 env.close();
3339
3340 auto const aliceSeq = env.seq(alice);
3341 auto const bobSeq = env.seq(bob);
3342 auto const preAlice = env.balance(alice);
3343 auto const preBob = env.balance(bob);
3344 auto const preAliceUSD = env.balance(alice, usd.issue());
3345 auto const preBobUSD = env.balance(bob, usd.issue());
3346
3347 auto const batchFee = batch::calcBatchFee(env, 1, 3);
3348 uint256 const chkID{getCheckIndex(bob, bobSeq + 1)};
3349 auto const [txIDs, batchID] = submitBatch(
3350 env,
3351 tesSUCCESS,
3352 batch::outer(alice, aliceSeq, batchFee, tfAllOrNothing),
3353 batch::Inner(ticket::create(bob, 10), bobSeq),
3354 batch::Inner(check::create(bob, alice, usd(10)), 0, bobSeq + 1),
3355 batch::Inner(check::cash(alice, chkID, usd(10)), aliceSeq + 1),
3356 batch::Sig(bob));
3357 env.close();
3358
3359 std::vector<TestLedgerData> const testCases = {
3360 {.index = 0,
3361 .txType = "Batch",
3362 .result = "tesSUCCESS",
3363 .txHash = batchID,
3364 .batchID = std::nullopt},
3365 {.index = 1,
3366 .txType = "TicketCreate",
3367 .result = "tesSUCCESS",
3368 .txHash = txIDs[0],
3369 .batchID = batchID},
3370 {.index = 2,
3371 .txType = "CheckCreate",
3372 .result = "tesSUCCESS",
3373 .txHash = txIDs[1],
3374 .batchID = batchID},
3375 {.index = 3,
3376 .txType = "CheckCash",
3377 .result = "tesSUCCESS",
3378 .txHash = txIDs[2],
3379 .batchID = batchID},
3380 };
3381 validateClosedLedger(env, testCases);
3382
3383 BEAST_EXPECT(env.seq(alice) == aliceSeq + 2);
3384 BEAST_EXPECT(env.seq(bob) == bobSeq + 10 + 1);
3385 BEAST_EXPECT(env.balance(alice) == preAlice - batchFee);
3386 BEAST_EXPECT(env.balance(bob) == preBob);
3387 BEAST_EXPECT(env.balance(alice, usd.issue()) == preAliceUSD + usd(10));
3388 BEAST_EXPECT(env.balance(bob, usd.issue()) == preBobUSD - usd(10));
3389 }
3390
3391 void
3393 {
3394 testcase("object create w/ 3rd party");
3395
3396 using namespace test::jtx;
3397 using namespace std::literals;
3398
3399 test::jtx::Env env{*this, features};
3400
3401 auto const alice = Account("alice");
3402 auto const bob = Account("bob");
3403 auto const carol = Account("carol");
3404 auto const gw = Account("gw");
3405 auto const usd = gw["USD"];
3406
3407 env.fund(XRP(10000), alice, bob, carol, gw);
3408 env.close();
3409
3410 env.trust(usd(1000), alice, bob);
3411 env(pay(gw, alice, usd(100)));
3412 env(pay(gw, bob, usd(100)));
3413 env.close();
3414
3415 auto const aliceSeq = env.seq(alice);
3416 auto const bobSeq = env.seq(bob);
3417 auto const carolSeq = env.seq(carol);
3418 auto const preAlice = env.balance(alice);
3419 auto const preBob = env.balance(bob);
3420 auto const preCarol = env.balance(carol);
3421 auto const preAliceUSD = env.balance(alice, usd.issue());
3422 auto const preBobUSD = env.balance(bob, usd.issue());
3423
3424 auto const batchFee = batch::calcBatchFee(env, 2, 2);
3425 uint256 const chkID{getCheckIndex(bob, env.seq(bob))};
3426 auto const [txIDs, batchID] = submitBatch(
3427 env,
3428 tesSUCCESS,
3429 batch::outer(carol, carolSeq, batchFee, tfAllOrNothing),
3430 batch::Inner(check::create(bob, alice, usd(10)), bobSeq),
3431 batch::Inner(check::cash(alice, chkID, usd(10)), aliceSeq),
3432 batch::Sig(alice, bob));
3433 env.close();
3434
3435 std::vector<TestLedgerData> const testCases = {
3436 {.index = 0,
3437 .txType = "Batch",
3438 .result = "tesSUCCESS",
3439 .txHash = batchID,
3440 .batchID = std::nullopt},
3441 {.index = 1,
3442 .txType = "CheckCreate",
3443 .result = "tesSUCCESS",
3444 .txHash = txIDs[0],
3445 .batchID = batchID},
3446 {.index = 2,
3447 .txType = "CheckCash",
3448 .result = "tesSUCCESS",
3449 .txHash = txIDs[1],
3450 .batchID = batchID},
3451 };
3452 validateClosedLedger(env, testCases);
3453
3454 BEAST_EXPECT(env.seq(alice) == aliceSeq + 1);
3455 BEAST_EXPECT(env.seq(bob) == bobSeq + 1);
3456 BEAST_EXPECT(env.seq(carol) == carolSeq + 1);
3457 BEAST_EXPECT(env.balance(alice) == preAlice);
3458 BEAST_EXPECT(env.balance(bob) == preBob);
3459 BEAST_EXPECT(env.balance(carol) == preCarol - batchFee);
3460 BEAST_EXPECT(env.balance(alice, usd.issue()) == preAliceUSD + usd(10));
3461 BEAST_EXPECT(env.balance(bob, usd.issue()) == preBobUSD - usd(10));
3462 }
3463
3464 void
3466 {
3467 {
3468 testcase("tickets outer");
3469
3470 using namespace test::jtx;
3471 using namespace std::literals;
3472
3473 test::jtx::Env env{*this, features};
3474
3475 auto const alice = Account("alice");
3476 auto const bob = Account("bob");
3477
3478 env.fund(XRP(10000), alice, bob);
3479 env.close();
3480
3481 std::uint32_t const aliceTicketSeq{env.seq(alice) + 1};
3482 env(ticket::create(alice, 10));
3483 env.close();
3484
3485 auto const aliceSeq = env.seq(alice);
3486 auto const preAlice = env.balance(alice);
3487 auto const preBob = env.balance(bob);
3488
3489 auto const batchFee = batch::calcBatchFee(env, 0, 2);
3490 auto const [txIDs, batchID] = submitBatch(
3491 env,
3492 tesSUCCESS,
3493 batch::outer(alice, 0, batchFee, tfAllOrNothing),
3494 batch::Inner(pay(alice, bob, XRP(1)), aliceSeq + 0),
3495 batch::Inner(pay(alice, bob, XRP(2)), aliceSeq + 1),
3496 ticket::Use(aliceTicketSeq));
3497 env.close();
3498
3499 std::vector<TestLedgerData> const testCases = {
3500 {.index = 0,
3501 .txType = "Batch",
3502 .result = "tesSUCCESS",
3503 .txHash = batchID,
3504 .batchID = std::nullopt},
3505 {.index = 1,
3506 .txType = "Payment",
3507 .result = "tesSUCCESS",
3508 .txHash = txIDs[0],
3509 .batchID = batchID},
3510 {.index = 2,
3511 .txType = "Payment",
3512 .result = "tesSUCCESS",
3513 .txHash = txIDs[1],
3514 .batchID = batchID},
3515 };
3516 validateClosedLedger(env, testCases);
3517
3518 auto const sle = env.le(keylet::account(alice));
3519 BEAST_EXPECT(sle);
3520 BEAST_EXPECT(sle->getFieldU32(sfOwnerCount) == 9);
3521 BEAST_EXPECT(sle->getFieldU32(sfTicketCount) == 9);
3522
3523 BEAST_EXPECT(env.seq(alice) == aliceSeq + 2);
3524 BEAST_EXPECT(env.balance(alice) == preAlice - XRP(3) - batchFee);
3525 BEAST_EXPECT(env.balance(bob) == preBob + XRP(3));
3526 }
3527
3528 {
3529 testcase("tickets inner");
3530
3531 using namespace test::jtx;
3532 using namespace std::literals;
3533
3534 test::jtx::Env env{*this, features};
3535
3536 auto const alice = Account("alice");
3537 auto const bob = Account("bob");
3538
3539 env.fund(XRP(10000), alice, bob);
3540 env.close();
3541
3542 std::uint32_t aliceTicketSeq{env.seq(alice) + 1};
3543 env(ticket::create(alice, 10));
3544 env.close();
3545
3546 auto const aliceSeq = env.seq(alice);
3547 auto const preAlice = env.balance(alice);
3548 auto const preBob = env.balance(bob);
3549
3550 auto const batchFee = batch::calcBatchFee(env, 0, 2);
3551 auto const [txIDs, batchID] = submitBatch(
3552 env,
3553 tesSUCCESS,
3554 batch::outer(alice, aliceSeq, batchFee, tfAllOrNothing),
3555 batch::Inner(pay(alice, bob, XRP(1)), 0, aliceTicketSeq),
3556 batch::Inner(pay(alice, bob, XRP(2)), 0, aliceTicketSeq + 1));
3557 env.close();
3558
3559 std::vector<TestLedgerData> const testCases = {
3560 {.index = 0,
3561 .txType = "Batch",
3562 .result = "tesSUCCESS",
3563 .txHash = batchID,
3564 .batchID = std::nullopt},
3565 {.index = 1,
3566 .txType = "Payment",
3567 .result = "tesSUCCESS",
3568 .txHash = txIDs[0],
3569 .batchID = batchID},
3570 {.index = 2,
3571 .txType = "Payment",
3572 .result = "tesSUCCESS",
3573 .txHash = txIDs[1],
3574 .batchID = batchID},
3575 };
3576 validateClosedLedger(env, testCases);
3577
3578 auto const sle = env.le(keylet::account(alice));
3579 BEAST_EXPECT(sle);
3580 BEAST_EXPECT(sle->getFieldU32(sfOwnerCount) == 8);
3581 BEAST_EXPECT(sle->getFieldU32(sfTicketCount) == 8);
3582
3583 BEAST_EXPECT(env.seq(alice) == aliceSeq + 1);
3584 BEAST_EXPECT(env.balance(alice) == preAlice - XRP(3) - batchFee);
3585 BEAST_EXPECT(env.balance(bob) == preBob + XRP(3));
3586 }
3587
3588 {
3589 testcase("tickets outer inner");
3590
3591 using namespace test::jtx;
3592 using namespace std::literals;
3593
3594 test::jtx::Env env{*this, features};
3595
3596 auto const alice = Account("alice");
3597 auto const bob = Account("bob");
3598
3599 env.fund(XRP(10000), alice, bob);
3600 env.close();
3601
3602 std::uint32_t const aliceTicketSeq{env.seq(alice) + 1};
3603 env(ticket::create(alice, 10));
3604 env.close();
3605
3606 auto const aliceSeq = env.seq(alice);
3607 auto const preAlice = env.balance(alice);
3608 auto const preBob = env.balance(bob);
3609
3610 auto const batchFee = batch::calcBatchFee(env, 0, 2);
3611 auto const [txIDs, batchID] = submitBatch(
3612 env,
3613 tesSUCCESS,
3614 batch::outer(alice, 0, batchFee, tfAllOrNothing),
3615 batch::Inner(pay(alice, bob, XRP(1)), 0, aliceTicketSeq + 1),
3616 batch::Inner(pay(alice, bob, XRP(2)), aliceSeq),
3617 ticket::Use(aliceTicketSeq));
3618 env.close();
3619
3620 std::vector<TestLedgerData> const testCases = {
3621 {.index = 0,
3622 .txType = "Batch",
3623 .result = "tesSUCCESS",
3624 .txHash = batchID,
3625 .batchID = std::nullopt},
3626 {.index = 1,
3627 .txType = "Payment",
3628 .result = "tesSUCCESS",
3629 .txHash = txIDs[0],
3630 .batchID = batchID},
3631 {.index = 2,
3632 .txType = "Payment",
3633 .result = "tesSUCCESS",
3634 .txHash = txIDs[1],
3635 .batchID = batchID},
3636 };
3637 validateClosedLedger(env, testCases);
3638
3639 auto const sle = env.le(keylet::account(alice));
3640 BEAST_EXPECT(sle);
3641 BEAST_EXPECT(sle->getFieldU32(sfOwnerCount) == 8);
3642 BEAST_EXPECT(sle->getFieldU32(sfTicketCount) == 8);
3643
3644 BEAST_EXPECT(env.seq(alice) == aliceSeq + 1);
3645 BEAST_EXPECT(env.balance(alice) == preAlice - XRP(3) - batchFee);
3646 BEAST_EXPECT(env.balance(bob) == preBob + XRP(3));
3647 }
3648 }
3649
3650 void
3652 {
3653 testcase("sequence open ledger");
3654
3655 using namespace test::jtx;
3656 using namespace std::literals;
3657
3658 auto const alice = Account("alice");
3659 auto const bob = Account("bob");
3660 auto const carol = Account("carol");
3661
3662 // Before Batch Txn w/ retry following ledger
3663 {
3664 // IMPORTANT: The batch txn is applied first, then the noop txn.
3665 // Because of this ordering, the noop txn is not applied and is
3666 // overwritten by the payment in the batch transaction. Because the
3667 // terPRE_SEQ is outside of the batch this noop transaction will ge
3668 // reapplied in the following ledger
3669 test::jtx::Env env{*this, features};
3670 env.fund(XRP(10000), alice, bob, carol);
3671 env.close();
3672
3673 auto const aliceSeq = env.seq(alice);
3674 auto const carolSeq = env.seq(carol);
3675
3676 // AccountSet Txn
3677 auto const noopTxn = env.jt(noop(alice), Seq(aliceSeq + 2));
3678 auto const noopTxnID = to_string(noopTxn.stx->getTransactionID());
3679 env(noopTxn, Ter(terPRE_SEQ));
3680
3681 // Batch Txn
3682 auto const batchFee = batch::calcBatchFee(env, 1, 2);
3683 auto const [txIDs, batchID] = submitBatch(
3684 env,
3685 tesSUCCESS,
3686 batch::outer(carol, carolSeq, batchFee, tfAllOrNothing),
3687 batch::Inner(pay(alice, bob, XRP(1)), aliceSeq),
3688 batch::Inner(pay(alice, bob, XRP(2)), aliceSeq + 1),
3689 batch::Sig(alice));
3690 env.close();
3691
3692 {
3693 std::vector<TestLedgerData> const testCases = {
3694 {.index = 0,
3695 .txType = "Batch",
3696 .result = "tesSUCCESS",
3697 .txHash = batchID,
3698 .batchID = std::nullopt},
3699 {.index = 1,
3700 .txType = "Payment",
3701 .result = "tesSUCCESS",
3702 .txHash = txIDs[0],
3703 .batchID = batchID},
3704 {.index = 2,
3705 .txType = "Payment",
3706 .result = "tesSUCCESS",
3707 .txHash = txIDs[1],
3708 .batchID = batchID},
3709 };
3710 validateClosedLedger(env, testCases);
3711 }
3712
3713 env.close();
3714 {
3715 // next ledger contains noop txn
3716 std::vector<TestLedgerData> const testCases = {
3717 {.index = 0,
3718 .txType = "AccountSet",
3719 .result = "tesSUCCESS",
3720 .txHash = noopTxnID,
3721 .batchID = std::nullopt},
3722 };
3723 validateClosedLedger(env, testCases);
3724 }
3725 }
3726
3727 // Before Batch Txn w/ same sequence
3728 {
3729 // IMPORTANT: The batch txn is applied first, then the noop txn.
3730 // Because of this ordering, the noop txn is not applied and is
3731 // overwritten by the payment in the batch transaction.
3732 test::jtx::Env env{*this, features};
3733 env.fund(XRP(10000), alice, bob);
3734 env.close();
3735
3736 auto const aliceSeq = env.seq(alice);
3737
3738 // AccountSet Txn
3739 auto const noopTxn = env.jt(noop(alice), Seq(aliceSeq + 1));
3740 env(noopTxn, Ter(terPRE_SEQ));
3741
3742 // Batch Txn
3743 auto const batchFee = batch::calcBatchFee(env, 0, 2);
3744 auto const [txIDs, batchID] = submitBatch(
3745 env,
3746 tesSUCCESS,
3747 batch::outer(alice, aliceSeq, batchFee, tfAllOrNothing),
3748 batch::Inner(pay(alice, bob, XRP(1)), aliceSeq + 1),
3749 batch::Inner(pay(alice, bob, XRP(2)), aliceSeq + 2));
3750 env.close();
3751
3752 {
3753 std::vector<TestLedgerData> const testCases = {
3754 {.index = 0,
3755 .txType = "Batch",
3756 .result = "tesSUCCESS",
3757 .txHash = batchID,
3758 .batchID = std::nullopt},
3759 {.index = 1,
3760 .txType = "Payment",
3761 .result = "tesSUCCESS",
3762 .txHash = txIDs[0],
3763 .batchID = batchID},
3764 {.index = 2,
3765 .txType = "Payment",
3766 .result = "tesSUCCESS",
3767 .txHash = txIDs[1],
3768 .batchID = batchID},
3769 };
3770 validateClosedLedger(env, testCases);
3771 }
3772
3773 env.close();
3774 {
3775 // next ledger is empty
3776 std::vector<TestLedgerData> const testCases = {};
3777 validateClosedLedger(env, testCases);
3778 }
3779 }
3780
3781 // After Batch Txn w/ same sequence
3782 {
3783 // IMPORTANT: The batch txn is applied first, then the noop txn.
3784 // Because of this ordering, the noop txn is not applied and is
3785 // overwritten by the payment in the batch transaction.
3786 test::jtx::Env env{*this, features};
3787 env.fund(XRP(10000), alice, bob);
3788 env.close();
3789
3790 auto const aliceSeq = env.seq(alice);
3791 auto const batchFee = batch::calcBatchFee(env, 0, 2);
3792 auto const [txIDs, batchID] = submitBatch(
3793 env,
3794 tesSUCCESS,
3795 batch::outer(alice, aliceSeq, batchFee, tfAllOrNothing),
3796 batch::Inner(pay(alice, bob, XRP(1)), aliceSeq + 1),
3797 batch::Inner(pay(alice, bob, XRP(2)), aliceSeq + 2));
3798
3799 auto const noopTxn = env.jt(noop(alice), Seq(aliceSeq + 1));
3800 env(noopTxn, Ter(tesSUCCESS));
3801 env.close();
3802
3803 {
3804 std::vector<TestLedgerData> const testCases = {
3805 {.index = 0,
3806 .txType = "Batch",
3807 .result = "tesSUCCESS",
3808 .txHash = batchID,
3809 .batchID = std::nullopt},
3810 {.index = 1,
3811 .txType = "Payment",
3812 .result = "tesSUCCESS",
3813 .txHash = txIDs[0],
3814 .batchID = batchID},
3815 {.index = 2,
3816 .txType = "Payment",
3817 .result = "tesSUCCESS",
3818 .txHash = txIDs[1],
3819 .batchID = batchID},
3820 };
3821 validateClosedLedger(env, testCases);
3822 }
3823
3824 env.close();
3825 {
3826 // next ledger is empty
3827 std::vector<TestLedgerData> const testCases = {};
3828 validateClosedLedger(env, testCases);
3829 }
3830 }
3831
3832 // Outer Batch terPRE_SEQ
3833 {
3834 test::jtx::Env env{*this, features};
3835 env.fund(XRP(10000), alice, bob, carol);
3836 env.close();
3837
3838 auto const aliceSeq = env.seq(alice);
3839 auto const carolSeq = env.seq(carol);
3840
3841 // Batch Txn
3842 auto const batchFee = batch::calcBatchFee(env, 1, 2);
3843 auto const [txIDs, batchID] = submitBatch(
3844 env,
3845 terPRE_SEQ,
3846 batch::outer(carol, carolSeq + 1, batchFee, tfAllOrNothing),
3847 batch::Inner(pay(alice, bob, XRP(1)), aliceSeq),
3848 batch::Inner(pay(alice, bob, XRP(2)), aliceSeq + 1),
3849 batch::Sig(alice));
3850
3851 // AccountSet Txn
3852 auto const noopTxn = env.jt(noop(carol), Seq(carolSeq));
3853 auto const noopTxnID = to_string(noopTxn.stx->getTransactionID());
3854 env(noopTxn, Ter(tesSUCCESS));
3855 env.close();
3856
3857 {
3858 std::vector<TestLedgerData> const testCases = {
3859 {.index = 0,
3860 .txType = "AccountSet",
3861 .result = "tesSUCCESS",
3862 .txHash = noopTxnID,
3863 .batchID = std::nullopt},
3864 {.index = 1,
3865 .txType = "Batch",
3866 .result = "tesSUCCESS",
3867 .txHash = batchID,
3868 .batchID = std::nullopt},
3869 {.index = 2,
3870 .txType = "Payment",
3871 .result = "tesSUCCESS",
3872 .txHash = txIDs[0],
3873 .batchID = batchID},
3874 {.index = 3,
3875 .txType = "Payment",
3876 .result = "tesSUCCESS",
3877 .txHash = txIDs[1],
3878 .batchID = batchID},
3879 };
3880 validateClosedLedger(env, testCases);
3881 }
3882
3883 env.close();
3884 {
3885 // next ledger contains no transactions
3886 std::vector<TestLedgerData> const testCases = {};
3887 validateClosedLedger(env, testCases);
3888 }
3889 }
3890 }
3891
3892 void
3894 {
3895 testcase("tickets open ledger");
3896
3897 using namespace test::jtx;
3898 using namespace std::literals;
3899
3900 auto const alice = Account("alice");
3901 auto const bob = Account("bob");
3902
3903 // Before Batch Txn w/ same ticket
3904 {
3905 // IMPORTANT: The batch txn is applied first, then the noop txn.
3906 // Because of this ordering, the noop txn is not applied and is
3907 // overwritten by the payment in the batch transaction.
3908 test::jtx::Env env{*this, features};
3909 env.fund(XRP(10000), alice, bob);
3910 env.close();
3911
3912 std::uint32_t const aliceTicketSeq{env.seq(alice) + 1};
3913 env(ticket::create(alice, 10));
3914 env.close();
3915
3916 auto const aliceSeq = env.seq(alice);
3917
3918 // AccountSet Txn
3919 auto const noopTxn = env.jt(noop(alice), ticket::Use(aliceTicketSeq + 1));
3920 env(noopTxn, Ter(tesSUCCESS));
3921
3922 // Batch Txn
3923 auto const batchFee = batch::calcBatchFee(env, 0, 2);
3924 auto const [txIDs, batchID] = submitBatch(
3925 env,
3926 tesSUCCESS,
3927 batch::outer(alice, 0, batchFee, tfAllOrNothing),
3928 batch::Inner(pay(alice, bob, XRP(1)), 0, aliceTicketSeq + 1),
3929 batch::Inner(pay(alice, bob, XRP(2)), aliceSeq),
3930 ticket::Use(aliceTicketSeq));
3931 env.close();
3932
3933 {
3934 std::vector<TestLedgerData> const testCases = {
3935 {.index = 0,
3936 .txType = "Batch",
3937 .result = "tesSUCCESS",
3938 .txHash = batchID,
3939 .batchID = std::nullopt},
3940 {.index = 1,
3941 .txType = "Payment",
3942 .result = "tesSUCCESS",
3943 .txHash = txIDs[0],
3944 .batchID = batchID},
3945 {.index = 2,
3946 .txType = "Payment",
3947 .result = "tesSUCCESS",
3948 .txHash = txIDs[1],
3949 .batchID = batchID},
3950 };
3951 validateClosedLedger(env, testCases);
3952 }
3953
3954 env.close();
3955 {
3956 // next ledger is empty
3957 std::vector<TestLedgerData> const testCases = {};
3958 validateClosedLedger(env, testCases);
3959 }
3960 }
3961
3962 // After Batch Txn w/ same ticket
3963 {
3964 // IMPORTANT: The batch txn is applied first, then the noop txn.
3965 // Because of this ordering, the noop txn is not applied and is
3966 // overwritten by the payment in the batch transaction.
3967 test::jtx::Env env{*this, features};
3968 env.fund(XRP(10000), alice, bob);
3969 env.close();
3970
3971 std::uint32_t const aliceTicketSeq{env.seq(alice) + 1};
3972 env(ticket::create(alice, 10));
3973 env.close();
3974
3975 auto const aliceSeq = env.seq(alice);
3976
3977 // Batch Txn
3978 auto const batchFee = batch::calcBatchFee(env, 0, 2);
3979 auto const [txIDs, batchID] = submitBatch(
3980 env,
3981 tesSUCCESS,
3982 batch::outer(alice, 0, batchFee, tfAllOrNothing),
3983 batch::Inner(pay(alice, bob, XRP(1)), 0, aliceTicketSeq + 1),
3984 batch::Inner(pay(alice, bob, XRP(2)), aliceSeq),
3985 ticket::Use(aliceTicketSeq));
3986
3987 // AccountSet Txn
3988 auto const noopTxn = env.jt(noop(alice), ticket::Use(aliceTicketSeq + 1));
3989 env(noopTxn);
3990
3991 env.close();
3992 {
3993 std::vector<TestLedgerData> const testCases = {
3994 {.index = 0,
3995 .txType = "Batch",
3996 .result = "tesSUCCESS",
3997 .txHash = batchID,
3998 .batchID = std::nullopt},
3999 {.index = 1,
4000 .txType = "Payment",
4001 .result = "tesSUCCESS",
4002 .txHash = txIDs[0],
4003 .batchID = batchID},
4004 {.index = 2,
4005 .txType = "Payment",
4006 .result = "tesSUCCESS",
4007 .txHash = txIDs[1],
4008 .batchID = batchID},
4009 };
4010 validateClosedLedger(env, testCases);
4011 }
4012
4013 env.close();
4014 {
4015 // next ledger is empty
4016 std::vector<TestLedgerData> const testCases = {};
4017 validateClosedLedger(env, testCases);
4018 }
4019 }
4020 }
4021
4022 void
4024 {
4025 testcase("objects open ledger");
4026
4027 using namespace test::jtx;
4028 using namespace std::literals;
4029
4030 auto const alice = Account("alice");
4031 auto const bob = Account("bob");
4032
4033 // Consume Object Before Batch Txn
4034 {
4035 // IMPORTANT: The initial result of `CheckCash` is tecNO_ENTRY
4036 // because the create transaction has not been applied because the
4037 // batch will run in the close ledger process. The batch will be
4038 // allied and then retry this transaction in the current ledger.
4039
4040 test::jtx::Env env{*this, features};
4041 env.fund(XRP(10000), alice, bob);
4042 env.close();
4043
4044 std::uint32_t const aliceTicketSeq{env.seq(alice) + 1};
4045 env(ticket::create(alice, 10));
4046 env.close();
4047
4048 auto const aliceSeq = env.seq(alice);
4049
4050 // CheckCash Txn
4051 uint256 const chkID{getCheckIndex(alice, aliceSeq)};
4052 auto const objTxn = env.jt(check::cash(bob, chkID, XRP(10)));
4053 auto const objTxnID = to_string(objTxn.stx->getTransactionID());
4054 env(objTxn, Ter(tecNO_ENTRY));
4055
4056 // Batch Txn
4057 auto const batchFee = batch::calcBatchFee(env, 0, 2);
4058 auto const [txIDs, batchID] = submitBatch(
4059 env,
4060 tesSUCCESS,
4061 batch::outer(alice, 0, batchFee, tfAllOrNothing),
4062 batch::Inner(check::create(alice, bob, XRP(10)), aliceSeq),
4063 batch::Inner(pay(alice, bob, XRP(1)), 0, aliceTicketSeq + 1),
4064 ticket::Use(aliceTicketSeq));
4065
4066 env.close();
4067 {
4068 std::vector<TestLedgerData> const testCases = {
4069 {.index = 0,
4070 .txType = "Batch",
4071 .result = "tesSUCCESS",
4072 .txHash = batchID,
4073 .batchID = std::nullopt},
4074 {.index = 1,
4075 .txType = "CheckCreate",
4076 .result = "tesSUCCESS",
4077 .txHash = txIDs[0],
4078 .batchID = batchID},
4079 {.index = 2,
4080 .txType = "Payment",
4081 .result = "tesSUCCESS",
4082 .txHash = txIDs[1],
4083 .batchID = batchID},
4084 {.index = 3,
4085 .txType = "CheckCash",
4086 .result = "tesSUCCESS",
4087 .txHash = objTxnID,
4088 .batchID = std::nullopt},
4089 };
4090 validateClosedLedger(env, testCases);
4091 }
4092
4093 env.close();
4094 {
4095 // next ledger is empty
4096 std::vector<TestLedgerData> const testCases = {};
4097 validateClosedLedger(env, testCases);
4098 }
4099 }
4100
4101 // Create Object Before Batch Txn
4102 {
4103 test::jtx::Env env{*this, features};
4104 env.fund(XRP(10000), alice, bob);
4105 env.close();
4106
4107 std::uint32_t const aliceTicketSeq{env.seq(alice) + 1};
4108 env(ticket::create(alice, 10));
4109 env.close();
4110
4111 auto const aliceSeq = env.seq(alice);
4112 auto const bobSeq = env.seq(bob);
4113
4114 // CheckCreate Txn
4115 uint256 const chkID{getCheckIndex(alice, aliceSeq)};
4116 auto const objTxn = env.jt(check::create(alice, bob, XRP(10)));
4117 auto const objTxnID = to_string(objTxn.stx->getTransactionID());
4118 env(objTxn, Ter(tesSUCCESS));
4119
4120 // Batch Txn
4121 auto const batchFee = batch::calcBatchFee(env, 1, 2);
4122 auto const [txIDs, batchID] = submitBatch(
4123 env,
4124 tesSUCCESS,
4125 batch::outer(alice, 0, batchFee, tfAllOrNothing),
4126 batch::Inner(check::cash(bob, chkID, XRP(10)), bobSeq),
4127 batch::Inner(pay(alice, bob, XRP(1)), 0, aliceTicketSeq + 1),
4128 ticket::Use(aliceTicketSeq),
4129 batch::Sig(bob));
4130
4131 env.close();
4132 {
4133 std::vector<TestLedgerData> const testCases = {
4134 {.index = 0,
4135 .txType = "CheckCreate",
4136 .result = "tesSUCCESS",
4137 .txHash = objTxnID,
4138 .batchID = std::nullopt},
4139 {.index = 1,
4140 .txType = "Batch",
4141 .result = "tesSUCCESS",
4142 .txHash = batchID,
4143 .batchID = std::nullopt},
4144 {.index = 2,
4145 .txType = "CheckCash",
4146 .result = "tesSUCCESS",
4147 .txHash = txIDs[0],
4148 .batchID = batchID},
4149 {.index = 3,
4150 .txType = "Payment",
4151 .result = "tesSUCCESS",
4152 .txHash = txIDs[1],
4153 .batchID = batchID},
4154 };
4155 validateClosedLedger(env, testCases);
4156 }
4157 }
4158
4159 // After Batch Txn
4160 {
4161 // IMPORTANT: The initial result of `CheckCash` is tecNO_ENTRY
4162 // because the create transaction has not been applied because the
4163 // batch will run in the close ledger process. The batch will be
4164 // applied and then retry this transaction in the current ledger.
4165
4166 test::jtx::Env env{*this, features};
4167 env.fund(XRP(10000), alice, bob);
4168 env.close();
4169
4170 std::uint32_t const aliceTicketSeq{env.seq(alice) + 1};
4171 env(ticket::create(alice, 10));
4172 env.close();
4173
4174 auto const aliceSeq = env.seq(alice);
4175
4176 // Batch Txn
4177 auto const batchFee = batch::calcBatchFee(env, 0, 2);
4178 uint256 const chkID{getCheckIndex(alice, aliceSeq)};
4179 auto const [txIDs, batchID] = submitBatch(
4180 env,
4181 tesSUCCESS,
4182 batch::outer(alice, 0, batchFee, tfAllOrNothing),
4183 batch::Inner(check::create(alice, bob, XRP(10)), aliceSeq),
4184 batch::Inner(pay(alice, bob, XRP(1)), 0, aliceTicketSeq + 1),
4185 ticket::Use(aliceTicketSeq));
4186
4187 // CheckCash Txn
4188 auto const objTxn = env.jt(check::cash(bob, chkID, XRP(10)));
4189 auto const objTxnID = to_string(objTxn.stx->getTransactionID());
4190 env(objTxn, Ter(tecNO_ENTRY));
4191
4192 env.close();
4193 {
4194 std::vector<TestLedgerData> const testCases = {
4195 {.index = 0,
4196 .txType = "Batch",
4197 .result = "tesSUCCESS",
4198 .txHash = batchID,
4199 .batchID = std::nullopt},
4200 {.index = 1,
4201 .txType = "CheckCreate",
4202 .result = "tesSUCCESS",
4203 .txHash = txIDs[0],
4204 .batchID = batchID},
4205 {.index = 2,
4206 .txType = "Payment",
4207 .result = "tesSUCCESS",
4208 .txHash = txIDs[1],
4209 .batchID = batchID},
4210 {.index = 3,
4211 .txType = "CheckCash",
4212 .result = "tesSUCCESS",
4213 .txHash = objTxnID,
4214 .batchID = std::nullopt},
4215 };
4216 validateClosedLedger(env, testCases);
4217 }
4218 }
4219 }
4220
4221 void
4223 {
4224 testcase("pseudo txn with tfInnerBatchTxn");
4225
4226 using namespace test::jtx;
4227 using namespace std::literals;
4228
4229 test::jtx::Env env{*this, features};
4230
4231 auto const alice = Account("alice");
4232 auto const bob = Account("bob");
4233 env.fund(XRP(10000), alice, bob);
4234 env.close();
4235
4236 STTx const stx = STTx(ttAMENDMENT, [&](auto& obj) {
4237 obj.setAccountID(sfAccount, AccountID());
4238 obj.setFieldH256(sfAmendment, uint256(2));
4239 obj.setFieldU32(sfLedgerSequence, env.seq(alice));
4240 obj.setFieldU32(sfFlags, tfInnerBatchTxn);
4241 });
4242
4243 std::string reason;
4244 BEAST_EXPECT(isPseudoTx(stx));
4245 BEAST_EXPECT(!passesLocalChecks(stx, reason));
4246 BEAST_EXPECT(reason == "Cannot submit pseudo transactions.");
4247 env.app().getOpenLedger().modify([&](OpenView& view, beast::Journal j) {
4248 auto const result = xrpl::apply(env.app(), view, stx, TapNone, j);
4249 BEAST_EXPECT(!result.applied && result.ter == temINVALID_FLAG);
4250 return result.applied;
4251 });
4252 }
4253
4254 void
4256 {
4257 testcase("batch open ledger");
4258 // IMPORTANT: When a transaction is submitted outside of a batch and
4259 // another transaction is part of the batch, the batch might fail
4260 // because the sequence is out of order. This is because the canonical
4261 // order of transactions is determined by the account first. So in this
4262 // case, alice's batch comes after bob's self submitted transaction even
4263 // though the payment was submitted after the batch.
4264
4265 using namespace test::jtx;
4266 using namespace std::literals;
4267
4268 test::jtx::Env env{*this, features};
4269 XRPAmount const baseFee = env.current()->fees().base;
4270
4271 auto const alice = Account("alice");
4272 auto const bob = Account("bob");
4273
4274 env.fund(XRP(10000), alice, bob);
4275 env.close();
4276
4277 env(noop(bob), Ter(tesSUCCESS));
4278 env.close();
4279
4280 auto const aliceSeq = env.seq(alice);
4281 auto const preAlice = env.balance(alice);
4282 auto const preBob = env.balance(bob);
4283 auto const bobSeq = env.seq(bob);
4284
4285 // Alice Pays Bob (Open Ledger)
4286 auto const payTxn1 = env.jt(pay(alice, bob, XRP(10)), Seq(aliceSeq));
4287 auto const payTxn1ID = to_string(payTxn1.stx->getTransactionID());
4288 env(payTxn1, Ter(tesSUCCESS));
4289
4290 // Alice & Bob Atomic Batch
4291 auto const batchFee = batch::calcBatchFee(env, 1, 2);
4292 auto const [txIDs, batchID] = submitBatch(
4293 env,
4294 tesSUCCESS,
4295 batch::outer(alice, aliceSeq + 1, batchFee, tfAllOrNothing),
4296 batch::Inner(pay(alice, bob, XRP(10)), aliceSeq + 2),
4297 batch::Inner(pay(bob, alice, XRP(5)), bobSeq),
4298 batch::Sig(bob));
4299
4300 // Bob pays Alice (Open Ledger)
4301 auto const payTxn2 = env.jt(pay(bob, alice, XRP(5)), Seq(bobSeq + 1));
4302 auto const payTxn2ID = to_string(payTxn2.stx->getTransactionID());
4303 env(payTxn2, Ter(terPRE_SEQ));
4304 env.close();
4305
4306 std::vector<TestLedgerData> const testCases = {
4307 {.index = 0,
4308 .txType = "Payment",
4309 .result = "tesSUCCESS",
4310 .txHash = payTxn1ID,
4311 .batchID = std::nullopt},
4312 {.index = 1,
4313 .txType = "Batch",
4314 .result = "tesSUCCESS",
4315 .txHash = batchID,
4316 .batchID = std::nullopt},
4317 {.index = 2,
4318 .txType = "Payment",
4319 .result = "tesSUCCESS",
4320 .txHash = txIDs[0],
4321 .batchID = batchID},
4322 {.index = 3,
4323 .txType = "Payment",
4324 .result = "tesSUCCESS",
4325 .txHash = txIDs[1],
4326 .batchID = batchID},
4327 };
4328 validateClosedLedger(env, testCases);
4329
4330 env.close();
4331 {
4332 // next ledger includes the payment txn
4333 std::vector<TestLedgerData> const testCases = {
4334 {.index = 0,
4335 .txType = "Payment",
4336 .result = "tesSUCCESS",
4337 .txHash = payTxn2ID,
4338 .batchID = std::nullopt},
4339 };
4340 validateClosedLedger(env, testCases);
4341 }
4342
4343 // Alice consumes sequences (# of txns)
4344 BEAST_EXPECT(env.seq(alice) == aliceSeq + 3);
4345
4346 // Alice consumes sequences (# of txns)
4347 BEAST_EXPECT(env.seq(bob) == bobSeq + 2);
4348
4349 // Alice pays XRP & Fee; Bob receives XRP & pays Fee
4350 BEAST_EXPECT(env.balance(alice) == preAlice - XRP(10) - batchFee - baseFee);
4351 BEAST_EXPECT(env.balance(bob) == preBob + XRP(10) - baseFee);
4352 }
4353
4354 void
4356 {
4357 testcase("batch tx queue");
4358
4359 using namespace test::jtx;
4360 using namespace std::literals;
4361
4362 // only outer batch transactions are counter towards the queue size
4363 {
4364 test::jtx::Env env{
4365 *this,
4367 features,
4368 nullptr,
4370
4371 auto alice = Account("alice");
4372 auto bob = Account("bob");
4373 auto carol = Account("carol");
4374
4375 // Fund across several ledgers so the TxQ metrics stay restricted.
4376 env.fund(XRP(10000), noripple(alice, bob));
4377 env.close(env.now() + 5s, 10000ms);
4378 env.fund(XRP(10000), noripple(carol));
4379 env.close(env.now() + 5s, 10000ms);
4380
4381 // Fill the ledger
4382 env(noop(alice));
4383 env(noop(alice));
4384 env(noop(alice));
4385 checkMetrics(*this, env, 0, std::nullopt, 3, 2);
4386
4387 env(noop(carol), Ter(terQUEUED));
4388 checkMetrics(*this, env, 1, std::nullopt, 3, 2);
4389
4390 auto const aliceSeq = env.seq(alice);
4391 auto const bobSeq = env.seq(bob);
4392 auto const batchFee = batch::calcBatchFee(env, 1, 2);
4393
4394 // Queue Batch
4395 {
4396 env(batch::outer(alice, aliceSeq, batchFee, tfAllOrNothing),
4397 batch::Inner(pay(alice, bob, XRP(10)), aliceSeq + 1),
4398 batch::Inner(pay(bob, alice, XRP(5)), bobSeq),
4399 batch::Sig(bob),
4400 Ter(terQUEUED));
4401 }
4402
4403 checkMetrics(*this, env, 2, std::nullopt, 3, 2);
4404
4405 // Replace Queued Batch
4406 {
4407 env(batch::outer(alice, aliceSeq, openLedgerFee(env, batchFee), tfAllOrNothing),
4408 batch::Inner(pay(alice, bob, XRP(10)), aliceSeq + 1),
4409 batch::Inner(pay(bob, alice, XRP(5)), bobSeq),
4410 batch::Sig(bob),
4411 Ter(tesSUCCESS));
4412 env.close();
4413 }
4414
4415 checkMetrics(*this, env, 0, 12, 1, 6);
4416 }
4417
4418 // inner batch transactions are counter towards the ledger tx count
4419 {
4420 test::jtx::Env env{
4421 *this,
4423 features,
4424 nullptr,
4426
4427 auto alice = Account("alice");
4428 auto bob = Account("bob");
4429 auto carol = Account("carol");
4430
4431 // Fund across several ledgers so the TxQ metrics stay restricted.
4432 env.fund(XRP(10000), noripple(alice, bob));
4433 env.close(env.now() + 5s, 10000ms);
4434 env.fund(XRP(10000), noripple(carol));
4435 env.close(env.now() + 5s, 10000ms);
4436
4437 // Fill the ledger leaving room for 1 queued transaction
4438 env(noop(alice));
4439 env(noop(alice));
4440 checkMetrics(*this, env, 0, std::nullopt, 2, 2);
4441
4442 auto const aliceSeq = env.seq(alice);
4443 auto const bobSeq = env.seq(bob);
4444 auto const batchFee = batch::calcBatchFee(env, 1, 2);
4445
4446 // Batch Successful
4447 {
4448 env(batch::outer(alice, aliceSeq, batchFee, tfAllOrNothing),
4449 batch::Inner(pay(alice, bob, XRP(10)), aliceSeq + 1),
4450 batch::Inner(pay(bob, alice, XRP(5)), bobSeq),
4451 batch::Sig(bob),
4452 Ter(tesSUCCESS));
4453 }
4454
4455 checkMetrics(*this, env, 0, std::nullopt, 3, 2);
4456
4457 env(noop(carol), Ter(terQUEUED));
4458 checkMetrics(*this, env, 1, std::nullopt, 3, 2);
4459 }
4460 }
4461
4462 void
4464 {
4465 testcase("batch network ops");
4466
4467 using namespace test::jtx;
4468 using namespace std::literals;
4469
4470 Env env(*this, envconfig(), features, nullptr, beast::Severity::Disabled);
4471
4472 auto alice = Account("alice");
4473 auto bob = Account("bob");
4474 env.fund(XRP(10000), alice, bob);
4475 env.close();
4476
4477 auto submitTx = [&](std::uint32_t flags) -> uint256 {
4478 auto jt = env.jt(pay(alice, bob, XRP(1)), Txflags(flags));
4479 Serializer s;
4480 jt.stx->add(s);
4481 env.app().getOPs().submitTransaction(jt.stx);
4482 return jt.stx->getTransactionID();
4483 };
4484
4485 auto processTxn = [&](std::uint32_t flags) -> uint256 {
4486 auto jt = env.jt(pay(alice, bob, XRP(1)), Txflags(flags));
4487 Serializer s;
4488 jt.stx->add(s);
4489 std::string reason;
4490 auto transaction = std::make_shared<Transaction>(jt.stx, reason, env.app());
4492 transaction, false, true, NetworkOPs::FailHard::Yes);
4493 return transaction->getID();
4494 };
4495
4496 // Validate: NetworkOPs::submitTransaction()
4497 {
4498 // Submit a tx with tfInnerBatchTxn
4499 uint256 const txBad = submitTx(tfInnerBatchTxn);
4500 BEAST_EXPECT(env.app().getHashRouter().getFlags(txBad) == HashRouterFlags::UNDEFINED);
4501 }
4502
4503 // Validate: NetworkOPs::processTransaction()
4504 {
4505 uint256 const txid = processTxn(tfInnerBatchTxn);
4506 // HashRouter::getFlags() should return LedgerFlags::BAD
4507 BEAST_EXPECT(env.app().getHashRouter().getFlags(txid) == HashRouterFlags::BAD);
4508 }
4509 }
4510
4511 void
4513 {
4514 testcase("batch delegate");
4515
4516 using namespace test::jtx;
4517 using namespace std::literals;
4518
4519 // delegated non atomic inner
4520 {
4521 test::jtx::Env env{*this, features};
4522
4523 auto const alice = Account("alice");
4524 auto const bob = Account("bob");
4525 auto const gw = Account("gw");
4526 auto const usd = gw["USD"];
4527 env.fund(XRP(10000), alice, bob, gw);
4528 env.close();
4529
4530 env(delegate::set(alice, bob, {"Payment"}));
4531 env.close();
4532
4533 auto const preAlice = env.balance(alice);
4534 auto const preBob = env.balance(bob);
4535
4536 auto const batchFee = batch::calcBatchFee(env, 0, 2);
4537 auto const seq = env.seq(alice);
4538
4539 auto tx = batch::Inner(pay(alice, bob, XRP(1)), seq + 1);
4540 tx[jss::Delegate] = bob.human();
4541 auto const [txIDs, batchID] = submitBatch(
4542 env,
4543 tesSUCCESS,
4544 batch::outer(alice, seq, batchFee, tfAllOrNothing),
4545 tx,
4546 batch::Inner(pay(alice, bob, XRP(2)), seq + 2));
4547 env.close();
4548
4549 std::vector<TestLedgerData> const testCases = {
4550 {.index = 0,
4551 .txType = "Batch",
4552 .result = "tesSUCCESS",
4553 .txHash = batchID,
4554 .batchID = std::nullopt},
4555 {.index = 1,
4556 .txType = "Payment",
4557 .result = "tesSUCCESS",
4558 .txHash = txIDs[0],
4559 .batchID = batchID},
4560 {.index = 2,
4561 .txType = "Payment",
4562 .result = "tesSUCCESS",
4563 .txHash = txIDs[1],
4564 .batchID = batchID},
4565 };
4566 validateClosedLedger(env, testCases);
4567
4568 // Alice consumes sequences (# of txns)
4569 BEAST_EXPECT(env.seq(alice) == seq + 3);
4570
4571 // Alice pays XRP & Fee; Bob receives XRP
4572 BEAST_EXPECT(env.balance(alice) == preAlice - XRP(3) - batchFee);
4573 BEAST_EXPECT(env.balance(bob) == preBob + XRP(3));
4574 }
4575
4576 // delegated atomic inner
4577 {
4578 test::jtx::Env env{*this, features};
4579
4580 auto const alice = Account("alice");
4581 auto const bob = Account("bob");
4582 auto const carol = Account("carol");
4583 auto const gw = Account("gw");
4584 auto const usd = gw["USD"];
4585 env.fund(XRP(10000), alice, bob, carol, gw);
4586 env.close();
4587
4588 env(delegate::set(bob, carol, {"Payment"}));
4589 env.close();
4590
4591 auto const preAlice = env.balance(alice);
4592 auto const preBob = env.balance(bob);
4593 auto const preCarol = env.balance(carol);
4594
4595 auto const batchFee = batch::calcBatchFee(env, 1, 2);
4596 auto const aliceSeq = env.seq(alice);
4597 auto const bobSeq = env.seq(bob);
4598
4599 auto tx = batch::Inner(pay(bob, alice, XRP(1)), bobSeq);
4600 tx[jss::Delegate] = carol.human();
4601 auto const [txIDs, batchID] = submitBatch(
4602 env,
4603 tesSUCCESS,
4604 batch::outer(alice, aliceSeq, batchFee, tfAllOrNothing),
4605 tx,
4606 batch::Inner(pay(alice, bob, XRP(2)), aliceSeq + 1),
4607 batch::Sig(bob));
4608 env.close();
4609
4610 std::vector<TestLedgerData> const testCases = {
4611 {.index = 0,
4612 .txType = "Batch",
4613 .result = "tesSUCCESS",
4614 .txHash = batchID,
4615 .batchID = std::nullopt},
4616 {.index = 1,
4617 .txType = "Payment",
4618 .result = "tesSUCCESS",
4619 .txHash = txIDs[0],
4620 .batchID = batchID},
4621 {.index = 2,
4622 .txType = "Payment",
4623 .result = "tesSUCCESS",
4624 .txHash = txIDs[1],
4625 .batchID = batchID},
4626 };
4627 validateClosedLedger(env, testCases);
4628
4629 BEAST_EXPECT(env.seq(alice) == aliceSeq + 2);
4630 BEAST_EXPECT(env.seq(bob) == bobSeq + 1);
4631 BEAST_EXPECT(env.balance(alice) == preAlice - XRP(1) - batchFee);
4632 BEAST_EXPECT(env.balance(bob) == preBob + XRP(1));
4633 // NOTE: Carol would normally pay the fee for delegated txns, but
4634 // because the batch is atomic, the fee is paid by the batch
4635 BEAST_EXPECT(env.balance(carol) == preCarol);
4636 }
4637
4638 // delegated non atomic inner (AccountSet)
4639 // this also makes sure tfInnerBatchTxn won't block delegated AccountSet
4640 // with granular permission
4641 {
4642 test::jtx::Env env{*this, features};
4643
4644 auto const alice = Account("alice");
4645 auto const bob = Account("bob");
4646 auto const gw = Account("gw");
4647 auto const usd = gw["USD"];
4648 env.fund(XRP(10000), alice, bob, gw);
4649 env.close();
4650
4651 env(delegate::set(alice, bob, {"AccountDomainSet"}));
4652 env.close();
4653
4654 auto const preAlice = env.balance(alice);
4655 auto const preBob = env.balance(bob);
4656
4657 auto const batchFee = batch::calcBatchFee(env, 0, 2);
4658 auto const seq = env.seq(alice);
4659
4660 auto tx = batch::Inner(noop(alice), seq + 1);
4661 std::string const domain = "example.com";
4662 tx[sfDomain.jsonName] = strHex(domain);
4663 tx[jss::Delegate] = bob.human();
4664 auto const [txIDs, batchID] = submitBatch(
4665 env,
4666 tesSUCCESS,
4667 batch::outer(alice, seq, batchFee, tfAllOrNothing),
4668 tx,
4669 batch::Inner(pay(alice, bob, XRP(2)), seq + 2));
4670 env.close();
4671
4672 std::vector<TestLedgerData> const testCases = {
4673 {.index = 0,
4674 .txType = "Batch",
4675 .result = "tesSUCCESS",
4676 .txHash = batchID,
4677 .batchID = std::nullopt},
4678 {.index = 1,
4679 .txType = "AccountSet",
4680 .result = "tesSUCCESS",
4681 .txHash = txIDs[0],
4682 .batchID = batchID},
4683 {.index = 2,
4684 .txType = "Payment",
4685 .result = "tesSUCCESS",
4686 .txHash = txIDs[1],
4687 .batchID = batchID},
4688 };
4689 validateClosedLedger(env, testCases);
4690
4691 // Alice consumes sequences (# of txns)
4692 BEAST_EXPECT(env.seq(alice) == seq + 3);
4693
4694 // Alice pays XRP & Fee; Bob receives XRP
4695 BEAST_EXPECT(env.balance(alice) == preAlice - XRP(2) - batchFee);
4696 BEAST_EXPECT(env.balance(bob) == preBob + XRP(2));
4697 }
4698
4699 // delegated non atomic inner (MPTokenIssuanceSet)
4700 // this also makes sure tfInnerBatchTxn won't block delegated
4701 // MPTokenIssuanceSet with granular permission
4702 {
4703 test::jtx::Env env{*this, features};
4704 Account const alice{"alice"};
4705 Account const bob{"bob"};
4706 env.fund(XRP(100000), alice, bob);
4707 env.close();
4708
4709 auto const mptID = makeMptID(env.seq(alice), alice);
4710 MPTTester mpt(env, alice, {.fund = false});
4711 env.close();
4712 mpt.create({.flags = tfMPTCanLock});
4713 env.close();
4714
4715 // alice gives granular permission to bob of MPTokenIssuanceLock
4716 env(delegate::set(alice, bob, {"MPTokenIssuanceLock", "MPTokenIssuanceUnlock"}));
4717 env.close();
4718
4719 auto const seq = env.seq(alice);
4720 auto const batchFee = batch::calcBatchFee(env, 0, 2);
4721
4722 json::Value jv1;
4723 jv1[sfTransactionType] = jss::MPTokenIssuanceSet;
4724 jv1[sfAccount] = alice.human();
4725 jv1[sfDelegate] = bob.human();
4726 jv1[sfSequence] = seq + 1;
4727 jv1[sfMPTokenIssuanceID] = to_string(mptID);
4728 jv1[sfFlags] = tfMPTLock;
4729
4730 json::Value jv2;
4731 jv2[sfTransactionType] = jss::MPTokenIssuanceSet;
4732 jv2[sfAccount] = alice.human();
4733 jv2[sfDelegate] = bob.human();
4734 jv2[sfSequence] = seq + 2;
4735 jv2[sfMPTokenIssuanceID] = to_string(mptID);
4736 jv2[sfFlags] = tfMPTUnlock;
4737
4738 auto const [txIDs, batchID] = submitBatch(
4739 env,
4740 tesSUCCESS,
4741 batch::outer(alice, seq, batchFee, tfAllOrNothing),
4742 batch::Inner(jv1, seq + 1),
4743 batch::Inner(jv2, seq + 2));
4744 env.close();
4745
4746 std::vector<TestLedgerData> const testCases = {
4747 {.index = 0,
4748 .txType = "Batch",
4749 .result = "tesSUCCESS",
4750 .txHash = batchID,
4751 .batchID = std::nullopt},
4752 {.index = 1,
4753 .txType = "MPTokenIssuanceSet",
4754 .result = "tesSUCCESS",
4755 .txHash = txIDs[0],
4756 .batchID = batchID},
4757 {.index = 2,
4758 .txType = "MPTokenIssuanceSet",
4759 .result = "tesSUCCESS",
4760 .txHash = txIDs[1],
4761 .batchID = batchID},
4762 };
4763 validateClosedLedger(env, testCases);
4764 }
4765
4766 // delegated non atomic inner (TrustSet)
4767 // this also makes sure tfInnerBatchTxn won't block delegated TrustSet
4768 // with granular permission
4769 {
4770 test::jtx::Env env{*this, features};
4771 Account const gw{"gw"};
4772 Account const alice{"alice"};
4773 Account const bob{"bob"};
4774 env.fund(XRP(10000), gw, alice, bob);
4775 env(fset(gw, asfRequireAuth));
4776 env.close();
4777 env(trust(alice, gw["USD"](50)));
4778 env.close();
4779
4780 env(delegate::set(gw, bob, {"TrustlineAuthorize", "TrustlineFreeze"}));
4781 env.close();
4782
4783 auto const seq = env.seq(gw);
4784 auto const batchFee = batch::calcBatchFee(env, 0, 2);
4785
4786 auto jv1 = trust(gw, gw["USD"](0), alice, tfSetfAuth);
4787 jv1[sfDelegate] = bob.human();
4788 auto jv2 = trust(gw, gw["USD"](0), alice, tfSetFreeze);
4789 jv2[sfDelegate] = bob.human();
4790
4791 auto const [txIDs, batchID] = submitBatch(
4792 env,
4793 tesSUCCESS,
4794 batch::outer(gw, seq, batchFee, tfAllOrNothing),
4795 batch::Inner(jv1, seq + 1),
4796 batch::Inner(jv2, seq + 2));
4797 env.close();
4798
4799 std::vector<TestLedgerData> const testCases = {
4800 {.index = 0,
4801 .txType = "Batch",
4802 .result = "tesSUCCESS",
4803 .txHash = batchID,
4804 .batchID = std::nullopt},
4805 {.index = 1,
4806 .txType = "TrustSet",
4807 .result = "tesSUCCESS",
4808 .txHash = txIDs[0],
4809 .batchID = batchID},
4810 {.index = 2,
4811 .txType = "TrustSet",
4812 .result = "tesSUCCESS",
4813 .txHash = txIDs[1],
4814 .batchID = batchID},
4815 };
4816 validateClosedLedger(env, testCases);
4817 }
4818
4819 // inner transaction not authorized by the delegating account.
4820 {
4821 test::jtx::Env env{*this, features};
4822 Account const gw{"gw"};
4823 Account const alice{"alice"};
4824 Account const bob{"bob"};
4825 env.fund(XRP(10000), gw, alice, bob);
4826 env(fset(gw, asfRequireAuth));
4827 env.close();
4828 env(trust(alice, gw["USD"](50)));
4829 env.close();
4830
4831 env(delegate::set(gw, bob, {"TrustlineAuthorize", "TrustlineFreeze"}));
4832 env.close();
4833
4834 auto const seq = env.seq(gw);
4835 auto const batchFee = batch::calcBatchFee(env, 0, 2);
4836
4837 auto jv1 = trust(gw, gw["USD"](0), alice, tfSetFreeze);
4838 jv1[sfDelegate] = bob.human();
4839 auto jv2 = trust(gw, gw["USD"](0), alice, tfClearFreeze);
4840 jv2[sfDelegate] = bob.human();
4841
4842 auto const [txIDs, batchID] = submitBatch(
4843 env,
4844 tesSUCCESS,
4845 batch::outer(gw, seq, batchFee, tfIndependent),
4846 batch::Inner(jv1, seq + 1),
4847 // terNO_DELEGATE_PERMISSION: not authorized to clear freeze
4848 batch::Inner(jv2, seq + 2));
4849 env.close();
4850
4851 std::vector<TestLedgerData> const testCases = {
4852 {.index = 0,
4853 .txType = "Batch",
4854 .result = "tesSUCCESS",
4855 .txHash = batchID,
4856 .batchID = std::nullopt},
4857 {.index = 1,
4858 .txType = "TrustSet",
4859 .result = "tesSUCCESS",
4860 .txHash = txIDs[0],
4861 .batchID = batchID},
4862 // jv2 fails with terNO_DELEGATE_PERMISSION.
4863 };
4864 validateClosedLedger(env, testCases);
4865
4866 // verify jv2 is not present in the closed ledger.
4867 BEAST_EXPECT(env.rpc("tx", txIDs[1])[jss::result][jss::error] == "txnNotFound");
4868 }
4869 }
4870
4871 void
4873 {
4874 // Verifying that the RPC response from submit includes
4875 // the account_sequence_available, account_sequence_next,
4876 // open_ledger_cost and validated_ledger_index fields.
4877 testcase("Validate RPC response");
4878
4879 using namespace jtx;
4880 Env env(*this, features);
4881 Account const alice("alice");
4882 Account const bob("bob");
4883 env.fund(XRP(10000), alice, bob);
4884 env.close();
4885
4886 // tes
4887 {
4888 auto const baseFee = env.current()->fees().base;
4889 auto const aliceSeq = env.seq(alice);
4890 auto jtx = env.jt(pay(alice, bob, XRP(1)));
4891
4892 Serializer s;
4893 jtx.stx->add(s);
4894 auto const jr = env.rpc("submit", strHex(s.slice()))[jss::result];
4895 env.close();
4896
4897 BEAST_EXPECT(jr.isMember(jss::account_sequence_available));
4898 BEAST_EXPECT(jr[jss::account_sequence_available].asUInt() == aliceSeq + 1);
4899 BEAST_EXPECT(jr.isMember(jss::account_sequence_next));
4900 BEAST_EXPECT(jr[jss::account_sequence_next].asUInt() == aliceSeq + 1);
4901 BEAST_EXPECT(jr.isMember(jss::open_ledger_cost));
4902 BEAST_EXPECT(jr[jss::open_ledger_cost] == to_string(baseFee));
4903 BEAST_EXPECT(jr.isMember(jss::validated_ledger_index));
4904 }
4905
4906 // tec failure
4907 {
4908 auto const baseFee = env.current()->fees().base;
4909 auto const aliceSeq = env.seq(alice);
4910 env(fset(bob, asfRequireDest));
4911 auto jtx = env.jt(pay(alice, bob, XRP(1)), Seq(aliceSeq));
4912
4913 Serializer s;
4914 jtx.stx->add(s);
4915 auto const jr = env.rpc("submit", strHex(s.slice()))[jss::result];
4916 env.close();
4917
4918 BEAST_EXPECT(jr.isMember(jss::account_sequence_available));
4919 BEAST_EXPECT(jr[jss::account_sequence_available].asUInt() == aliceSeq + 1);
4920 BEAST_EXPECT(jr.isMember(jss::account_sequence_next));
4921 BEAST_EXPECT(jr[jss::account_sequence_next].asUInt() == aliceSeq + 1);
4922 BEAST_EXPECT(jr.isMember(jss::open_ledger_cost));
4923 BEAST_EXPECT(jr[jss::open_ledger_cost] == to_string(baseFee));
4924 BEAST_EXPECT(jr.isMember(jss::validated_ledger_index));
4925 }
4926
4927 // tem failure
4928 {
4929 auto const baseFee = env.current()->fees().base;
4930 auto const aliceSeq = env.seq(alice);
4931 auto jtx = env.jt(pay(alice, bob, XRP(1)), Seq(aliceSeq + 1));
4932
4933 Serializer s;
4934 jtx.stx->add(s);
4935 auto const jr = env.rpc("submit", strHex(s.slice()))[jss::result];
4936 env.close();
4937
4938 BEAST_EXPECT(jr.isMember(jss::account_sequence_available));
4939 BEAST_EXPECT(jr[jss::account_sequence_available].asUInt() == aliceSeq);
4940 BEAST_EXPECT(jr.isMember(jss::account_sequence_next));
4941 BEAST_EXPECT(jr[jss::account_sequence_next].asUInt() == aliceSeq);
4942 BEAST_EXPECT(jr.isMember(jss::open_ledger_cost));
4943 BEAST_EXPECT(jr[jss::open_ledger_cost] == to_string(baseFee));
4944 BEAST_EXPECT(jr.isMember(jss::validated_ledger_index));
4945 }
4946 }
4947
4948 void
4950 {
4951 using namespace jtx;
4952 Env env(*this, features);
4953 Account const alice("alice");
4954 Account const bob("bob");
4955 Account const carol("carol");
4956 env.fund(XRP(10000), alice, bob, carol);
4957 env.close();
4958
4959 auto getBaseFee = [&](JTx const& jtx) -> XRPAmount {
4960 Serializer s;
4961 jtx.stx->add(s);
4962 return Batch::calculateBaseFee(*env.current(), *jtx.stx);
4963 };
4964
4965 // bad: Inner Batch transaction found
4966 {
4967 auto const seq = env.seq(alice);
4968 XRPAmount const batchFee = batch::calcBatchFee(env, 0, 2);
4969 auto jtx = env.jt(
4970 batch::outer(alice, seq, batchFee, tfAllOrNothing),
4971 batch::Inner(batch::outer(alice, seq, batchFee, tfAllOrNothing), seq),
4972 batch::Inner(pay(alice, bob, XRP(1)), seq + 2));
4973 XRPAmount const txBaseFee = getBaseFee(jtx);
4974 BEAST_EXPECT(txBaseFee == XRPAmount(kInitialXrp));
4975 }
4976
4977 // bad: Raw Transactions array exceeds max entries.
4978 {
4979 auto const seq = env.seq(alice);
4980 XRPAmount const batchFee = batch::calcBatchFee(env, 0, 2);
4981
4982 auto jtx = env.jt(
4983 batch::outer(alice, seq, batchFee, tfAllOrNothing),
4984 batch::Inner(pay(alice, bob, XRP(1)), seq + 1),
4985 batch::Inner(pay(alice, bob, XRP(1)), seq + 2),
4986 batch::Inner(pay(alice, bob, XRP(1)), seq + 3),
4987 batch::Inner(pay(alice, bob, XRP(1)), seq + 4),
4988 batch::Inner(pay(alice, bob, XRP(1)), seq + 5),
4989 batch::Inner(pay(alice, bob, XRP(1)), seq + 6),
4990 batch::Inner(pay(alice, bob, XRP(1)), seq + 7),
4991 batch::Inner(pay(alice, bob, XRP(1)), seq + 8),
4992 batch::Inner(pay(alice, bob, XRP(1)), seq + 9));
4993
4994 XRPAmount const txBaseFee = getBaseFee(jtx);
4995 BEAST_EXPECT(txBaseFee == XRPAmount(kInitialXrp));
4996 }
4997
4998 // bad: Signers array exceeds max entries.
4999 {
5000 auto const seq = env.seq(alice);
5001 XRPAmount const batchFee = batch::calcBatchFee(env, 0, 2);
5002
5003 auto jtx = env.jt(
5004 batch::outer(alice, seq, batchFee, tfAllOrNothing),
5005 batch::Inner(pay(alice, bob, XRP(10)), seq + 1),
5006 batch::Inner(pay(alice, bob, XRP(5)), seq + 2),
5007 batch::Sig(bob, carol, alice, bob, carol, alice, bob, carol, alice, alice));
5008 XRPAmount const txBaseFee = getBaseFee(jtx);
5009 BEAST_EXPECT(txBaseFee == XRPAmount(kInitialXrp));
5010 }
5011
5012 // good:
5013 {
5014 auto const seq = env.seq(alice);
5015 XRPAmount const batchFee = batch::calcBatchFee(env, 0, 2);
5016 auto jtx = env.jt(
5017 batch::outer(alice, seq, batchFee, tfAllOrNothing),
5018 batch::Inner(pay(alice, bob, XRP(1)), seq + 1),
5019 batch::Inner(pay(bob, alice, XRP(2)), seq + 2));
5020 XRPAmount const txBaseFee = getBaseFee(jtx);
5021 BEAST_EXPECT(txBaseFee == batchFee);
5022 }
5023 }
5024
5025 void
5027 {
5028 testEnable(features);
5029 testPreflight(features);
5030 testPreclaim(features);
5031 testBadRawTxn(features);
5032 testBadSequence(features);
5033 testBadOuterFee(features);
5034 testCalculateBaseFee(features);
5035 testAllOrNothing(features);
5036 testOnlyOne(features);
5037 testUntilFailure(features);
5038 testIndependent(features);
5039 testInnerSubmitRPC(features);
5040 testAccountActivation(features);
5041 testAccountSet(features);
5042 testAccountDelete(features);
5043 testLoan(features);
5044 testObjectCreateSequence(features);
5045 testObjectCreateTicket(features);
5046 testObjectCreate3rdParty(features);
5047 testTickets(features);
5048 testSequenceOpenLedger(features);
5049 testTicketsOpenLedger(features);
5050 testObjectsOpenLedger(features);
5051 testPseudoTxn(features);
5052 testOpenLedger(features);
5053 testBatchTxQueue(features);
5054 testBatchNetworkOps(features);
5055 testBatchDelegate(features);
5056 testValidateRPCResponse(features);
5057 testBatchCalculateBaseFee(features);
5058 }
5059
5060public:
5061 void
5062 run() override
5063 {
5064 using namespace test::jtx;
5065
5066 auto const sa = testableAmendments();
5067 testWithFeats(sa - fixBatchInnerSigs);
5068 testWithFeats(sa);
5069 }
5070};
5071
5073
5074} // namespace xrpl::test
T any_of(T... args)
T begin(T... args)
A generic endpoint for log messages.
Definition Journal.h:38
A testsuite class.
Definition suite.h:50
bool expect(Condition const &shouldBeTrue)
Evaluate a test condition.
Definition suite.h:223
void fail(String const &reason, char const *file, int line)
Record a failure.
Definition suite.h:522
TestcaseT testcase
Memberspace for declaring test cases.
Definition suite.h:149
Represents a JSON value.
Definition json_value.h:130
static XRPAmount calculateBaseFee(ReadView const &view, STTx const &tx)
Calculates the total base fee for a batch transaction.
Definition Batch.cpp:53
static constexpr auto kDisabledTxTypes
HashRouterFlags getFlags(uint256 const &key)
virtual void submitTransaction(std::shared_ptr< STTx const > const &)=0
virtual void processTransaction(std::shared_ptr< Transaction > &transaction, bool bUnlimited, bool bLocal, FailHard failType)=0
Process transactions as they arrive from the network or which are submitted by clients.
bool modify(modify_type const &f)
Modify the open ledger.
Writable ledger view that accumulates state and tx changes.
Definition OpenView.h:45
json::Value getJson(JsonOptions=JsonOptions::Values::None) const override
Definition STAmount.cpp:734
Holds the serialized result of parsing an input JSON object.
std::optional< STObject > object
The STObject if the parse was successful.
json::Value getJson(JsonOptions options) const override
Definition STTx.cpp:316
Slice slice() const noexcept
Definition Serializer.h:44
virtual TxQ & getTxQ()=0
virtual NetworkOPs & getOPs()=0
virtual OpenLedger & getOpenLedger()=0
virtual HashRouter & getHashRouter()=0
An immutable linear range of bytes.
Definition Slice.h:26
Metrics getMetrics(OpenView const &view) const
Returns fee metrics in reference fee level units.
Definition TxQ.cpp:1739
void testOpenLedger(FeatureBitset features)
void testTicketsOpenLedger(FeatureBitset features)
static json::Value getLastLedger(jtx::Env &env)
void run() override
Runs the suite.
void testBadRawTxn(FeatureBitset features)
void testObjectsOpenLedger(FeatureBitset features)
void testSequenceOpenLedger(FeatureBitset features)
void testEnable(FeatureBitset features)
void testPreflight(FeatureBitset features)
void testObjectCreate3rdParty(FeatureBitset features)
void testIndependent(FeatureBitset features)
void doTestInnerSubmitRPC(FeatureBitset features, bool withBatch)
void testBatchDelegate(FeatureBitset features)
void testBadSequence(FeatureBitset features)
void testTickets(FeatureBitset features)
void testAccountSet(FeatureBitset features)
std::pair< std::vector< std::string >, std::string > submitBatch(jtx::Env &env, TER const &result, Args &&... args)
void testPreclaim(FeatureBitset features)
void testBatchTxQueue(FeatureBitset features)
static auto openLedgerFee(jtx::Env &env, XRPAmount const &batchFee)
void testAllOrNothing(FeatureBitset features)
void testValidateRPCResponse(FeatureBitset features)
void testWithFeats(FeatureBitset features)
void testBatchCalculateBaseFee(FeatureBitset features)
void testLoan(FeatureBitset features)
void testBatchNetworkOps(FeatureBitset features)
static json::Value getTxByIndex(json::Value const &jrr, int const index)
void testObjectCreateTicket(FeatureBitset features)
void testInnerSubmitRPC(FeatureBitset features)
void validateClosedLedger(jtx::Env &env, std::vector< TestLedgerData > const &ledgerResults)
void testCalculateBaseFee(FeatureBitset features)
void testAccountActivation(FeatureBitset features)
void testUntilFailure(FeatureBitset features)
static uint256 getCheckIndex(AccountID const &account, std::uint32_t uSequence)
void validateInnerTxn(jtx::Env &env, std::string const &batchID, TestLedgerData const &ledgerResult)
void testObjectCreateSequence(FeatureBitset features)
void testOnlyOne(FeatureBitset features)
static std::unique_ptr< Config > makeSmallQueueConfig(std::map< std::string, std::string > extraTxQ={}, std::map< std::string, std::string > extraVoting={})
void testAccountDelete(FeatureBitset features)
void testPseudoTxn(FeatureBitset features)
void testBadOuterFee(FeatureBitset features)
Immutable cryptographic account descriptor.
Definition jtx/Account.h:17
std::string const & human() const
Returns the human readable public key.
Definition jtx/Account.h:92
AccountID id() const
Returns the Account ID.
Definition jtx/Account.h:85
A transaction testing environment.
Definition Env.h:143
Application & app()
Definition Env.h:280
bool close(NetClock::time_point closeTime, std::optional< std::chrono::milliseconds > consensusDelay=std::nullopt)
Close and advance the ledger.
Definition Env.cpp:133
json::Value json(JsonValue &&jv, FN const &... fN)
Create JSON from parameters.
Definition Env.h:592
SLE::const_pointer le(Account const &account) const
Return an account root.
Definition Env.cpp:284
std::shared_ptr< ReadView const > closed()
Returns the last closed ledger.
Definition Env.cpp:127
void fund(bool setDefaultRipple, STAmount const &amount, Account const &account)
Definition Env.cpp:296
std::uint32_t seq(Account const &account) const
Returns the next sequence number on account.
Definition Env.cpp:275
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:864
void setParseFailureExpected(bool b)
Definition Env.h:480
JTx jt(JsonValue &&jv, FN const &... fN)
Create a JTx from parameters.
Definition Env.h:566
PrettyAmount balance(Account const &account) const
Returns the XRP balance on an account.
Definition Env.cpp:201
void trust(STAmount const &amount, Account const &account)
Establish trust lines.
Definition Env.cpp:327
JTx jtnofill(JsonValue &&jv, FN const &... fN)
Create a JTx from parameters.
Definition Env.h:578
void memoize(Account const &account)
Associate AccountID with account.
Definition Env.cpp:174
std::shared_ptr< OpenView const > current() const
Returns the current ledger.
Definition Env.h:353
NetClock::time_point now()
Returns the current network time.
Definition Env.h:305
Set the fee on a JTx.
Definition fee.h:15
Test helper for creating, mutating, and asserting MPT and confidential MPT ledger state.
Definition mpt.h:385
Set a multisignature on a JTx.
Definition multisign.h:39
Set the regular signature on a JTx.
Definition sig.h:13
Set the expected result code for a JTx The test will fail if the code doesn't match.
Definition ter.h:13
Set the flags on a JTx.
Definition txflags.h:9
Adds an inner Batch transaction to a JTx and autofills it.
Definition batch.h:58
Sets a nested multi-signature for a Batch transaction on a JTx.
Definition batch.h:131
Sets the Batch transaction signers on a JTx.
Definition batch.h:108
Set a ticket sequence on a JTx.
Definition ticket.h:26
T end(T... args)
T forward(T... args)
T make_pair(T... args)
T make_shared(T... args)
@ Array
array value (ordered list)
Definition json_value.h:25
@ Object
object value (collection of name/value pairs).
Definition json_value.h:26
Keylet loanBroker(AccountID const &owner, std::uint32_t seq) noexcept
Definition Indexes.cpp:557
Keylet check(AccountID const &id, std::uint32_t seq) noexcept
A Check.
Definition Indexes.cpp:322
Keylet loan(uint256 const &loanBrokerID, std::uint32_t loanSeq) noexcept
Definition Indexes.cpp:563
Keylet account(AccountID const &id) noexcept
AccountID root.
Definition Indexes.cpp:186
json::Value outer(jtx::Account const &account, uint32_t seq, STAmount const &fee, std::uint32_t flags)
Build an outer Batch transaction JSON object.
Definition batch.cpp:53
XRPAmount calcBatchFee(jtx::Env const &env, uint32_t const &numSigners, uint32_t const &txns=0)
Calculate the expected outer Batch transaction fee.
Definition batch.cpp:35
json::Value create(A const &account, A const &dest, STAmount const &sendMax)
Create a check.
json::Value cash(jtx::Account const &dest, uint256 const &checkId, STAmount const &amount)
Cash a check requiring that a specific amount be delivered.
Definition check.cpp:15
json::Value set(jtx::Account const &account, jtx::Account const &authorize, std::vector< std::string > const &permissions)
Definition delegate.cpp:17
Deposit preauthorize operations.
Definition deposit.h:7
json::Value create(Account const &account, std::uint32_t count)
Create one of more tickets.
Definition ticket.cpp:16
static NoneT const kNone
Definition tags.h:9
json::Value pay(AccountID const &account, AccountID const &to, AnyAmount amount)
Create a payment.
Definition pay.cpp:14
json::Value regkey(Account const &account, DisabledT)
Disable the regular key.
Definition regkey.cpp:13
XrpT const XRP
Converts to XRP Issue or STAmount.
Definition amount.cpp:92
json::Value noop(Account const &account)
The null transaction.
Definition noop.h:9
void checkMetrics(Suite &test, jtx::Env &env, std::size_t expectedCount, std::optional< std::size_t > expectedMaxCount, std::size_t expectedInLedger, std::size_t expectedPerLedger, std::uint64_t expectedMinFeeLevel=kBaseFeeLevel.fee(), std::uint64_t expectedMedFeeLevel=kMinEscalationFeeLevel.fee(), std::source_location const location=std::source_location::current())
FeatureBitset testableAmendments()
Definition Env.h:76
json::Value acctdelete(Account const &account, Account const &dest)
Delete account.
void incLgrSeqForAccDel(jtx::Env &env, jtx::Account const &acc, std::uint32_t margin=0)
std::array< Account, 1+sizeof...(Args)> noripple(Account const &account, Args const &... args)
Designate accounts as no-ripple in Env::fund.
Definition Env.h:70
json::Value offer(Account const &account, STAmount const &takerPays, STAmount const &takerGets, std::uint32_t flags)
Create an offer.
Definition offer.cpp:14
std::unique_ptr< Config > envconfig()
creates and initializes a default configuration for jtx::Env
Definition envconfig.h:28
json::Value trust(Account const &account, STAmount const &amount, std::uint32_t flags)
Modify a trust line.
Definition trust.cpp:18
json::Value signers(Account const &account, std::uint32_t quorum, std::vector< Signer > const &v)
Definition multisign.cpp:31
json::Value fset(Account const &account, std::uint32_t on, std::uint32_t off=0)
Add and/or remove flag.
Definition flags.cpp:15
BEAST_DEFINE_TESTSUITE(AMMClawback, app, xrpl)
constexpr XRPAmount
Convert XRP to drops (integral types).
Definition TxTest.h:48
Use hash_* containers for keys that do not need a cryptographically secure hashing algorithm.
Definition algorithm.h:5
@ telINSUF_FEE_P
Definition TER.h:41
@ telENV_RPC_FAILED
Definition TER.h:52
@ terPRE_SEQ
Definition TER.h:213
@ terQUEUED
Definition TER.h:217
bool set(T &target, std::string const &name, Section const &section)
Set a value from a configuration Section If the named value is not found or doesn't parse as a T,...
Issue const & xrpIssue()
Returns an asset specifier that represents XRP.
Definition Issue.h:97
constexpr FlagValue tfInnerBatchTxn
Definition TxFlags.h:43
ApplyResult apply(ServiceRegistry &registry, OpenView &view, STTx const &tx, ApplyFlags flags, beast::Journal journal)
Apply a transaction to an OpenView.
Definition apply.cpp:139
std::string strHex(FwdIt begin, FwdIt end)
Definition strHex.h:10
constexpr TenthBips32 percentageToTenthBips(std::uint32_t percentage)
Definition Protocol.h:99
@ tefBAD_QUORUM
Definition TER.h:170
@ tefMASTER_DISABLED
Definition TER.h:167
@ tefBAD_SIGNATURE
Definition TER.h:169
@ tefBAD_AUTH
Definition TER.h:159
@ tefNOT_MULTI_SIGNING
Definition TER.h:171
XRPAmount toDrops(FeeLevel< T > const &level, XRPAmount baseFee)
Definition TxQ.h:830
TenthBips< std::uint32_t > TenthBips32
Definition Units.h:439
TenthBips< std::uint16_t > TenthBips16
Definition Units.h:438
std::string to_string(BaseUInt< Bits, Tag > const &a)
Definition base_uint.h:633
bool passesLocalChecks(STObject const &st, std::string &)
Definition STTx.cpp:771
@ TapNone
Definition ApplyView.h:13
BaseUInt< 160, detail::AccountIDTag > AccountID
A 160-bit unsigned that uniquely identifies an account.
Definition AccountID.h:28
@ temBAD_REGKEY
Definition TER.h:84
@ temARRAY_TOO_LARGE
Definition TER.h:127
@ temBAD_FEE
Definition TER.h:78
@ temINVALID_FLAG
Definition TER.h:97
@ temARRAY_EMPTY
Definition TER.h:126
@ temSEQ_AND_TICKET
Definition TER.h:112
@ temDISABLED
Definition TER.h:100
@ temBAD_SIGNATURE
Definition TER.h:91
@ temINVALID_INNER_BATCH
Definition TER.h:129
@ temREDUNDANT
Definition TER.h:98
@ temBAD_SIGNER
Definition TER.h:101
Buffer sign(PublicKey const &pk, SecretKey const &sk, Slice const &message)
Generate a signature for a message.
TERSubset< CanCvtToTER > TER
Definition TER.h:634
@ tecNO_ENTRY
Definition TER.h:304
void serializeBatch(Serializer &msg, std::uint32_t const &flags, std::vector< uint256 > const &txids)
std::vector< unsigned char > Blob
Storage for linear binary data.
Definition Blob.h:10
constexpr XRPAmount kInitialXrp
Configure the native currency.
bool isPseudoTx(STObject const &tx)
Check whether a transaction is a pseudo-transaction.
Definition STTx.cpp:810
BaseUInt< 256 > uint256
Definition base_uint.h:562
MPTID makeMptID(std::uint32_t sequence, AccountID const &account)
Definition Indexes.cpp:172
uint256 TxID
A transaction identifier.
Definition Protocol.h:275
@ tesSUCCESS
Definition TER.h:240
T push_back(T... args)
T reserve(T... args)
T size(T... args)
T str(T... args)
uint256 key
Definition Keylet.h:20
static constexpr auto kMinLedgersToComputeSizeLimit
Definition Constants.h:125
static constexpr auto kRetrySequencePercent
Definition Constants.h:148
static constexpr auto kMaxLedgerCountsToStore
Definition Constants.h:120
static constexpr auto kMinimumQueueSize
Definition Constants.h:128
static constexpr auto kNormalConsensusIncreasePercent
Definition Constants.h:131
static constexpr auto kLedgersInQueue
Definition Constants.h:116
static constexpr auto kMinimumTxnInLedgerStandalone
Definition Constants.h:130
static constexpr auto kTransactionQueue
Definition Constants.h:65
std::optional< std::string > batchID
Execution context for applying a JSON transaction.
Definition JTx.h:23
Thrown when parse fails.
Definition utility.h:16
Set the sequence number on a JTx.
Definition seq.h:12