xrpld
Loading...
Searching...
No Matches
Batch_test.cpp
1#include <test/jtx/Account.h>
2#include <test/jtx/Env.h>
3#include <test/jtx/SignerUtils.h>
4#include <test/jtx/TestHelpers.h>
5#include <test/jtx/acctdelete.h>
6#include <test/jtx/amount.h>
7#include <test/jtx/balance.h> // IWYU pragma: keep
8#include <test/jtx/batch.h>
9#include <test/jtx/check.h>
10#include <test/jtx/delegate.h>
11#include <test/jtx/envconfig.h>
12#include <test/jtx/fee.h>
13#include <test/jtx/flags.h>
14#include <test/jtx/mpt.h>
15#include <test/jtx/multisign.h>
16#include <test/jtx/noop.h>
17#include <test/jtx/offer.h>
18#include <test/jtx/pay.h>
19#include <test/jtx/regkey.h>
20#include <test/jtx/seq.h>
21#include <test/jtx/sig.h>
22#include <test/jtx/tags.h>
23#include <test/jtx/ter.h>
24#include <test/jtx/ticket.h>
25#include <test/jtx/trust.h>
26#include <test/jtx/txflags.h>
27#include <test/jtx/utility.h>
28#include <test/jtx/vault.h>
29
30#include <xrpld/app/misc/Transaction.h>
31#include <xrpld/app/misc/TxQ.h>
32
33#include <xrpl/basics/base_uint.h>
34#include <xrpl/basics/strHex.h>
35#include <xrpl/beast/unit_test/suite.h>
36#include <xrpl/beast/utility/Journal.h>
37#include <xrpl/config/BasicConfig.h>
38#include <xrpl/config/Constants.h>
39#include <xrpl/core/HashRouter.h>
40#include <xrpl/json/json_value.h>
41#include <xrpl/json/to_string.h>
42#include <xrpl/ledger/ApplyView.h>
43#include <xrpl/ledger/OpenView.h>
44#include <xrpl/protocol/AccountID.h>
45#include <xrpl/protocol/Batch.h>
46#include <xrpl/protocol/Feature.h>
47#include <xrpl/protocol/Indexes.h>
48#include <xrpl/protocol/Issue.h>
49#include <xrpl/protocol/KeyType.h>
50#include <xrpl/protocol/MPTIssue.h>
51#include <xrpl/protocol/Protocol.h>
52#include <xrpl/protocol/SField.h>
53#include <xrpl/protocol/STParsedJSON.h>
54#include <xrpl/protocol/STTx.h>
55#include <xrpl/protocol/SecretKey.h>
56#include <xrpl/protocol/SeqProxy.h>
57#include <xrpl/protocol/Serializer.h>
58#include <xrpl/protocol/Sign.h>
59#include <xrpl/protocol/TER.h>
60#include <xrpl/protocol/TxFlags.h>
61#include <xrpl/protocol/TxFormats.h>
62#include <xrpl/protocol/Units.h>
63#include <xrpl/protocol/XRPAmount.h>
64#include <xrpl/protocol/jss.h>
65#include <xrpl/server/NetworkOPs.h>
66#include <xrpl/tx/Transactor.h>
67#include <xrpl/tx/apply.h>
68#include <xrpl/tx/transactors/payment/Payment.h>
69#include <xrpl/tx/transactors/system/Batch.h>
70
71#include <cstddef>
72#include <cstdint>
73#include <exception>
74#include <map>
75#include <memory>
76#include <optional>
77#include <string>
78#include <utility>
79#include <vector>
80
81namespace xrpl::test {
82
84{
93
99
100 static json::Value
101 getTxByIndex(json::Value const& jrr, int const index)
102 {
103 for (auto const& txn : jrr[jss::result][jss::ledger][jss::transactions])
104 {
105 if (txn[jss::metaData][sfTransactionIndex.jsonName] == index)
106 return txn;
107 }
108 return {};
109 }
110
111 static json::Value
113 {
114 json::Value params;
115 params[jss::ledger_index] = env.closed()->seq();
116 params[jss::transactions] = true;
117 params[jss::expand] = true;
118 return env.rpc("json", "ledger", to_string(params));
119 }
120
121 void
123 {
124 auto const jrr = getLastLedger(env);
125 auto const transactions = jrr[jss::result][jss::ledger][jss::transactions];
126 BEAST_EXPECT(transactions.size() == ledgerResults.size());
127 for (TestLedgerData const& ledgerResult : ledgerResults)
128 {
129 auto const txn = getTxByIndex(jrr, ledgerResult.index);
130 BEAST_EXPECT(txn[jss::hash].asString() == ledgerResult.txHash);
131 BEAST_EXPECT(txn.isMember(jss::metaData));
132 json::Value const meta = txn[jss::metaData];
133 BEAST_EXPECT(txn[sfTransactionType.jsonName] == ledgerResult.txType);
134 BEAST_EXPECT(meta[sfTransactionResult.jsonName] == ledgerResult.result);
135 if (ledgerResult.batchID)
136 {
138 env,
139 *ledgerResult.batchID,
140 ledgerResult.txHash,
141 ledgerResult.txType,
142 ledgerResult.result);
143 }
144 }
145 }
146
147 template <typename... Args>
149 submitBatch(jtx::Env& env, TER const& result, Args&&... args)
150 {
151 auto batchTxn = env.jt(std::forward<Args>(args)...);
152 env(batchTxn, jtx::Ter(result));
153
154 auto const ids = batchTxn.stx->getBatchTransactionIDs();
156 txIDs.reserve(ids.size());
157 for (auto const& id : ids)
158 txIDs.push_back(strHex(id));
159 TxID const batchID = batchTxn.stx->getTransactionID();
160 return std::make_pair(txIDs, strHex(batchID));
161 }
162
163 static UInt256
164 getCheckIndex(AccountID const& account, std::uint32_t uSequence)
165 {
166 return keylet::check(account, SeqProxy::rawSequence(uSequence)).key;
167 }
168
172 std::map<std::string, std::string> extraVoting = {})
173 {
174 auto p = test::jtx::envconfig();
175 auto& section = p->section(Sections::kTransactionQueue);
176 section.set(Keys::kLedgersInQueue, "2");
177 section.set(Keys::kMinimumQueueSize, "2");
178 section.set(Keys::kMinLedgersToComputeSizeLimit, "3");
179 section.set(Keys::kMaxLedgerCountsToStore, "100");
180 section.set(Keys::kRetrySequencePercent, "25");
182
183 for (auto const& [k, v] : extraTxQ)
184 section.set(k, v);
185
186 return p;
187 }
188
189 static auto
190 openLedgerFee(jtx::Env& env, XRPAmount const& batchFee)
191 {
192 using namespace jtx;
193
194 auto const& view = *env.current();
195 auto metrics = env.app().getTxQ().getMetrics(view);
196 return toDrops(metrics.openLedgerFeeLevel, batchFee) + 1;
197 }
198
199 void
201 {
202 using namespace test::jtx;
203 using namespace std::literals;
204
205 for (bool const withBatch : {true, false})
206 {
207 testcase << "enabled: Batch " << (withBatch ? "enabled" : "disabled");
208
209 auto const amend = withBatch ? features : features - featureBatchV1_1;
210
211 Env env{*this, amend};
212
213 auto const alice = Account("alice");
214 auto const bob = Account("bob");
215 auto const carol = Account("carol");
216 env.fund(XRP(10000), alice, bob, carol);
217 env.close();
218
219 // ttBatch
220 {
221 auto const seq = env.seq(alice);
222 auto const batchFee = batch::calcBatchFee(env, 0, 2);
223 auto const txResult = withBatch ? Ter(tesSUCCESS) : Ter(temDISABLED);
224 env(batch::outer(alice, seq, batchFee, tfAllOrNothing),
225 batch::Inner(pay(alice, bob, XRP(1)), seq + 1),
226 batch::Inner(pay(alice, bob, XRP(1)), seq + 2),
227 txResult);
228 env.close();
229 }
230
231 // tfInnerBatchTxn
232 // A standalone transaction carrying this flag is never valid, so it
233 // is rejected early in checkValidity() regardless of the amendment.
234 {
235 env(pay(alice, bob, XRP(1)), Txflags(tfInnerBatchTxn), Ter(telENV_RPC_FAILED));
236 env.close();
237 }
238
239 env.close();
240 }
241 }
242
243 void
245 {
246 testcase("preflight");
247
248 using namespace test::jtx;
249 using namespace std::literals;
250
251 //----------------------------------------------------------------------
252 // preflight
253
254 Env env{*this, features};
255
256 auto const alice = Account("alice");
257 auto const bob = Account("bob");
258 auto const carol = Account("carol");
259 env.fund(XRP(10000), alice, bob, carol);
260 env.close();
261
262 // temBAD_FEE: preflight1
263 {
264 env(batch::outer(alice, env.seq(alice), XRP(-1), tfAllOrNothing), Ter(temBAD_FEE));
265 env.close();
266 }
267
268 // DEFENSIVE: temINVALID_FLAG: Batch: inner batch flag.
269 // ACTUAL: telENV_RPC_FAILED: checkValidity()
270 {
271 auto const seq = env.seq(alice);
272 auto const batchFee = batch::calcBatchFee(env, 0, 0);
273 env(batch::outer(alice, seq, batchFee, tfInnerBatchTxn), Ter(telENV_RPC_FAILED));
274 env.close();
275 }
276
277 // temINVALID_FLAG: Batch: invalid flags.
278 {
279 auto const seq = env.seq(alice);
280 auto const batchFee = batch::calcBatchFee(env, 0, 0);
281 env(batch::outer(alice, seq, batchFee, tfDisallowXRP), Ter(temINVALID_FLAG));
282 env.close();
283 }
284
285 // temINVALID_FLAG: Batch: too many flags.
286 {
287 auto const seq = env.seq(alice);
288 auto const batchFee = batch::calcBatchFee(env, 0, 0);
289 env(batch::outer(alice, seq, batchFee, tfAllOrNothing),
290 Txflags(tfAllOrNothing | tfOnlyOne),
292 env.close();
293 }
294
295 // temARRAY_EMPTY: Batch: txns array must have at least 2 entries.
296 {
297 auto const seq = env.seq(alice);
298 auto const batchFee = batch::calcBatchFee(env, 0, 0);
299 env(batch::outer(alice, seq, batchFee, tfAllOrNothing), Ter(temARRAY_EMPTY));
300 env.close();
301 }
302
303 // temARRAY_EMPTY: Batch: txns array must have at least 2 entries.
304 {
305 auto const seq = env.seq(alice);
306 auto const batchFee = batch::calcBatchFee(env, 0, 0);
307 env(batch::outer(alice, seq, batchFee, tfAllOrNothing),
308 batch::Inner(pay(alice, bob, XRP(1)), seq + 1),
310 env.close();
311 }
312
313 // An oversized batch (more than kMaxBatchTxCount inners) fails STTx
314 // construction, so the transaction cannot be built.
315 {
316 auto const seq = env.seq(alice);
317 auto const batchFee = batch::calcBatchFee(env, 0, 9);
318 env(batch::outer(alice, seq, batchFee, tfAllOrNothing),
319 batch::Inner(pay(alice, bob, XRP(1)), seq + 1),
320 batch::Inner(pay(alice, bob, XRP(1)), seq + 2),
321 batch::Inner(pay(alice, bob, XRP(1)), seq + 3),
322 batch::Inner(pay(alice, bob, XRP(1)), seq + 4),
323 batch::Inner(pay(alice, bob, XRP(1)), seq + 5),
324 batch::Inner(pay(alice, bob, XRP(1)), seq + 6),
325 batch::Inner(pay(alice, bob, XRP(1)), seq + 7),
326 batch::Inner(pay(alice, bob, XRP(1)), seq + 8),
327 batch::Inner(pay(alice, bob, XRP(1)), seq + 9),
329 env.close();
330 }
331
332 // temREDUNDANT: Batch: duplicate Txn found.
333 {
334 auto const batchFee = batch::calcBatchFee(env, 1, 2);
335 auto const seq = env.seq(alice);
336 auto jt = env.jtnofill(
337 batch::outer(alice, env.seq(alice), batchFee, tfAllOrNothing),
338 batch::Inner(pay(alice, bob, XRP(10)), seq + 1),
339 batch::Inner(pay(alice, bob, XRP(10)), seq + 1));
340
341 env(jt.jv, batch::Sig(bob), Ter(temREDUNDANT));
342 env.close();
343 }
344
345 // A batch may not contain a batch: the nested inner fails STTx
346 // construction, so the transaction cannot be built.
347 {
348 auto const seq = env.seq(alice);
349 auto const batchFee = batch::calcBatchFee(env, 0, 2);
350 env(batch::outer(alice, seq, batchFee, tfAllOrNothing),
351 batch::Inner(batch::outer(alice, seq, batchFee, tfAllOrNothing), seq),
352 batch::Inner(pay(alice, bob, XRP(1)), seq + 2),
354 env.close();
355 }
356
357 // temINVALID_FLAG: Batch: inner txn must have the
358 // tfInnerBatchTxn flag.
359 {
360 auto const batchFee = batch::calcBatchFee(env, 1, 2);
361 auto const seq = env.seq(alice);
362 auto tx1 = batch::Inner(pay(alice, bob, XRP(10)), seq + 1);
363 tx1[jss::Flags] = 0;
364 auto jt = env.jtnofill(
365 batch::outer(alice, seq, batchFee, tfAllOrNothing),
366 tx1,
367 batch::Inner(pay(alice, bob, XRP(10)), seq + 2));
368
369 env(jt.jv, batch::Sig(bob), Ter(temINVALID_FLAG));
370 env.close();
371 }
372
373 // temBAD_SIGNATURE: Batch: inner txn cannot include TxnSignature.
374 {
375 auto const seq = env.seq(alice);
376 auto const batchFee = batch::calcBatchFee(env, 0, 2);
377 auto jt = env.jt(pay(alice, bob, XRP(1)));
378 env(batch::outer(alice, seq, batchFee, tfAllOrNothing),
379 batch::Inner(jt.jv, seq + 1),
380 batch::Inner(pay(alice, bob, XRP(1)), seq + 2),
382 env.close();
383 }
384
385 // temBAD_SIGNER: Batch: inner txn cannot include Signers.
386 {
387 auto const seq = env.seq(alice);
388 auto const batchFee = batch::calcBatchFee(env, 0, 2);
389 auto tx1 = pay(alice, bob, XRP(1));
390 tx1[sfSigners.jsonName] = json::ValueType::Array;
391 tx1[sfSigners.jsonName][0U][sfSigner.jsonName] = json::ValueType::Object;
392 tx1[sfSigners.jsonName][0U][sfSigner.jsonName][sfAccount.jsonName] = alice.human();
393 tx1[sfSigners.jsonName][0U][sfSigner.jsonName][sfSigningPubKey.jsonName] =
394 strHex(alice.pk());
395 tx1[sfSigners.jsonName][0U][sfSigner.jsonName][sfTxnSignature.jsonName] = "DEADBEEF";
396 env(batch::outer(alice, seq, batchFee, tfAllOrNothing),
397 batch::Inner(tx1, seq + 1),
398 batch::Inner(pay(alice, bob, XRP(1)), seq + 2),
400 env.close();
401 }
402
403 // temBAD_REGKEY: Batch: inner txn must include empty
404 // SigningPubKey.
405 {
406 auto const seq = env.seq(alice);
407 auto const batchFee = batch::calcBatchFee(env, 0, 2);
408 auto tx1 = batch::Inner(pay(alice, bob, XRP(1)), seq + 1);
409 tx1[jss::SigningPubKey] = strHex(alice.pk());
410 auto jt = env.jtnofill(
411 batch::outer(alice, seq, batchFee, tfAllOrNothing),
412 tx1,
413 batch::Inner(pay(alice, bob, XRP(1)), seq + 2));
414
415 env(jt.jv, Ter(temBAD_REGKEY));
416 env.close();
417 }
418
419 // temINVALID_INNER_BATCH: Batch: inner txn preflight failed.
420 {
421 auto const seq = env.seq(alice);
422 auto const batchFee = batch::calcBatchFee(env, 0, 2);
423 env(batch::outer(alice, seq, batchFee, tfAllOrNothing),
424 batch::Inner(pay(alice, bob, XRP(1)), seq + 1),
425 // amount can't be negative
426 batch::Inner(pay(alice, bob, XRP(-1)), seq + 2),
428 env.close();
429 }
430
431 // temINVALID_INNER_BATCH: tfInnerBatchTxn set but no parentBatchId.
432 {
433 auto jtx = env.jt(pay(alice, bob, XRP(1)), Txflags(tfInnerBatchTxn));
434 PreflightContext const pfCtx(
435 env.app(), *jtx.stx, env.current()->rules(), TapNone, env.journal);
436 auto const pf = Transactor::invokePreflight<Payment>(pfCtx);
437 BEAST_EXPECT(pf == temINVALID_INNER_BATCH);
438 }
439
440 // temINVALID_FLAG: tfInnerBatchTxn set but featureBatchV1_1 disabled.
441 {
442 Env disabledEnv{*this, features - featureBatchV1_1};
443 disabledEnv.fund(XRP(10000), alice, bob);
444 disabledEnv.close();
445 auto jtx = disabledEnv.jt(pay(alice, bob, XRP(1)), Txflags(tfInnerBatchTxn));
446 PreflightContext const pfCtx(
447 disabledEnv.app(),
448 *jtx.stx,
449 disabledEnv.current()->rules(),
450 TapNone,
451 disabledEnv.journal);
452 auto const pf = Transactor::invokePreflight<Payment>(pfCtx);
453 BEAST_EXPECT(pf == temINVALID_FLAG);
454 }
455
456 // temINVALID_INNER_BATCH: parentBatchId set but tfInnerBatchTxn not
457 // set.
458 {
459 auto jtx = env.jt(pay(alice, bob, XRP(1)));
460 PreflightContext const pfCtx(
461 env.app(), *jtx.stx, UInt256{1}, env.current()->rules(), TapBatch, env.journal);
462 auto const pf = Transactor::invokePreflight<Payment>(pfCtx);
463 BEAST_EXPECT(pf == temINVALID_INNER_BATCH);
464 }
465
466 // temBAD_FEE: Batch: inner txn must have a fee of 0.
467 {
468 auto const seq = env.seq(alice);
469 auto const batchFee = batch::calcBatchFee(env, 0, 2);
470 auto tx1 = batch::Inner(pay(alice, bob, XRP(1)), seq + 1);
471 tx1[jss::Fee] = to_string(env.current()->fees().base);
472 env(batch::outer(alice, seq, batchFee, tfAllOrNothing),
473 tx1,
474 batch::Inner(pay(alice, bob, XRP(2)), seq + 2),
475 Ter(temBAD_FEE));
476 env.close();
477 }
478
479 // temBAD_FEE: Inner txn with negative fee
480 {
481 auto const seq = env.seq(alice);
482 auto const batchFee = batch::calcBatchFee(env, 0, 2);
483 auto tx1 = batch::Inner(pay(alice, bob, XRP(1)), seq + 1);
484 tx1[jss::Fee] = "-1";
485 env(batch::outer(alice, seq, batchFee, tfAllOrNothing),
486 tx1,
487 batch::Inner(pay(alice, bob, XRP(2)), seq + 2),
488 Ter(temBAD_FEE));
489 env.close();
490 }
491
492 // temBAD_FEE: Inner txn with non-integer fee
493 {
494 auto const seq = env.seq(alice);
495 auto const batchFee = batch::calcBatchFee(env, 0, 2);
496 auto tx1 = batch::Inner(pay(alice, bob, XRP(1)), seq + 1);
497 tx1[jss::Fee] = "1.5";
498 auto const g = env.getParseFailureGuard(true);
499 try
500 {
501 env(batch::outer(alice, seq, batchFee, tfAllOrNothing),
502 tx1,
503 batch::Inner(pay(alice, bob, XRP(2)), seq + 2));
504 fail("Expected parse_error for fractional fee");
505 }
506 catch (jtx::ParseError const&)
507 {
508 BEAST_EXPECT(true);
509 }
510 }
511
512 // temSEQ_AND_TICKET: Batch: inner txn cannot have both Sequence
513 // and TicketSequence.
514 {
515 auto const seq = env.seq(alice);
516 auto const batchFee = batch::calcBatchFee(env, 0, 2);
517 auto tx1 = batch::Inner(pay(alice, bob, XRP(1)), 0, 1);
518 tx1[jss::Sequence] = seq + 1;
519 env(batch::outer(alice, seq, batchFee, tfAllOrNothing),
520 tx1,
521 batch::Inner(pay(alice, bob, XRP(2)), seq + 2),
523 env.close();
524 }
525
526 // temSEQ_AND_TICKET: Batch: inner txn must have either Sequence or
527 // TicketSequence.
528 {
529 auto const seq = env.seq(alice);
530 auto const batchFee = batch::calcBatchFee(env, 0, 2);
531 env(batch::outer(alice, seq, batchFee, tfAllOrNothing),
532 batch::Inner(pay(alice, bob, XRP(1)), 0),
533 batch::Inner(pay(alice, bob, XRP(2)), seq + 2),
535 env.close();
536 }
537
538 // temREDUNDANT: Batch: duplicate sequence found:
539 {
540 auto const seq = env.seq(alice);
541 auto const batchFee = batch::calcBatchFee(env, 0, 2);
542 env(batch::outer(alice, seq, batchFee, tfAllOrNothing),
543 batch::Inner(pay(alice, bob, XRP(1)), seq + 1),
544 batch::Inner(pay(alice, bob, XRP(2)), seq + 1),
546 env.close();
547 }
548
549 // temREDUNDANT: Batch: duplicate ticket found:
550 {
551 auto const seq = env.seq(alice);
552 auto const batchFee = batch::calcBatchFee(env, 0, 2);
553 env(batch::outer(alice, seq, batchFee, tfAllOrNothing),
554 batch::Inner(pay(alice, bob, XRP(1)), 0, seq + 1),
555 batch::Inner(pay(alice, bob, XRP(2)), 0, seq + 1),
557 env.close();
558 }
559
560 // temREDUNDANT: Batch: duplicate ticket & sequence found:
561 {
562 auto const seq = env.seq(alice);
563 auto const batchFee = batch::calcBatchFee(env, 0, 2);
564 env(batch::outer(alice, seq, batchFee, tfAllOrNothing),
565 batch::Inner(pay(alice, bob, XRP(1)), 0, seq + 1),
566 batch::Inner(pay(alice, bob, XRP(2)), seq + 1),
568 env.close();
569 }
570
571 // DEFENSIVE: temARRAY_TOO_LARGE: Batch: signers array exceeds
572 // kMaxBatchSigners entries.
573 // ACTUAL: telENV_RPC_FAILED: isRawTransactionOkay()
574 {
575 auto const seq = env.seq(alice);
576 auto const batchFee = batch::calcBatchFee(env, kMaxBatchSigners + 1, 2);
577 env(batch::outer(alice, seq, batchFee, tfAllOrNothing),
578 batch::Inner(pay(alice, bob, XRP(10)), seq + 1),
579 batch::Inner(pay(alice, bob, XRP(5)), seq + 2),
582 env.close();
583 }
584
585 // temBAD_SIGNER: Batch: signer cannot be the outer account
586 {
587 auto const seq = env.seq(alice);
588 auto const batchFee = batch::calcBatchFee(env, 2, 2);
589 env(batch::outer(alice, seq, batchFee, tfAllOrNothing),
590 batch::Inner(pay(alice, bob, XRP(10)), seq + 1),
591 batch::Inner(pay(bob, alice, XRP(5)), env.seq(bob)),
592 batch::Sig(alice, bob),
594 env.close();
595 }
596
597 // temBAD_SIGNER: Batch: duplicate signer (caught by ascending order check)
598 {
599 auto const seq = env.seq(alice);
600 auto const batchFee = batch::calcBatchFee(env, 2, 2);
601 env(batch::outer(alice, seq, batchFee, tfAllOrNothing),
602 batch::Inner(pay(alice, bob, XRP(10)), seq + 1),
603 batch::Inner(pay(bob, alice, XRP(5)), env.seq(bob)),
604 batch::Sig(bob, bob),
606 env.close();
607 }
608
609 // temBAD_SIGNER: Batch: no account signature for inner txn.
610 // Note: Extra signature by bob
611 {
612 auto const seq = env.seq(alice);
613 auto const batchFee = batch::calcBatchFee(env, 1, 2);
614 env(batch::outer(alice, seq, batchFee, tfAllOrNothing),
615 batch::Inner(pay(alice, bob, XRP(10)), seq + 1),
616 batch::Inner(pay(alice, bob, XRP(5)), seq + 2),
617 batch::Sig(bob),
619 env.close();
620 }
621
622 // temBAD_SIGNER: Batch: no account signature for inner txn.
623 {
624 auto const seq = env.seq(alice);
625 auto const batchFee = batch::calcBatchFee(env, 1, 2);
626 env(batch::outer(alice, seq, batchFee, tfAllOrNothing),
627 batch::Inner(pay(alice, bob, XRP(10)), seq + 1),
628 batch::Inner(pay(bob, alice, XRP(5)), env.seq(bob)),
629 batch::Sig(carol),
631 env.close();
632 }
633
634 // temBAD_SIGNATURE: Batch: invalid batch txn signature.
635 {
636 auto const seq = env.seq(alice);
637 auto const bobSeq = env.seq(bob);
638 auto const batchFee = batch::calcBatchFee(env, 1, 2);
639 auto jt = env.jtnofill(
640 batch::outer(alice, env.seq(alice), batchFee, tfAllOrNothing),
641 batch::Inner(pay(alice, bob, XRP(10)), seq + 1),
642 batch::Inner(pay(bob, alice, XRP(5)), bobSeq));
643
644 Serializer msg;
646 msg,
647 jt.stx->getAccountID(sfAccount),
648 jt.stx->getSeqProxy().value(),
649 tfAllOrNothing,
650 jt.stx->getBatchTransactionIDs());
651 finishMultiSigningData(bob.id(), msg);
652 auto const sig = xrpl::sign(bob.pk(), bob.sk(), msg.slice());
653 jt.jv[sfBatchSigners.jsonName][0u][sfBatchSigner.jsonName][sfAccount.jsonName] =
654 bob.human();
655 jt.jv[sfBatchSigners.jsonName][0u][sfBatchSigner.jsonName][sfSigningPubKey.jsonName] =
656 strHex(alice.pk());
657 jt.jv[sfBatchSigners.jsonName][0u][sfBatchSigner.jsonName][sfTxnSignature.jsonName] =
658 strHex(Slice{sig.data(), sig.size()});
659
660 env(jt.jv, Ter(telENV_RPC_FAILED));
661 env.close();
662 }
663
664 // temBAD_SIGNER: Batch: invalid batch signers.
665 {
666 auto const seq = env.seq(alice);
667 auto const batchFee = batch::calcBatchFee(env, 2, 2);
668 env(batch::outer(alice, seq, batchFee, tfAllOrNothing),
669 batch::Inner(pay(alice, bob, XRP(10)), seq + 1),
670 batch::Inner(pay(bob, alice, XRP(5)), env.seq(bob)),
671 batch::Inner(pay(carol, alice, XRP(5)), env.seq(carol)),
672 batch::Sig(bob),
674 env.close();
675 }
676 }
677
678 void
680 {
681 testcase("preclaim");
682
683 using namespace test::jtx;
684 using namespace std::literals;
685
686 //----------------------------------------------------------------------
687 // preclaim
688
689 Env env{*this, features};
690
691 auto const alice = Account("alice");
692 auto const bob = Account("bob");
693 auto const carol = Account("carol");
694 auto const dave = Account("dave");
695 auto const elsa = Account("elsa");
696 auto const frank = Account("frank");
697 auto const phantom = Account("phantom");
698 env.memoize(phantom);
699
700 env.fund(XRP(10000), alice, bob, carol, dave, elsa, frank);
701 env.close();
702
703 //----------------------------------------------------------------------
704 // checkSign.checkSingleSign
705
706 // tefBAD_AUTH: Bob is not authorized to sign for Alice
707 {
708 auto const seq = env.seq(alice);
709 auto const batchFee = batch::calcBatchFee(env, 3, 2);
710 env(batch::outer(alice, seq, batchFee, tfAllOrNothing),
711 batch::Inner(pay(alice, bob, XRP(10)), seq + 1),
712 batch::Inner(pay(alice, bob, XRP(20)), seq + 2),
713 Sig(bob),
715 env.close();
716 }
717
718 //----------------------------------------------------------------------
719 // checkBatchSign.checkMultiSign
720
721 // tefNOT_MULTI_SIGNING: SignersList not enabled
722 {
723 auto const seq = env.seq(alice);
724 auto const batchFee = batch::calcBatchFee(env, 3, 2);
725 env(batch::outer(alice, seq, batchFee, tfAllOrNothing),
726 batch::Inner(pay(alice, bob, XRP(10)), seq + 1),
727 batch::Inner(pay(bob, alice, XRP(5)), env.seq(bob)),
728 batch::Msig(bob, {dave, carol}),
730 env.close();
731 }
732
733 env(signers(alice, 2, {{bob, 1}, {carol, 1}}));
734 env.close();
735
736 env(signers(bob, 2, {{carol, 1}, {dave, 1}, {elsa, 1}}));
737 env.close();
738
739 // tefBAD_SIGNATURE: Account not in SignersList
740 {
741 auto const seq = env.seq(alice);
742 auto const batchFee = batch::calcBatchFee(env, 3, 2);
743 env(batch::outer(alice, seq, batchFee, tfAllOrNothing),
744 batch::Inner(pay(alice, bob, XRP(10)), seq + 1),
745 batch::Inner(pay(bob, alice, XRP(5)), env.seq(bob)),
746 batch::Msig(bob, {carol, frank}),
748 env.close();
749 }
750
751 // tefBAD_SIGNATURE: Wrong publicKey type
752 {
753 auto const seq = env.seq(alice);
754 auto const batchFee = batch::calcBatchFee(env, 3, 2);
755 env(batch::outer(alice, seq, batchFee, tfAllOrNothing),
756 batch::Inner(pay(alice, bob, XRP(10)), seq + 1),
757 batch::Inner(pay(bob, alice, XRP(5)), env.seq(bob)),
758 batch::Msig(bob, {carol, Account("dave", KeyType::Ed25519)}),
760 env.close();
761 }
762
763 // tefMASTER_DISABLED: Master key disabled
764 {
765 env(regkey(elsa, frank));
766 env(fset(elsa, asfDisableMaster), Sig(elsa));
767 auto const seq = env.seq(alice);
768 auto const batchFee = batch::calcBatchFee(env, 3, 2);
769 env(batch::outer(alice, seq, batchFee, tfAllOrNothing),
770 batch::Inner(pay(alice, bob, XRP(10)), seq + 1),
771 batch::Inner(pay(bob, alice, XRP(5)), env.seq(bob)),
772 batch::Msig(bob, {carol, elsa}),
774 env.close();
775 }
776
777 // tefBAD_SIGNATURE: Signer does not exist
778 {
779 auto const seq = env.seq(alice);
780 auto const batchFee = batch::calcBatchFee(env, 3, 2);
781 env(batch::outer(alice, seq, batchFee, tfAllOrNothing),
782 batch::Inner(pay(alice, bob, XRP(10)), seq + 1),
783 batch::Inner(pay(bob, alice, XRP(5)), env.seq(bob)),
784 batch::Msig(bob, {carol, phantom}),
786 env.close();
787 }
788
789 // tefBAD_SIGNATURE: Signer has not enabled RegularKey
790 {
791 auto const seq = env.seq(alice);
792 auto const batchFee = batch::calcBatchFee(env, 3, 2);
793 Account const davo{"davo", KeyType::Ed25519};
794 env(batch::outer(alice, seq, batchFee, tfAllOrNothing),
795 batch::Inner(pay(alice, bob, XRP(10)), seq + 1),
796 batch::Inner(pay(bob, alice, XRP(5)), env.seq(bob)),
797 batch::Msig(bob, {carol, Reg{dave, davo}}),
799 env.close();
800 }
801
802 // tefBAD_SIGNATURE: Wrong RegularKey Set
803 {
804 env(regkey(dave, frank));
805 auto const seq = env.seq(alice);
806 auto const batchFee = batch::calcBatchFee(env, 3, 2);
807 Account const davo{"davo", KeyType::Ed25519};
808 env(batch::outer(alice, seq, batchFee, tfAllOrNothing),
809 batch::Inner(pay(alice, bob, XRP(10)), seq + 1),
810 batch::Inner(pay(bob, alice, XRP(5)), env.seq(bob)),
811 batch::Msig(bob, {carol, Reg{dave, davo}}),
812 Ter(tefBAD_SIGNATURE));
813 env.close();
814 }
815
816 // tefBAD_QUORUM
817 {
818 auto const seq = env.seq(alice);
819 auto const batchFee = batch::calcBatchFee(env, 2, 2);
820 env(batch::outer(alice, seq, batchFee, tfAllOrNothing),
821 batch::Inner(pay(alice, bob, XRP(10)), seq + 1),
822 batch::Inner(pay(bob, alice, XRP(5)), env.seq(bob)),
823 batch::Msig(bob, {carol}),
825 env.close();
826 }
827
828 // tesSUCCESS: BatchSigners.Signers
829 {
830 auto const seq = env.seq(alice);
831 auto const batchFee = batch::calcBatchFee(env, 3, 2);
832 env(batch::outer(alice, seq, batchFee, tfAllOrNothing),
833 batch::Inner(pay(alice, bob, XRP(10)), seq + 1),
834 batch::Inner(pay(bob, alice, XRP(5)), env.seq(bob)),
835 batch::Msig(bob, {carol, dave}),
836 Ter(tesSUCCESS));
837 env.close();
838 }
839
840 // tesSUCCESS: Multisign + BatchSigners.Signers
841 {
842 auto const seq = env.seq(alice);
843 auto const batchFee = batch::calcBatchFee(env, 4, 2);
844 env(batch::outer(alice, seq, batchFee, tfAllOrNothing),
845 batch::Inner(pay(alice, bob, XRP(10)), seq + 1),
846 batch::Inner(pay(bob, alice, XRP(5)), env.seq(bob)),
847 batch::Msig(bob, {carol, dave}),
848 Msig(bob, carol),
849 Ter(tesSUCCESS));
850 env.close();
851 }
852
853 //----------------------------------------------------------------------
854 // checkBatchSign.checkSingleSign
855
856 // tefBAD_AUTH: Inner Account is not signer
857 {
858 auto const ledSeq = env.current()->seq();
859 auto const seq = env.seq(alice);
860 auto const batchFee = batch::calcBatchFee(env, 1, 2);
861 env(batch::outer(alice, seq, batchFee, tfAllOrNothing),
862 batch::Inner(pay(alice, phantom, XRP(1000)), seq + 1),
863 batch::Inner(noop(phantom), ledSeq),
864 batch::Sig(Reg{phantom, carol}),
866 env.close();
867 }
868
869 // tefBAD_AUTH: Account is not signer
870 {
871 auto const ledSeq = env.current()->seq();
872 auto const seq = env.seq(alice);
873 auto const batchFee = batch::calcBatchFee(env, 1, 2);
874 env(batch::outer(alice, seq, batchFee, tfAllOrNothing),
875 batch::Inner(pay(alice, bob, XRP(1000)), seq + 1),
876 batch::Inner(noop(bob), ledSeq),
877 batch::Sig(Reg{bob, carol}),
879 env.close();
880 }
881
882 // tesSUCCESS: Signed With Regular Key
883 {
884 env(regkey(bob, carol));
885 auto const seq = env.seq(alice);
886 auto const batchFee = batch::calcBatchFee(env, 1, 2);
887 env(batch::outer(alice, seq, batchFee, tfAllOrNothing),
888 batch::Inner(pay(alice, bob, XRP(1)), seq + 1),
889 batch::Inner(pay(bob, alice, XRP(2)), env.seq(bob)),
890 batch::Sig(Reg{bob, carol}),
891 Ter(tesSUCCESS));
892 env.close();
893 }
894
895 // tesSUCCESS: Signed With Master Key
896 {
897 auto const seq = env.seq(alice);
898 auto const batchFee = batch::calcBatchFee(env, 1, 2);
899 env(batch::outer(alice, seq, batchFee, tfAllOrNothing),
900 batch::Inner(pay(alice, bob, XRP(1)), seq + 1),
901 batch::Inner(pay(bob, alice, XRP(2)), env.seq(bob)),
902 batch::Sig(bob),
903 Ter(tesSUCCESS));
904 env.close();
905 }
906
907 // tefMASTER_DISABLED: Signed With Master Key Disabled
908 {
909 env(regkey(bob, carol));
910 env(fset(bob, asfDisableMaster), Sig(bob));
911 auto const seq = env.seq(alice);
912 auto const batchFee = batch::calcBatchFee(env, 1, 2);
913 env(batch::outer(alice, seq, batchFee, tfAllOrNothing),
914 batch::Inner(pay(alice, bob, XRP(1)), seq + 1),
915 batch::Inner(pay(bob, alice, XRP(2)), env.seq(bob)),
916 batch::Sig(bob),
918 env.close();
919 }
920 }
921
922 void
924 {
925 testcase("bad raw txn");
926
927 using namespace test::jtx;
928 using namespace std::literals;
929
930 Env env{*this, features};
931
932 auto const alice = Account("alice");
933 auto const bob = Account("bob");
934
935 env.fund(XRP(10000), alice, bob);
936
937 // An inner missing a required field can no longer be submitted: the
938 // outer STTx builds and validates each inner at construction, so
939 // building the batch (as signing does) throws. Returns true if the
940 // build fails.
941 auto batchCtorFails = [&](json::StaticString const& field) -> bool {
942 auto const batchFee = batch::calcBatchFee(env, 1, 2);
943 auto const seq = env.seq(alice);
944 auto tx1 = batch::Inner(pay(alice, bob, XRP(10)), seq + 1);
945 tx1.removeMember(field);
946 try
947 {
948 // Env::st swallows the construction failure and yields a null
949 // stx, so a malformed inner shows up as no transaction built.
950 auto const jt = env.jtnofill(
951 batch::outer(alice, seq, batchFee, tfAllOrNothing),
952 tx1,
953 batch::Inner(pay(alice, bob, XRP(10)), seq + 2));
954 return jt.stx == nullptr;
955 }
956 catch (std::exception const&)
957 {
958 return true;
959 }
960 };
961
962 // Invalid: sfTransactionType
963 BEAST_EXPECT(batchCtorFails(jss::TransactionType));
964 // Invalid: sfAccount
965 BEAST_EXPECT(batchCtorFails(jss::Account));
966 // Invalid: sfSequence
967 BEAST_EXPECT(batchCtorFails(jss::Sequence));
968 // Invalid: sfFee
969 BEAST_EXPECT(batchCtorFails(jss::Fee));
970 // Invalid: sfSigningPubKey
971 BEAST_EXPECT(batchCtorFails(jss::SigningPubKey));
972
973 // Inner OfferCreate with MPT TakerPays. Valid under featureMPTokensV2.
974 {
975 MPTIssue const issue(makeMptID(1, alice));
976 STAmount const mptAmt{issue, UINT64_C(100)};
977
978 auto const batchFee = batch::calcBatchFee(env, 0, 2);
979 auto const seq = env.seq(alice);
980
981 json::Value tx1;
982 tx1[jss::TransactionType] = jss::OfferCreate;
983 tx1[jss::Account] = alice.human();
984 tx1[jss::TakerPays] = mptAmt.getJson(JsonOptions::Values::None);
985 tx1[jss::TakerGets] = XRP(10).value().getJson(JsonOptions::Values::None);
986
987 env(batch::outer(alice, seq, batchFee, tfAllOrNothing),
988 batch::Inner(tx1, seq + 1),
989 batch::Inner(pay(alice, bob, XRP(1)), seq + 2),
990 Ter(tesSUCCESS));
991 env.close();
992 }
993
994 // Invalid: inner txn with invalid memo (non-URL-safe MemoType)
995 {
996 auto const batchFee = batch::calcBatchFee(env, 0, 2);
997 auto const seq = env.seq(alice);
998
999 auto tx1 = pay(alice, bob, XRP(10));
1000 auto& ma = tx1["Memos"];
1001 auto& mi = ma[ma.size()];
1002 auto& m = mi["Memo"];
1003 m["MemoType"] = strHex(std::string("\x01\x02\x03", 3));
1004 m["MemoData"] = strHex(std::string("test"));
1005
1006 env(batch::outer(alice, seq, batchFee, tfAllOrNothing),
1007 batch::Inner(tx1, seq + 1),
1008 batch::Inner(pay(alice, bob, XRP(1)), seq + 2),
1010 env.close();
1011 }
1012 }
1013
1014 void
1016 {
1017 testcase("bad sequence");
1018
1019 using namespace test::jtx;
1020 using namespace std::literals;
1021
1022 Env env{*this, features};
1023
1024 auto const alice = Account("alice");
1025 auto const bob = Account("bob");
1026 auto const gw = Account("gw");
1027 auto const usd = gw["USD"];
1028
1029 env.fund(XRP(10000), alice, bob, gw);
1030 env.close();
1031 env.trust(usd(1000), alice, bob);
1032 env(pay(gw, alice, usd(100)));
1033 env(pay(gw, bob, usd(100)));
1034 env.close();
1035
1036 env(noop(bob), Ter(tesSUCCESS));
1037 env.close();
1038
1039 // Invalid: Alice Sequence is a past sequence
1040 {
1041 auto const preAliceSeq = env.seq(alice);
1042 auto const preAlice = env.balance(alice);
1043 auto const preAliceUSD = env.balance(alice, usd.issue());
1044 auto const preBobSeq = env.seq(bob);
1045 auto const preBob = env.balance(bob);
1046 auto const preBobUSD = env.balance(bob, usd.issue());
1047
1048 auto const batchFee = batch::calcBatchFee(env, 1, 2);
1049 auto const [txIDs, batchID] = submitBatch(
1050 env,
1051 tesSUCCESS,
1052 batch::outer(alice, preAliceSeq, batchFee, tfAllOrNothing),
1053 batch::Inner(pay(alice, bob, XRP(10)), preAliceSeq - 10),
1054 batch::Inner(pay(bob, alice, XRP(5)), preBobSeq),
1055 batch::Sig(bob));
1056
1057 env.close();
1058 {
1059 std::vector<TestLedgerData> const testCases = {
1060 {.index = 0,
1061 .txType = "Batch",
1062 .result = "tesSUCCESS",
1063 .txHash = batchID,
1064 .batchID = std::nullopt},
1065 };
1066 validateClosedLedger(env, testCases);
1067 }
1068
1069 env.close();
1070 {
1071 // next ledger is empty
1072 std::vector<TestLedgerData> const testCases = {};
1073 validateClosedLedger(env, testCases);
1074 }
1075
1076 // Alice pays fee & Bob should not be affected.
1077 BEAST_EXPECT(env.seq(alice) == preAliceSeq + 1);
1078 BEAST_EXPECT(env.balance(alice) == preAlice - batchFee);
1079 BEAST_EXPECT(env.balance(alice, usd.issue()) == preAliceUSD);
1080 BEAST_EXPECT(env.seq(bob) == preBobSeq);
1081 BEAST_EXPECT(env.balance(bob) == preBob);
1082 BEAST_EXPECT(env.balance(bob, usd.issue()) == preBobUSD);
1083 }
1084
1085 // Invalid: Alice Sequence is a future sequence
1086 {
1087 auto const preAliceSeq = env.seq(alice);
1088 auto const preAlice = env.balance(alice);
1089 auto const preAliceUSD = env.balance(alice, usd.issue());
1090 auto const preBobSeq = env.seq(bob);
1091 auto const preBob = env.balance(bob);
1092 auto const preBobUSD = env.balance(bob, usd.issue());
1093
1094 auto const batchFee = batch::calcBatchFee(env, 1, 2);
1095 auto const [txIDs, batchID] = submitBatch(
1096 env,
1097 tesSUCCESS,
1098 batch::outer(alice, preAliceSeq, batchFee, tfAllOrNothing),
1099 batch::Inner(pay(alice, bob, XRP(10)), preAliceSeq + 10),
1100 batch::Inner(pay(bob, alice, XRP(5)), preBobSeq),
1101 batch::Sig(bob));
1102
1103 env.close();
1104 {
1105 std::vector<TestLedgerData> const testCases = {
1106 {.index = 0,
1107 .txType = "Batch",
1108 .result = "tesSUCCESS",
1109 .txHash = batchID,
1110 .batchID = std::nullopt},
1111 };
1112 validateClosedLedger(env, testCases);
1113 }
1114
1115 env.close();
1116 {
1117 // next ledger is empty
1118 std::vector<TestLedgerData> const testCases = {};
1119 validateClosedLedger(env, testCases);
1120 }
1121
1122 // Alice pays fee & Bob should not be affected.
1123 BEAST_EXPECT(env.seq(alice) == preAliceSeq + 1);
1124 BEAST_EXPECT(env.balance(alice) == preAlice - batchFee);
1125 BEAST_EXPECT(env.balance(alice, usd.issue()) == preAliceUSD);
1126 BEAST_EXPECT(env.seq(bob) == preBobSeq);
1127 BEAST_EXPECT(env.balance(bob) == preBob);
1128 BEAST_EXPECT(env.balance(bob, usd.issue()) == preBobUSD);
1129 }
1130
1131 // Invalid: Bob Sequence is a past sequence
1132 {
1133 auto const preAliceSeq = env.seq(alice);
1134 auto const preAlice = env.balance(alice);
1135 auto const preAliceUSD = env.balance(alice, usd.issue());
1136 auto const preBobSeq = env.seq(bob);
1137 auto const preBob = env.balance(bob);
1138 auto const preBobUSD = env.balance(bob, usd.issue());
1139
1140 auto const batchFee = batch::calcBatchFee(env, 1, 2);
1141 auto const [txIDs, batchID] = submitBatch(
1142 env,
1143 tesSUCCESS,
1144 batch::outer(alice, preAliceSeq, batchFee, tfAllOrNothing),
1145 batch::Inner(pay(alice, bob, XRP(10)), preAliceSeq + 1),
1146 batch::Inner(pay(bob, alice, XRP(5)), preBobSeq - 10),
1147 batch::Sig(bob));
1148
1149 env.close();
1150 {
1151 std::vector<TestLedgerData> const testCases = {
1152 {.index = 0,
1153 .txType = "Batch",
1154 .result = "tesSUCCESS",
1155 .txHash = batchID,
1156 .batchID = std::nullopt},
1157 };
1158 validateClosedLedger(env, testCases);
1159 }
1160
1161 env.close();
1162 {
1163 // next ledger is empty
1164 std::vector<TestLedgerData> const testCases = {};
1165 validateClosedLedger(env, testCases);
1166 }
1167
1168 // Alice pays fee & Bob should not be affected.
1169 BEAST_EXPECT(env.seq(alice) == preAliceSeq + 1);
1170 BEAST_EXPECT(env.balance(alice) == preAlice - batchFee);
1171 BEAST_EXPECT(env.balance(alice, usd.issue()) == preAliceUSD);
1172 BEAST_EXPECT(env.seq(bob) == preBobSeq);
1173 BEAST_EXPECT(env.balance(bob) == preBob);
1174 BEAST_EXPECT(env.balance(bob, usd.issue()) == preBobUSD);
1175 }
1176
1177 // Invalid: Bob Sequence is a future sequence
1178 {
1179 auto const preAliceSeq = env.seq(alice);
1180 auto const preAlice = env.balance(alice);
1181 auto const preAliceUSD = env.balance(alice, usd.issue());
1182 auto const preBobSeq = env.seq(bob);
1183 auto const preBob = env.balance(bob);
1184 auto const preBobUSD = env.balance(bob, usd.issue());
1185
1186 auto const batchFee = batch::calcBatchFee(env, 1, 2);
1187 auto const [txIDs, batchID] = submitBatch(
1188 env,
1189 tesSUCCESS,
1190 batch::outer(alice, preAliceSeq, batchFee, tfAllOrNothing),
1191 batch::Inner(pay(alice, bob, XRP(10)), preAliceSeq + 1),
1192 batch::Inner(pay(bob, alice, XRP(5)), preBobSeq + 10),
1193 batch::Sig(bob));
1194
1195 env.close();
1196 {
1197 std::vector<TestLedgerData> const testCases = {
1198 {.index = 0,
1199 .txType = "Batch",
1200 .result = "tesSUCCESS",
1201 .txHash = batchID,
1202 .batchID = std::nullopt},
1203 };
1204 validateClosedLedger(env, testCases);
1205 }
1206
1207 env.close();
1208 {
1209 // next ledger is empty
1210 std::vector<TestLedgerData> const testCases = {};
1211 validateClosedLedger(env, testCases);
1212 }
1213
1214 // Alice pays fee & Bob should not be affected.
1215 BEAST_EXPECT(env.seq(alice) == preAliceSeq + 1);
1216 BEAST_EXPECT(env.balance(alice) == preAlice - batchFee);
1217 BEAST_EXPECT(env.balance(alice, usd.issue()) == preAliceUSD);
1218 BEAST_EXPECT(env.seq(bob) == preBobSeq);
1219 BEAST_EXPECT(env.balance(bob) == preBob);
1220 BEAST_EXPECT(env.balance(bob, usd.issue()) == preBobUSD);
1221 }
1222
1223 // Invalid: Outer and Inner Sequence are the same
1224 {
1225 auto const preAliceSeq = env.seq(alice);
1226 auto const preAlice = env.balance(alice);
1227 auto const preAliceUSD = env.balance(alice, usd.issue());
1228 auto const preBobSeq = env.seq(bob);
1229 auto const preBob = env.balance(bob);
1230 auto const preBobUSD = env.balance(bob, usd.issue());
1231
1232 auto const batchFee = batch::calcBatchFee(env, 1, 2);
1233 auto const [txIDs, batchID] = submitBatch(
1234 env,
1235 tesSUCCESS,
1236 batch::outer(alice, preAliceSeq, batchFee, tfAllOrNothing),
1237 batch::Inner(pay(alice, bob, XRP(10)), preAliceSeq),
1238 batch::Inner(pay(bob, alice, XRP(5)), preBobSeq),
1239 batch::Sig(bob));
1240
1241 env.close();
1242 {
1243 std::vector<TestLedgerData> const testCases = {
1244 {.index = 0,
1245 .txType = "Batch",
1246 .result = "tesSUCCESS",
1247 .txHash = batchID,
1248 .batchID = std::nullopt},
1249 };
1250 validateClosedLedger(env, testCases);
1251 }
1252
1253 env.close();
1254 {
1255 // next ledger is empty
1256 std::vector<TestLedgerData> const testCases = {};
1257 validateClosedLedger(env, testCases);
1258 }
1259
1260 // Alice pays fee & Bob should not be affected.
1261 BEAST_EXPECT(env.seq(alice) == preAliceSeq + 1);
1262 BEAST_EXPECT(env.balance(alice) == preAlice - batchFee);
1263 BEAST_EXPECT(env.balance(alice, usd.issue()) == preAliceUSD);
1264 BEAST_EXPECT(env.seq(bob) == preBobSeq);
1265 BEAST_EXPECT(env.balance(bob) == preBob);
1266 BEAST_EXPECT(env.balance(bob, usd.issue()) == preBobUSD);
1267 }
1268 }
1269
1270 void
1272 {
1273 testcase("bad outer fee");
1274
1275 using namespace test::jtx;
1276 using namespace std::literals;
1277
1278 // Bad Fee Without Signer
1279 {
1280 Env env{*this, features};
1281
1282 auto const alice = Account("alice");
1283 auto const bob = Account("bob");
1284 env.fund(XRP(10000), alice, bob);
1285 env.close();
1286
1287 env(noop(bob), Ter(tesSUCCESS));
1288 env.close();
1289
1290 // Bad Fee: Should be batch::calcBatchFee(env, 0, 2)
1291 auto const batchFee = batch::calcBatchFee(env, 0, 1);
1292 auto const aliceSeq = env.seq(alice);
1293 env(batch::outer(alice, aliceSeq, batchFee, tfAllOrNothing),
1294 batch::Inner(pay(alice, bob, XRP(10)), aliceSeq + 1),
1295 batch::Inner(pay(alice, bob, XRP(15)), aliceSeq + 2),
1297 env.close();
1298 }
1299
1300 // Bad Fee With MultiSign
1301 {
1302 Env env{*this, features};
1303
1304 auto const alice = Account("alice");
1305 auto const bob = Account("bob");
1306 auto const carol = Account("carol");
1307 env.fund(XRP(10000), alice, bob, carol);
1308 env.close();
1309
1310 env(noop(bob), Ter(tesSUCCESS));
1311 env.close();
1312
1313 env(signers(alice, 2, {{bob, 1}, {carol, 1}}));
1314 env.close();
1315
1316 // Bad Fee: Should be batch::calcBatchFee(env, 2, 2)
1317 auto const batchFee = batch::calcBatchFee(env, 1, 2);
1318 auto const aliceSeq = env.seq(alice);
1319 env(batch::outer(alice, aliceSeq, batchFee, tfAllOrNothing),
1320 batch::Inner(pay(alice, bob, XRP(10)), aliceSeq + 1),
1321 batch::Inner(pay(alice, bob, XRP(15)), aliceSeq + 2),
1322 Msig(bob, carol),
1324 env.close();
1325 }
1326
1327 // Bad Fee With MultiSign + BatchSigners
1328 {
1329 Env env{*this, features};
1330
1331 auto const alice = Account("alice");
1332 auto const bob = Account("bob");
1333 auto const carol = Account("carol");
1334 env.fund(XRP(10000), alice, bob, carol);
1335 env.close();
1336
1337 env(noop(bob), Ter(tesSUCCESS));
1338 env.close();
1339
1340 env(signers(alice, 2, {{bob, 1}, {carol, 1}}));
1341 env.close();
1342
1343 // Bad Fee: Should be batch::calcBatchFee(env, 3, 2)
1344 auto const batchFee = batch::calcBatchFee(env, 2, 2);
1345 auto const aliceSeq = env.seq(alice);
1346 auto const bobSeq = env.seq(bob);
1347 env(batch::outer(alice, aliceSeq, batchFee, tfAllOrNothing),
1348 batch::Inner(pay(alice, bob, XRP(10)), aliceSeq + 1),
1349 batch::Inner(pay(bob, alice, XRP(5)), bobSeq),
1350 batch::Sig(bob),
1351 Msig(bob, carol),
1353 env.close();
1354 }
1355
1356 // Bad Fee With MultiSign + BatchSigners.Signers
1357 {
1358 Env env{*this, features};
1359
1360 auto const alice = Account("alice");
1361 auto const bob = Account("bob");
1362 auto const carol = Account("carol");
1363 env.fund(XRP(10000), alice, bob, carol);
1364 env.close();
1365
1366 env(noop(bob), Ter(tesSUCCESS));
1367 env.close();
1368
1369 env(signers(alice, 2, {{bob, 1}, {carol, 1}}));
1370 env.close();
1371
1372 env(signers(bob, 2, {{alice, 1}, {carol, 1}}));
1373 env.close();
1374
1375 // Bad Fee: Should be batch::calcBatchFee(env, 4, 2)
1376 auto const batchFee = batch::calcBatchFee(env, 3, 2);
1377 auto const aliceSeq = env.seq(alice);
1378 auto const bobSeq = env.seq(bob);
1379 env(batch::outer(alice, aliceSeq, batchFee, tfAllOrNothing),
1380 batch::Inner(pay(alice, bob, XRP(10)), aliceSeq + 1),
1381 batch::Inner(pay(bob, alice, XRP(5)), bobSeq),
1382 batch::Msig(bob, {alice, carol}),
1383 Msig(bob, carol),
1385 env.close();
1386 }
1387
1388 // Bad Fee With BatchSigners
1389 {
1390 Env env{*this, features};
1391
1392 auto const alice = Account("alice");
1393 auto const bob = Account("bob");
1394 env.fund(XRP(10000), alice, bob);
1395 env.close();
1396
1397 env(noop(bob), Ter(tesSUCCESS));
1398 env.close();
1399
1400 // Bad Fee: Should be batch::calcBatchFee(env, 1, 2)
1401 auto const batchFee = batch::calcBatchFee(env, 0, 2);
1402 auto const aliceSeq = env.seq(alice);
1403 auto const bobSeq = env.seq(bob);
1404 env(batch::outer(alice, aliceSeq, batchFee, tfAllOrNothing),
1405 batch::Inner(pay(alice, bob, XRP(10)), aliceSeq + 1),
1406 batch::Inner(pay(bob, alice, XRP(5)), bobSeq),
1407 batch::Sig(bob),
1409 env.close();
1410 }
1411
1412 // Bad Fee Dynamic Fee Calculation
1413 {
1414 Env env{*this, features};
1415
1416 auto const alice = Account("alice");
1417 auto const bob = Account("bob");
1418 auto const gw = Account("gw");
1419 auto const usd = gw["USD"];
1420
1421 env.fund(XRP(10000), alice, bob, gw);
1422 env.close();
1423 auto const ammCreate = [&alice](STAmount const& amount, STAmount const& amount2) {
1424 json::Value jv;
1425 jv[jss::Account] = alice.human();
1426 jv[jss::Amount] = amount.getJson(JsonOptions::Values::None);
1427 jv[jss::Amount2] = amount2.getJson(JsonOptions::Values::None);
1428 jv[jss::TradingFee] = 0;
1429 jv[jss::TransactionType] = jss::AMMCreate;
1430 return jv;
1431 };
1432
1433 auto const batchFee = batch::calcBatchFee(env, 0, 2);
1434 auto const seq = env.seq(alice);
1435 env(batch::outer(alice, seq, batchFee, tfAllOrNothing),
1436 batch::Inner(ammCreate(XRP(10), usd(10)), seq + 1),
1437 batch::Inner(pay(alice, bob, XRP(10)), seq + 2),
1439 env.close();
1440 }
1441 }
1442
1443 void
1445 {
1446 testcase("calculate base fee");
1447
1448 using namespace test::jtx;
1449 using namespace std::literals;
1450
1451 // telENV_RPC_FAILED: Batch: txns array exceeds 8 entries.
1452 {
1453 Env env{*this, features};
1454
1455 auto const alice = Account("alice");
1456 auto const bob = Account("bob");
1457 env.fund(XRP(10000), alice, bob);
1458 env.close();
1459
1460 auto const batchFee = batch::calcBatchFee(env, 0, 9);
1461 auto const aliceSeq = env.seq(alice);
1462 env(batch::outer(alice, aliceSeq, batchFee, tfAllOrNothing),
1463 batch::Inner(pay(alice, bob, XRP(1)), aliceSeq),
1464 batch::Inner(pay(alice, bob, XRP(1)), aliceSeq),
1465 batch::Inner(pay(alice, bob, XRP(1)), aliceSeq),
1466 batch::Inner(pay(alice, bob, XRP(1)), aliceSeq),
1467 batch::Inner(pay(alice, bob, XRP(1)), aliceSeq),
1468 batch::Inner(pay(alice, bob, XRP(1)), aliceSeq),
1469 batch::Inner(pay(alice, bob, XRP(1)), aliceSeq),
1470 batch::Inner(pay(alice, bob, XRP(1)), aliceSeq),
1471 batch::Inner(pay(alice, bob, XRP(1)), aliceSeq),
1472 Ter(temMALFORMED));
1473 env.close();
1474 }
1475
1476 // An oversized batch (more than kMaxBatchTxCount inners) fails STTx
1477 // construction, so it never reaches apply or checkValidity - Env::st
1478 // swallows the failure and yields a null stx.
1479 {
1480 Env env{*this, features};
1481
1482 auto const alice = Account("alice");
1483 auto const bob = Account("bob");
1484 env.fund(XRP(10000), alice, bob);
1485 env.close();
1486
1487 auto const batchFee = batch::calcBatchFee(env, 0, 9);
1488 auto const aliceSeq = env.seq(alice);
1489 auto jt = env.jtnofill(
1490 batch::outer(alice, aliceSeq, batchFee, tfAllOrNothing),
1491 batch::Inner(pay(alice, bob, XRP(1)), aliceSeq),
1492 batch::Inner(pay(alice, bob, XRP(1)), aliceSeq),
1493 batch::Inner(pay(alice, bob, XRP(1)), aliceSeq),
1494 batch::Inner(pay(alice, bob, XRP(1)), aliceSeq),
1495 batch::Inner(pay(alice, bob, XRP(1)), aliceSeq),
1496 batch::Inner(pay(alice, bob, XRP(1)), aliceSeq),
1497 batch::Inner(pay(alice, bob, XRP(1)), aliceSeq),
1498 batch::Inner(pay(alice, bob, XRP(1)), aliceSeq),
1499 batch::Inner(pay(alice, bob, XRP(1)), aliceSeq));
1500
1501 BEAST_EXPECT(jt.stx == nullptr);
1502 }
1503
1504 // An oversized batch cannot slip through as validly signed even when it
1505 // carries a BatchSigners array: the oversized inner array fails STTx
1506 // construction before any signature is checked, so the batch can't be
1507 // built at all (building it - as signing does - throws).
1508 {
1509 Env env{*this, features};
1510
1511 auto const alice = Account("alice");
1512 auto const bob = Account("bob");
1513 env.fund(XRP(10000), alice, bob);
1514 env.close();
1515
1516 auto const aliceSeq = env.seq(alice);
1517 auto const batchFee = batch::calcBatchFee(env, kMaxBatchSigners + 1, 9);
1518 bool threw = false;
1519 try
1520 {
1521 env.jtnofill(
1522 batch::outer(alice, aliceSeq, batchFee, tfAllOrNothing),
1523 batch::Inner(pay(alice, bob, XRP(1)), aliceSeq),
1524 batch::Inner(pay(alice, bob, XRP(1)), aliceSeq),
1525 batch::Inner(pay(alice, bob, XRP(1)), aliceSeq),
1526 batch::Inner(pay(alice, bob, XRP(1)), aliceSeq),
1527 batch::Inner(pay(alice, bob, XRP(1)), aliceSeq),
1528 batch::Inner(pay(alice, bob, XRP(1)), aliceSeq),
1529 batch::Inner(pay(alice, bob, XRP(1)), aliceSeq),
1530 batch::Inner(pay(alice, bob, XRP(1)), aliceSeq),
1531 batch::Inner(pay(alice, bob, XRP(1)), aliceSeq),
1533 }
1534 catch (std::exception const&)
1535 {
1536 threw = true;
1537 }
1538 BEAST_EXPECT(threw);
1539 }
1540
1541 // Regression: the relay-boundary local check (isBatchRawTransactionOkay)
1542 // caps the signers array at kMaxBatchSigners, matching Batch::preflight -
1543 // not kMaxBatchTxCount. A batch with more than kMaxBatchTxCount but at
1544 // most kMaxBatchSigners signers must get past local checks and reach
1545 // signer validation. Before the cap was aligned it was wrongly rejected
1546 // at the boundary (telENV_RPC_FAILED) before preflight ran; now it
1547 // reaches preflightSigValidated and fails there as extra signers
1548 // (temBAD_SIGNER) rather than being dropped pre-engine.
1549 {
1550 Env env{*this, features};
1551
1552 auto const alice = Account("alice");
1553 auto const bob = Account("bob");
1554 env.fund(XRP(10000), alice, bob);
1555 env.close();
1556
1557 // Over kMaxBatchTxCount (8) but within kMaxBatchSigners (24).
1558 std::size_t const signerCount = kMaxBatchTxCount + 1;
1559
1560 auto const aliceSeq = env.seq(alice);
1561 auto const batchFee = batch::calcBatchFee(env, signerCount, 2);
1562 env(batch::outer(alice, aliceSeq, batchFee, tfAllOrNothing),
1563 batch::Inner(pay(alice, bob, XRP(10)), aliceSeq + 1),
1564 batch::Inner(pay(alice, bob, XRP(5)), aliceSeq + 2),
1565 batch::Sig(std::vector<Reg>(signerCount, bob)),
1567 env.close();
1568 }
1569
1570 // temARRAY_TOO_LARGE: Batch preflight: signers array exceeds
1571 // kMaxBatchSigners entries.
1572 {
1573 Env env{*this, features};
1574
1575 auto const alice = Account("alice");
1576 auto const bob = Account("bob");
1577 env.fund(XRP(10000), alice, bob);
1578 env.close();
1579
1580 auto const batchFee = batch::calcBatchFee(env, kMaxBatchSigners + 1, 2);
1581 auto const aliceSeq = env.seq(alice);
1582 auto jt = env.jtnofill(
1583 batch::outer(alice, aliceSeq, batchFee, tfAllOrNothing),
1584 batch::Inner(pay(alice, bob, XRP(10)), aliceSeq + 1),
1585 batch::Inner(pay(alice, bob, XRP(5)), aliceSeq + 2),
1587
1588 env.app().getOpenLedger().modify([&](OpenView& view, beast::Journal j) {
1589 auto const result = xrpl::apply(env.app(), view, *jt.stx, TapNone, j);
1590 BEAST_EXPECT(!result.applied && result.ter == temARRAY_TOO_LARGE);
1591 return result.applied;
1592 });
1593
1594 // Regression (uncapped batch-signer verification): the relay
1595 // boundary (checkValidity) rejects the oversized signers array via
1596 // the checkBatchSign guard, BEFORE verifying a single signature.
1597 auto const [valid, reason] =
1598 xrpl::checkValidity(env.app().getHashRouter(), *jt.stx, env.current()->rules());
1599 BEAST_EXPECT(valid == xrpl::Validity::SigBad);
1600 BEAST_EXPECT(reason == "BatchSigners array exceeds max entries.");
1601 }
1602 }
1603
1604 void
1606 {
1607 testcase("all or nothing");
1608
1609 using namespace test::jtx;
1610 using namespace std::literals;
1611
1612 Env env{*this, features};
1613
1614 auto const alice = Account("alice");
1615 auto const bob = Account("bob");
1616 auto const gw = Account("gw");
1617 auto const usd = gw["USD"];
1618 env.fund(XRP(10000), alice, bob, gw);
1619 env.close();
1620
1621 // all
1622 {
1623 auto const preAlice = env.balance(alice);
1624 auto const preBob = env.balance(bob);
1625
1626 auto const batchFee = batch::calcBatchFee(env, 0, 2);
1627 auto const seq = env.seq(alice);
1628 auto const [txIDs, batchID] = submitBatch(
1629 env,
1630 tesSUCCESS,
1631 batch::outer(alice, seq, batchFee, tfAllOrNothing),
1632 batch::Inner(pay(alice, bob, XRP(1)), seq + 1),
1633 batch::Inner(pay(alice, bob, XRP(2)), seq + 2));
1634 env.close();
1635
1636 std::vector<TestLedgerData> const testCases = {
1637 {.index = 0,
1638 .txType = "Batch",
1639 .result = "tesSUCCESS",
1640 .txHash = batchID,
1641 .batchID = std::nullopt},
1642 {.index = 1,
1643 .txType = "Payment",
1644 .result = "tesSUCCESS",
1645 .txHash = txIDs[0],
1646 .batchID = batchID},
1647 {.index = 2,
1648 .txType = "Payment",
1649 .result = "tesSUCCESS",
1650 .txHash = txIDs[1],
1651 .batchID = batchID},
1652 };
1653 validateClosedLedger(env, testCases);
1654
1655 // Alice consumes sequences (# of txns)
1656 BEAST_EXPECT(env.seq(alice) == seq + 3);
1657
1658 // Alice pays XRP & Fee; Bob receives XRP
1659 BEAST_EXPECT(env.balance(alice) == preAlice - XRP(3) - batchFee);
1660 BEAST_EXPECT(env.balance(bob) == preBob + XRP(3));
1661 }
1662
1663 // tec failure
1664 {
1665 auto const preAlice = env.balance(alice);
1666 auto const preBob = env.balance(bob);
1667
1668 auto const batchFee = batch::calcBatchFee(env, 0, 2);
1669 auto const seq = env.seq(alice);
1670
1671 auto const [txIDs, batchID] = submitBatch(
1672 env,
1673 tesSUCCESS,
1674 batch::outer(alice, seq, batchFee, tfAllOrNothing),
1675 batch::Inner(pay(alice, bob, XRP(1)), seq + 1),
1676 // tecUNFUNDED_PAYMENT: alice does not have enough XRP
1677 batch::Inner(pay(alice, bob, XRP(9999)), seq + 2));
1678 env.close();
1679
1680 std::vector<TestLedgerData> const testCases = {
1681 {.index = 0,
1682 .txType = "Batch",
1683 .result = "tesSUCCESS",
1684 .txHash = batchID,
1685 .batchID = std::nullopt},
1686 };
1687 validateClosedLedger(env, testCases);
1688
1689 // Alice consumes sequence
1690 BEAST_EXPECT(env.seq(alice) == seq + 1);
1691
1692 // Alice pays Fee; Bob should not be affected
1693 BEAST_EXPECT(env.balance(alice) == preAlice - batchFee);
1694 BEAST_EXPECT(env.balance(bob) == preBob);
1695 }
1696
1697 // tef failure
1698 {
1699 auto const preAlice = env.balance(alice);
1700 auto const preBob = env.balance(bob);
1701
1702 auto const batchFee = batch::calcBatchFee(env, 0, 2);
1703 auto const seq = env.seq(alice);
1704 auto const [txIDs, batchID] = submitBatch(
1705 env,
1706 tesSUCCESS,
1707 batch::outer(alice, seq, batchFee, tfAllOrNothing),
1708 batch::Inner(pay(alice, bob, XRP(1)), seq + 1),
1709 // tefNO_AUTH_REQUIRED: trustline auth is not required
1710 batch::Inner(trust(alice, usd(1000), tfSetfAuth), seq + 2));
1711 env.close();
1712
1713 std::vector<TestLedgerData> const testCases = {
1714 {.index = 0,
1715 .txType = "Batch",
1716 .result = "tesSUCCESS",
1717 .txHash = batchID,
1718 .batchID = std::nullopt},
1719 };
1720 validateClosedLedger(env, testCases);
1721
1722 // Alice consumes sequence
1723 BEAST_EXPECT(env.seq(alice) == seq + 1);
1724
1725 // Alice pays Fee; Bob should not be affected
1726 BEAST_EXPECT(env.balance(alice) == preAlice - batchFee);
1727 BEAST_EXPECT(env.balance(bob) == preBob);
1728 }
1729
1730 // ter failure
1731 {
1732 auto const preAlice = env.balance(alice);
1733 auto const preBob = env.balance(bob);
1734
1735 auto const batchFee = batch::calcBatchFee(env, 0, 2);
1736 auto const seq = env.seq(alice);
1737 auto const [txIDs, batchID] = submitBatch(
1738 env,
1739 tesSUCCESS,
1740 batch::outer(alice, seq, batchFee, tfAllOrNothing),
1741 batch::Inner(pay(alice, bob, XRP(1)), seq + 1),
1742 // terPRE_TICKET: ticket does not exist
1743 batch::Inner(trust(alice, usd(1000), tfSetfAuth), 0, seq + 2));
1744 env.close();
1745
1746 std::vector<TestLedgerData> const testCases = {
1747 {.index = 0,
1748 .txType = "Batch",
1749 .result = "tesSUCCESS",
1750 .txHash = batchID,
1751 .batchID = std::nullopt},
1752 };
1753 validateClosedLedger(env, testCases);
1754
1755 // Alice consumes sequence
1756 BEAST_EXPECT(env.seq(alice) == seq + 1);
1757
1758 // Alice pays Fee; Bob should not be affected
1759 BEAST_EXPECT(env.balance(alice) == preAlice - batchFee);
1760 BEAST_EXPECT(env.balance(bob) == preBob);
1761 }
1762 }
1763
1764 void
1766 {
1767 testcase("only one");
1768
1769 using namespace test::jtx;
1770 using namespace std::literals;
1771
1772 Env env{*this, features};
1773
1774 auto const alice = Account("alice");
1775 auto const bob = Account("bob");
1776 auto const carol = Account("carol");
1777 auto const dave = Account("dave");
1778 auto const gw = Account("gw");
1779 auto const usd = gw["USD"];
1780 env.fund(XRP(10000), alice, bob, carol, dave, gw);
1781 env.close();
1782
1783 // all transactions fail
1784 {
1785 auto const preAlice = env.balance(alice);
1786 auto const preBob = env.balance(bob);
1787
1788 auto const batchFee = batch::calcBatchFee(env, 0, 3);
1789 auto const seq = env.seq(alice);
1790 auto const [txIDs, batchID] = submitBatch(
1791 env,
1792 tesSUCCESS,
1793 batch::outer(alice, seq, batchFee, tfOnlyOne),
1794 // tecUNFUNDED_PAYMENT: alice does not have enough XRP
1795 batch::Inner(pay(alice, bob, XRP(9999)), seq + 1),
1796 // tecUNFUNDED_PAYMENT: alice does not have enough XRP
1797 batch::Inner(pay(alice, bob, XRP(9999)), seq + 2),
1798 // tecUNFUNDED_PAYMENT: alice does not have enough XRP
1799 batch::Inner(pay(alice, bob, XRP(9999)), seq + 3));
1800 env.close();
1801
1802 std::vector<TestLedgerData> const testCases = {
1803 {.index = 0,
1804 .txType = "Batch",
1805 .result = "tesSUCCESS",
1806 .txHash = batchID,
1807 .batchID = std::nullopt},
1808 {.index = 1,
1809 .txType = "Payment",
1810 .result = "tecUNFUNDED_PAYMENT",
1811 .txHash = txIDs[0],
1812 .batchID = batchID},
1813 {.index = 2,
1814 .txType = "Payment",
1815 .result = "tecUNFUNDED_PAYMENT",
1816 .txHash = txIDs[1],
1817 .batchID = batchID},
1818 {.index = 3,
1819 .txType = "Payment",
1820 .result = "tecUNFUNDED_PAYMENT",
1821 .txHash = txIDs[2],
1822 .batchID = batchID},
1823 };
1824 validateClosedLedger(env, testCases);
1825
1826 // Alice consumes sequences (# of txns)
1827 BEAST_EXPECT(env.seq(alice) == seq + 4);
1828
1829 // Alice pays XRP & Fee; Bob receives XRP
1830 BEAST_EXPECT(env.balance(alice) == preAlice - batchFee);
1831 BEAST_EXPECT(env.balance(bob) == preBob);
1832 }
1833
1834 // first transaction fails
1835 {
1836 auto const preAlice = env.balance(alice);
1837 auto const preBob = env.balance(bob);
1838
1839 auto const batchFee = batch::calcBatchFee(env, 0, 3);
1840 auto const seq = env.seq(alice);
1841 auto const [txIDs, batchID] = submitBatch(
1842 env,
1843 tesSUCCESS,
1844 batch::outer(alice, seq, batchFee, tfOnlyOne),
1845 // tecUNFUNDED_PAYMENT: alice does not have enough XRP
1846 batch::Inner(pay(alice, bob, XRP(9999)), seq + 1),
1847 batch::Inner(pay(alice, bob, XRP(1)), seq + 2),
1848 batch::Inner(pay(alice, bob, XRP(2)), seq + 3));
1849 env.close();
1850
1851 std::vector<TestLedgerData> const testCases = {
1852 {.index = 0,
1853 .txType = "Batch",
1854 .result = "tesSUCCESS",
1855 .txHash = batchID,
1856 .batchID = std::nullopt},
1857 {.index = 1,
1858 .txType = "Payment",
1859 .result = "tecUNFUNDED_PAYMENT",
1860 .txHash = txIDs[0],
1861 .batchID = batchID},
1862 {.index = 2,
1863 .txType = "Payment",
1864 .result = "tesSUCCESS",
1865 .txHash = txIDs[1],
1866 .batchID = batchID},
1867 };
1868 validateClosedLedger(env, testCases);
1869
1870 // Alice consumes sequences (# of txns)
1871 BEAST_EXPECT(env.seq(alice) == seq + 3);
1872
1873 // Alice pays XRP & Fee; Bob receives XRP
1874 BEAST_EXPECT(env.balance(alice) == preAlice - XRP(1) - batchFee);
1875 BEAST_EXPECT(env.balance(bob) == preBob + XRP(1));
1876 }
1877
1878 // tec failure
1879 {
1880 auto const preAlice = env.balance(alice);
1881 auto const preBob = env.balance(bob);
1882
1883 auto const batchFee = batch::calcBatchFee(env, 0, 3);
1884 auto const seq = env.seq(alice);
1885 auto const [txIDs, batchID] = submitBatch(
1886 env,
1887 tesSUCCESS,
1888 batch::outer(alice, seq, batchFee, tfOnlyOne),
1889 batch::Inner(pay(alice, bob, XRP(1)), seq + 1),
1890 // tecUNFUNDED_PAYMENT: alice does not have enough XRP
1891 batch::Inner(pay(alice, bob, XRP(9999)), seq + 2),
1892 batch::Inner(pay(alice, bob, XRP(2)), seq + 3));
1893 env.close();
1894
1895 std::vector<TestLedgerData> const testCases = {
1896 {.index = 0,
1897 .txType = "Batch",
1898 .result = "tesSUCCESS",
1899 .txHash = batchID,
1900 .batchID = std::nullopt},
1901 {.index = 1,
1902 .txType = "Payment",
1903 .result = "tesSUCCESS",
1904 .txHash = txIDs[0],
1905 .batchID = batchID},
1906 };
1907 validateClosedLedger(env, testCases);
1908
1909 // Alice consumes sequences (# of txns)
1910 BEAST_EXPECT(env.seq(alice) == seq + 2);
1911
1912 // Alice pays XRP & Fee; Bob receives XRP
1913 BEAST_EXPECT(env.balance(alice) == preAlice - XRP(1) - batchFee);
1914 BEAST_EXPECT(env.balance(bob) == preBob + XRP(1));
1915 }
1916
1917 // tef failure
1918 {
1919 auto const preAlice = env.balance(alice);
1920 auto const preBob = env.balance(bob);
1921
1922 auto const batchFee = batch::calcBatchFee(env, 0, 3);
1923 auto const seq = env.seq(alice);
1924 auto const [txIDs, batchID] = submitBatch(
1925 env,
1926 tesSUCCESS,
1927 batch::outer(alice, seq, batchFee, tfOnlyOne),
1928 // tefNO_AUTH_REQUIRED: trustline auth is not required
1929 batch::Inner(trust(alice, usd(1000), tfSetfAuth), seq + 1),
1930 batch::Inner(pay(alice, bob, XRP(1)), seq + 1),
1931 batch::Inner(pay(alice, bob, XRP(2)), seq + 3));
1932 env.close();
1933
1934 std::vector<TestLedgerData> const testCases = {
1935 {.index = 0,
1936 .txType = "Batch",
1937 .result = "tesSUCCESS",
1938 .txHash = batchID,
1939 .batchID = std::nullopt},
1940 {.index = 1,
1941 .txType = "Payment",
1942 .result = "tesSUCCESS",
1943 .txHash = txIDs[1],
1944 .batchID = batchID},
1945 };
1946 validateClosedLedger(env, testCases);
1947
1948 // Alice consumes sequences (# of txns)
1949 BEAST_EXPECT(env.seq(alice) == seq + 2);
1950
1951 // Alice pays XRP & Fee; Bob receives XRP
1952 BEAST_EXPECT(env.balance(alice) == preAlice - batchFee - XRP(1));
1953 BEAST_EXPECT(env.balance(bob) == preBob + XRP(1));
1954 }
1955
1956 // ter failure
1957 {
1958 auto const preAlice = env.balance(alice);
1959 auto const preBob = env.balance(bob);
1960
1961 auto const batchFee = batch::calcBatchFee(env, 0, 3);
1962 auto const seq = env.seq(alice);
1963 auto const [txIDs, batchID] = submitBatch(
1964 env,
1965 tesSUCCESS,
1966 batch::outer(alice, seq, batchFee, tfOnlyOne),
1967 // terPRE_TICKET: ticket does not exist
1968 batch::Inner(trust(alice, usd(1000), tfSetfAuth), 0, seq + 1),
1969 batch::Inner(pay(alice, bob, XRP(1)), seq + 1),
1970 batch::Inner(pay(alice, bob, XRP(2)), seq + 3));
1971 env.close();
1972
1973 std::vector<TestLedgerData> const testCases = {
1974 {.index = 0,
1975 .txType = "Batch",
1976 .result = "tesSUCCESS",
1977 .txHash = batchID,
1978 .batchID = std::nullopt},
1979 {.index = 1,
1980 .txType = "Payment",
1981 .result = "tesSUCCESS",
1982 .txHash = txIDs[1],
1983 .batchID = batchID},
1984 };
1985 validateClosedLedger(env, testCases);
1986
1987 // Alice consumes sequences (# of txns)
1988 BEAST_EXPECT(env.seq(alice) == seq + 2);
1989
1990 // Alice pays XRP & Fee; Bob receives XRP
1991 BEAST_EXPECT(env.balance(alice) == preAlice - batchFee - XRP(1));
1992 BEAST_EXPECT(env.balance(bob) == preBob + XRP(1));
1993 }
1994
1995 // tec (tecKILLED) error
1996 {
1997 auto const preAlice = env.balance(alice);
1998 auto const preBob = env.balance(bob);
1999 auto const preCarol = env.balance(carol);
2000 auto const seq = env.seq(alice);
2001 auto const batchFee = batch::calcBatchFee(env, 0, 6);
2002
2003 auto const [txIDs, batchID] = submitBatch(
2004 env,
2005 tesSUCCESS,
2006 batch::outer(alice, seq, batchFee, tfOnlyOne),
2008 offer(alice, alice["USD"](100), XRP(100), tfImmediateOrCancel), seq + 1),
2010 offer(alice, alice["USD"](100), XRP(100), tfImmediateOrCancel), seq + 2),
2012 offer(alice, alice["USD"](100), XRP(100), tfImmediateOrCancel), seq + 3),
2013 batch::Inner(pay(alice, bob, XRP(100)), seq + 4),
2014 batch::Inner(pay(alice, carol, XRP(100)), seq + 5),
2015 batch::Inner(pay(alice, dave, XRP(100)), seq + 6));
2016 env.close();
2017
2018 std::vector<TestLedgerData> const testCases = {
2019 {.index = 0,
2020 .txType = "Batch",
2021 .result = "tesSUCCESS",
2022 .txHash = batchID,
2023 .batchID = std::nullopt},
2024 {.index = 1,
2025 .txType = "OfferCreate",
2026 .result = "tecKILLED",
2027 .txHash = txIDs[0],
2028 .batchID = batchID},
2029 {.index = 2,
2030 .txType = "OfferCreate",
2031 .result = "tecKILLED",
2032 .txHash = txIDs[1],
2033 .batchID = batchID},
2034 {.index = 3,
2035 .txType = "OfferCreate",
2036 .result = "tecKILLED",
2037 .txHash = txIDs[2],
2038 .batchID = batchID},
2039 {.index = 4,
2040 .txType = "Payment",
2041 .result = "tesSUCCESS",
2042 .txHash = txIDs[3],
2043 .batchID = batchID},
2044 };
2045 validateClosedLedger(env, testCases);
2046
2047 BEAST_EXPECT(env.balance(alice) == preAlice - XRP(100) - batchFee);
2048 BEAST_EXPECT(env.balance(bob) == preBob + XRP(100));
2049 BEAST_EXPECT(env.balance(carol) == preCarol);
2050 }
2051 }
2052
2053 void
2055 {
2056 testcase("until failure");
2057
2058 using namespace test::jtx;
2059 using namespace std::literals;
2060
2061 Env env{*this, features};
2062
2063 auto const alice = Account("alice");
2064 auto const bob = Account("bob");
2065 auto const carol = Account("carol");
2066 auto const dave = Account("dave");
2067 auto const gw = Account("gw");
2068 auto const usd = gw["USD"];
2069 env.fund(XRP(10000), alice, bob, carol, dave, gw);
2070 env.close();
2071
2072 // first transaction fails
2073 {
2074 auto const preAlice = env.balance(alice);
2075 auto const preBob = env.balance(bob);
2076
2077 auto const batchFee = batch::calcBatchFee(env, 0, 4);
2078 auto const seq = env.seq(alice);
2079 auto const [txIDs, batchID] = submitBatch(
2080 env,
2081 tesSUCCESS,
2082 batch::outer(alice, seq, batchFee, tfUntilFailure),
2083 // tecUNFUNDED_PAYMENT: alice does not have enough XRP
2084 batch::Inner(pay(alice, bob, XRP(9999)), seq + 1),
2085 batch::Inner(pay(alice, bob, XRP(1)), seq + 2),
2086 batch::Inner(pay(alice, bob, XRP(2)), seq + 3),
2087 batch::Inner(pay(alice, bob, XRP(3)), seq + 4));
2088 env.close();
2089
2090 std::vector<TestLedgerData> const testCases = {
2091 {.index = 0,
2092 .txType = "Batch",
2093 .result = "tesSUCCESS",
2094 .txHash = batchID,
2095 .batchID = std::nullopt},
2096 {.index = 1,
2097 .txType = "Payment",
2098 .result = "tecUNFUNDED_PAYMENT",
2099 .txHash = txIDs[0],
2100 .batchID = batchID},
2101 };
2102 validateClosedLedger(env, testCases);
2103
2104 // Alice consumes sequences (# of txns)
2105 BEAST_EXPECT(env.seq(alice) == seq + 2);
2106
2107 // Alice pays XRP & Fee; Bob receives XRP
2108 BEAST_EXPECT(env.balance(alice) == preAlice - batchFee);
2109 BEAST_EXPECT(env.balance(bob) == preBob);
2110 }
2111
2112 // all transactions succeed
2113 {
2114 auto const preAlice = env.balance(alice);
2115 auto const preBob = env.balance(bob);
2116
2117 auto const batchFee = batch::calcBatchFee(env, 0, 4);
2118 auto const seq = env.seq(alice);
2119 auto const [txIDs, batchID] = submitBatch(
2120 env,
2121 tesSUCCESS,
2122 batch::outer(alice, seq, batchFee, tfUntilFailure),
2123 batch::Inner(pay(alice, bob, XRP(1)), seq + 1),
2124 batch::Inner(pay(alice, bob, XRP(2)), seq + 2),
2125 batch::Inner(pay(alice, bob, XRP(3)), seq + 3),
2126 batch::Inner(pay(alice, bob, XRP(4)), seq + 4));
2127 env.close();
2128
2129 std::vector<TestLedgerData> const testCases = {
2130 {.index = 0,
2131 .txType = "Batch",
2132 .result = "tesSUCCESS",
2133 .txHash = batchID,
2134 .batchID = std::nullopt},
2135 {.index = 1,
2136 .txType = "Payment",
2137 .result = "tesSUCCESS",
2138 .txHash = txIDs[0],
2139 .batchID = batchID},
2140 {.index = 2,
2141 .txType = "Payment",
2142 .result = "tesSUCCESS",
2143 .txHash = txIDs[1],
2144 .batchID = batchID},
2145 {.index = 3,
2146 .txType = "Payment",
2147 .result = "tesSUCCESS",
2148 .txHash = txIDs[2],
2149 .batchID = batchID},
2150 {.index = 4,
2151 .txType = "Payment",
2152 .result = "tesSUCCESS",
2153 .txHash = txIDs[3],
2154 .batchID = batchID},
2155 };
2156 validateClosedLedger(env, testCases);
2157
2158 // Alice consumes sequences (# of txns)
2159 BEAST_EXPECT(env.seq(alice) == seq + 5);
2160
2161 // Alice pays XRP & Fee; Bob receives XRP
2162 BEAST_EXPECT(env.balance(alice) == preAlice - XRP(10) - batchFee);
2163 BEAST_EXPECT(env.balance(bob) == preBob + XRP(10));
2164 }
2165
2166 // tec error
2167 {
2168 auto const preAlice = env.balance(alice);
2169 auto const preBob = env.balance(bob);
2170
2171 auto const batchFee = batch::calcBatchFee(env, 0, 4);
2172 auto const seq = env.seq(alice);
2173 auto const [txIDs, batchID] = submitBatch(
2174 env,
2175 tesSUCCESS,
2176 batch::outer(alice, seq, batchFee, tfUntilFailure),
2177 batch::Inner(pay(alice, bob, XRP(1)), seq + 1),
2178 batch::Inner(pay(alice, bob, XRP(2)), seq + 2),
2179 // tecUNFUNDED_PAYMENT: alice does not have enough XRP
2180 batch::Inner(pay(alice, bob, XRP(9999)), seq + 3),
2181 batch::Inner(pay(alice, bob, XRP(3)), seq + 4));
2182 env.close();
2183
2184 std::vector<TestLedgerData> const testCases = {
2185 {.index = 0,
2186 .txType = "Batch",
2187 .result = "tesSUCCESS",
2188 .txHash = batchID,
2189 .batchID = std::nullopt},
2190 {.index = 1,
2191 .txType = "Payment",
2192 .result = "tesSUCCESS",
2193 .txHash = txIDs[0],
2194 .batchID = batchID},
2195 {.index = 2,
2196 .txType = "Payment",
2197 .result = "tesSUCCESS",
2198 .txHash = txIDs[1],
2199 .batchID = batchID},
2200 {.index = 3,
2201 .txType = "Payment",
2202 .result = "tecUNFUNDED_PAYMENT",
2203 .txHash = txIDs[2],
2204 .batchID = batchID},
2205 };
2206 validateClosedLedger(env, testCases);
2207
2208 // Alice consumes sequences (# of txns)
2209 BEAST_EXPECT(env.seq(alice) == seq + 4);
2210
2211 // Alice pays XRP & Fee; Bob receives XRP
2212 BEAST_EXPECT(env.balance(alice) == preAlice - XRP(3) - batchFee);
2213 BEAST_EXPECT(env.balance(bob) == preBob + XRP(3));
2214 }
2215
2216 // tef error
2217 {
2218 auto const preAlice = env.balance(alice);
2219 auto const preBob = env.balance(bob);
2220
2221 auto const batchFee = batch::calcBatchFee(env, 0, 4);
2222 auto const seq = env.seq(alice);
2223 auto const [txIDs, batchID] = submitBatch(
2224 env,
2225 tesSUCCESS,
2226 batch::outer(alice, seq, batchFee, tfUntilFailure),
2227 batch::Inner(pay(alice, bob, XRP(1)), seq + 1),
2228 batch::Inner(pay(alice, bob, XRP(2)), seq + 2),
2229 // tefNO_AUTH_REQUIRED: trustline auth is not required
2230 batch::Inner(trust(alice, usd(1000), tfSetfAuth), seq + 3),
2231 batch::Inner(pay(alice, bob, XRP(3)), seq + 4));
2232 env.close();
2233
2234 std::vector<TestLedgerData> const testCases = {
2235 {.index = 0,
2236 .txType = "Batch",
2237 .result = "tesSUCCESS",
2238 .txHash = batchID,
2239 .batchID = std::nullopt},
2240 {.index = 1,
2241 .txType = "Payment",
2242 .result = "tesSUCCESS",
2243 .txHash = txIDs[0],
2244 .batchID = batchID},
2245 {.index = 2,
2246 .txType = "Payment",
2247 .result = "tesSUCCESS",
2248 .txHash = txIDs[1],
2249 .batchID = batchID},
2250 };
2251 validateClosedLedger(env, testCases);
2252
2253 // Alice consumes sequences (# of txns)
2254 BEAST_EXPECT(env.seq(alice) == seq + 3);
2255
2256 // Alice pays XRP & Fee; Bob receives XRP
2257 BEAST_EXPECT(env.balance(alice) == preAlice - XRP(3) - batchFee);
2258 BEAST_EXPECT(env.balance(bob) == preBob + XRP(3));
2259 }
2260
2261 // ter error
2262 {
2263 auto const preAlice = env.balance(alice);
2264 auto const preBob = env.balance(bob);
2265
2266 auto const batchFee = batch::calcBatchFee(env, 0, 4);
2267 auto const seq = env.seq(alice);
2268 auto const [txIDs, batchID] = submitBatch(
2269 env,
2270 tesSUCCESS,
2271 batch::outer(alice, seq, batchFee, tfUntilFailure),
2272 batch::Inner(pay(alice, bob, XRP(1)), seq + 1),
2273 batch::Inner(pay(alice, bob, XRP(2)), seq + 2),
2274 // terPRE_TICKET: ticket does not exist
2275 batch::Inner(trust(alice, usd(1000), tfSetfAuth), 0, seq + 3),
2276 batch::Inner(pay(alice, bob, XRP(3)), seq + 4));
2277 env.close();
2278
2279 std::vector<TestLedgerData> const testCases = {
2280 {.index = 0,
2281 .txType = "Batch",
2282 .result = "tesSUCCESS",
2283 .txHash = batchID,
2284 .batchID = std::nullopt},
2285 {.index = 1,
2286 .txType = "Payment",
2287 .result = "tesSUCCESS",
2288 .txHash = txIDs[0],
2289 .batchID = batchID},
2290 {.index = 2,
2291 .txType = "Payment",
2292 .result = "tesSUCCESS",
2293 .txHash = txIDs[1],
2294 .batchID = batchID},
2295 };
2296 validateClosedLedger(env, testCases);
2297
2298 // Alice consumes sequences (# of txns)
2299 BEAST_EXPECT(env.seq(alice) == seq + 3);
2300
2301 // Alice pays XRP & Fee; Bob receives XRP
2302 BEAST_EXPECT(env.balance(alice) == preAlice - XRP(3) - batchFee);
2303 BEAST_EXPECT(env.balance(bob) == preBob + XRP(3));
2304 }
2305
2306 // tec (tecKILLED) error
2307 {
2308 auto const preAlice = env.balance(alice);
2309 auto const preBob = env.balance(bob);
2310 auto const preCarol = env.balance(carol);
2311 auto const seq = env.seq(alice);
2312 auto const batchFee = batch::calcBatchFee(env, 0, 4);
2313 auto const [txIDs, batchID] = submitBatch(
2314 env,
2315 tesSUCCESS,
2316 batch::outer(alice, seq, batchFee, tfUntilFailure),
2317 batch::Inner(pay(alice, bob, XRP(100)), seq + 1),
2318 batch::Inner(pay(alice, carol, XRP(100)), seq + 2),
2320 offer(alice, alice["USD"](100), XRP(100), tfImmediateOrCancel), seq + 3),
2321 batch::Inner(pay(alice, dave, XRP(100)), seq + 4));
2322 env.close();
2323
2324 std::vector<TestLedgerData> const testCases = {
2325 {.index = 0,
2326 .txType = "Batch",
2327 .result = "tesSUCCESS",
2328 .txHash = batchID,
2329 .batchID = std::nullopt},
2330 {.index = 1,
2331 .txType = "Payment",
2332 .result = "tesSUCCESS",
2333 .txHash = txIDs[0],
2334 .batchID = batchID},
2335 {.index = 2,
2336 .txType = "Payment",
2337 .result = "tesSUCCESS",
2338 .txHash = txIDs[1],
2339 .batchID = batchID},
2340 {.index = 3,
2341 .txType = "OfferCreate",
2342 .result = "tecKILLED",
2343 .txHash = txIDs[2],
2344 .batchID = batchID},
2345 };
2346 validateClosedLedger(env, testCases);
2347
2348 BEAST_EXPECT(env.balance(alice) == preAlice - XRP(200) - batchFee);
2349 BEAST_EXPECT(env.balance(bob) == preBob + XRP(100));
2350 BEAST_EXPECT(env.balance(carol) == preCarol + XRP(100));
2351 }
2352 }
2353
2354 void
2356 {
2357 testcase("independent");
2358
2359 using namespace test::jtx;
2360 using namespace std::literals;
2361
2362 Env env{*this, features};
2363
2364 auto const alice = Account("alice");
2365 auto const bob = Account("bob");
2366 auto const carol = Account("carol");
2367 auto const gw = Account("gw");
2368 auto const usd = gw["USD"];
2369 env.fund(XRP(10000), alice, bob, carol, gw);
2370 env.close();
2371
2372 // multiple transactions fail
2373 {
2374 auto const preAlice = env.balance(alice);
2375 auto const preBob = env.balance(bob);
2376
2377 auto const batchFee = batch::calcBatchFee(env, 0, 4);
2378 auto const seq = env.seq(alice);
2379 auto const [txIDs, batchID] = submitBatch(
2380 env,
2381 tesSUCCESS,
2382 batch::outer(alice, seq, batchFee, tfIndependent),
2383 batch::Inner(pay(alice, bob, XRP(1)), seq + 1),
2384 // tecUNFUNDED_PAYMENT: alice does not have enough XRP
2385 batch::Inner(pay(alice, bob, XRP(9999)), seq + 2),
2386 // tecUNFUNDED_PAYMENT: alice does not have enough XRP
2387 batch::Inner(pay(alice, bob, XRP(9999)), seq + 3),
2388 batch::Inner(pay(alice, bob, XRP(3)), seq + 4));
2389 env.close();
2390
2391 std::vector<TestLedgerData> const testCases = {
2392 {.index = 0,
2393 .txType = "Batch",
2394 .result = "tesSUCCESS",
2395 .txHash = batchID,
2396 .batchID = std::nullopt},
2397 {.index = 1,
2398 .txType = "Payment",
2399 .result = "tesSUCCESS",
2400 .txHash = txIDs[0],
2401 .batchID = batchID},
2402 {.index = 2,
2403 .txType = "Payment",
2404 .result = "tecUNFUNDED_PAYMENT",
2405 .txHash = txIDs[1],
2406 .batchID = batchID},
2407 {.index = 3,
2408 .txType = "Payment",
2409 .result = "tecUNFUNDED_PAYMENT",
2410 .txHash = txIDs[2],
2411 .batchID = batchID},
2412 {.index = 4,
2413 .txType = "Payment",
2414 .result = "tesSUCCESS",
2415 .txHash = txIDs[3],
2416 .batchID = batchID},
2417 };
2418 validateClosedLedger(env, testCases);
2419
2420 // Alice consumes sequences (# of txns)
2421 BEAST_EXPECT(env.seq(alice) == seq + 5);
2422
2423 // Alice pays XRP & Fee; Bob receives XRP
2424 BEAST_EXPECT(env.balance(alice) == preAlice - XRP(4) - batchFee);
2425 BEAST_EXPECT(env.balance(bob) == preBob + XRP(4));
2426 }
2427
2428 // tec error
2429 {
2430 auto const preAlice = env.balance(alice);
2431 auto const preBob = env.balance(bob);
2432
2433 auto const batchFee = batch::calcBatchFee(env, 0, 4);
2434 auto const seq = env.seq(alice);
2435 auto const [txIDs, batchID] = submitBatch(
2436 env,
2437 tesSUCCESS,
2438 batch::outer(alice, seq, batchFee, tfIndependent),
2439 batch::Inner(pay(alice, bob, XRP(1)), seq + 1),
2440 batch::Inner(pay(alice, bob, XRP(2)), seq + 2),
2441 // tecUNFUNDED_PAYMENT: alice does not have enough XRP
2442 batch::Inner(pay(alice, bob, XRP(9999)), seq + 3),
2443 batch::Inner(pay(alice, bob, XRP(3)), seq + 4));
2444 env.close();
2445
2446 std::vector<TestLedgerData> const testCases = {
2447 {.index = 0,
2448 .txType = "Batch",
2449 .result = "tesSUCCESS",
2450 .txHash = batchID,
2451 .batchID = std::nullopt},
2452 {.index = 1,
2453 .txType = "Payment",
2454 .result = "tesSUCCESS",
2455 .txHash = txIDs[0],
2456 .batchID = batchID},
2457 {.index = 2,
2458 .txType = "Payment",
2459 .result = "tesSUCCESS",
2460 .txHash = txIDs[1],
2461 .batchID = batchID},
2462 {.index = 3,
2463 .txType = "Payment",
2464 .result = "tecUNFUNDED_PAYMENT",
2465 .txHash = txIDs[2],
2466 .batchID = batchID},
2467 {.index = 4,
2468 .txType = "Payment",
2469 .result = "tesSUCCESS",
2470 .txHash = txIDs[3],
2471 .batchID = batchID},
2472 };
2473 validateClosedLedger(env, testCases);
2474
2475 // Alice consumes sequences (# of txns)
2476 BEAST_EXPECT(env.seq(alice) == seq + 5);
2477
2478 // Alice pays XRP & Fee; Bob receives XRP
2479 BEAST_EXPECT(env.balance(alice) == preAlice - XRP(6) - batchFee);
2480 BEAST_EXPECT(env.balance(bob) == preBob + XRP(6));
2481 }
2482
2483 // tef error
2484 {
2485 auto const preAlice = env.balance(alice);
2486 auto const preBob = env.balance(bob);
2487
2488 auto const batchFee = batch::calcBatchFee(env, 0, 4);
2489 auto const seq = env.seq(alice);
2490 auto const [txIDs, batchID] = submitBatch(
2491 env,
2492 tesSUCCESS,
2493 batch::outer(alice, seq, batchFee, tfIndependent),
2494 batch::Inner(pay(alice, bob, XRP(1)), seq + 1),
2495 batch::Inner(pay(alice, bob, XRP(2)), seq + 2),
2496 // tefNO_AUTH_REQUIRED: trustline auth is not required
2497 batch::Inner(trust(alice, usd(1000), tfSetfAuth), seq + 3),
2498 batch::Inner(pay(alice, bob, XRP(3)), seq + 3));
2499 env.close();
2500
2501 std::vector<TestLedgerData> const testCases = {
2502 {.index = 0,
2503 .txType = "Batch",
2504 .result = "tesSUCCESS",
2505 .txHash = batchID,
2506 .batchID = std::nullopt},
2507 {.index = 1,
2508 .txType = "Payment",
2509 .result = "tesSUCCESS",
2510 .txHash = txIDs[0],
2511 .batchID = batchID},
2512 {.index = 2,
2513 .txType = "Payment",
2514 .result = "tesSUCCESS",
2515 .txHash = txIDs[1],
2516 .batchID = batchID},
2517 {.index = 3,
2518 .txType = "Payment",
2519 .result = "tesSUCCESS",
2520 .txHash = txIDs[3],
2521 .batchID = batchID},
2522 };
2523 validateClosedLedger(env, testCases);
2524
2525 // Alice consumes sequences (# of txns)
2526 BEAST_EXPECT(env.seq(alice) == seq + 4);
2527
2528 // Alice pays XRP & Fee; Bob receives XRP
2529 BEAST_EXPECT(env.balance(alice) == preAlice - batchFee - XRP(6));
2530 BEAST_EXPECT(env.balance(bob) == preBob + XRP(6));
2531 }
2532
2533 // ter error
2534 {
2535 auto const preAlice = env.balance(alice);
2536 auto const preBob = env.balance(bob);
2537
2538 auto const batchFee = batch::calcBatchFee(env, 0, 4);
2539 auto const seq = env.seq(alice);
2540 auto const [txIDs, batchID] = submitBatch(
2541 env,
2542 tesSUCCESS,
2543 batch::outer(alice, seq, batchFee, tfIndependent),
2544 batch::Inner(pay(alice, bob, XRP(1)), seq + 1),
2545 batch::Inner(pay(alice, bob, XRP(2)), seq + 2),
2546 // terPRE_TICKET: ticket does not exist
2547 batch::Inner(trust(alice, usd(1000), tfSetfAuth), 0, seq + 3),
2548 batch::Inner(pay(alice, bob, XRP(3)), seq + 3));
2549 env.close();
2550
2551 std::vector<TestLedgerData> const testCases = {
2552 {.index = 0,
2553 .txType = "Batch",
2554 .result = "tesSUCCESS",
2555 .txHash = batchID,
2556 .batchID = std::nullopt},
2557 {.index = 1,
2558 .txType = "Payment",
2559 .result = "tesSUCCESS",
2560 .txHash = txIDs[0],
2561 .batchID = batchID},
2562 {.index = 2,
2563 .txType = "Payment",
2564 .result = "tesSUCCESS",
2565 .txHash = txIDs[1],
2566 .batchID = batchID},
2567 {.index = 3,
2568 .txType = "Payment",
2569 .result = "tesSUCCESS",
2570 .txHash = txIDs[3],
2571 .batchID = batchID},
2572 };
2573 validateClosedLedger(env, testCases);
2574
2575 // Alice consumes sequences (# of txns)
2576 BEAST_EXPECT(env.seq(alice) == seq + 4);
2577
2578 // Alice pays XRP & Fee; Bob receives XRP
2579 BEAST_EXPECT(env.balance(alice) == preAlice - batchFee - XRP(6));
2580 BEAST_EXPECT(env.balance(bob) == preBob + XRP(6));
2581 }
2582
2583 // tec (tecKILLED) error
2584 {
2585 auto const preAlice = env.balance(alice);
2586 auto const preBob = env.balance(bob);
2587 auto const preCarol = env.balance(carol);
2588 auto const seq = env.seq(alice);
2589 auto const batchFee = batch::calcBatchFee(env, 0, 3);
2590 auto const [txIDs, batchID] = submitBatch(
2591 env,
2592 tesSUCCESS,
2593 batch::outer(alice, seq, batchFee, tfIndependent),
2594 batch::Inner(pay(alice, bob, XRP(100)), seq + 1),
2595 batch::Inner(pay(alice, carol, XRP(100)), seq + 2),
2597 offer(alice, alice["USD"](100), XRP(100), tfImmediateOrCancel), seq + 3));
2598 env.close();
2599
2600 std::vector<TestLedgerData> const testCases = {
2601 {.index = 0,
2602 .txType = "Batch",
2603 .result = "tesSUCCESS",
2604 .txHash = batchID,
2605 .batchID = std::nullopt},
2606 {.index = 1,
2607 .txType = "Payment",
2608 .result = "tesSUCCESS",
2609 .txHash = txIDs[0],
2610 .batchID = batchID},
2611 {.index = 2,
2612 .txType = "Payment",
2613 .result = "tesSUCCESS",
2614 .txHash = txIDs[1],
2615 .batchID = batchID},
2616 {.index = 3,
2617 .txType = "OfferCreate",
2618 .result = "tecKILLED",
2619 .txHash = txIDs[2],
2620 .batchID = batchID},
2621 };
2622 validateClosedLedger(env, testCases);
2623
2624 BEAST_EXPECT(env.balance(alice) == preAlice - XRP(200) - batchFee);
2625 BEAST_EXPECT(env.balance(bob) == preBob + XRP(100));
2626 BEAST_EXPECT(env.balance(carol) == preCarol + XRP(100));
2627 }
2628 }
2629
2630 void
2631 doTestInnerSubmitRPC(FeatureBitset features, bool withBatch)
2632 {
2633 std::string const testName =
2634 std::string("inner submit rpc: batch ") + (withBatch ? "enabled" : "disabled") + ": ";
2635
2636 auto const amend = withBatch ? features : features - featureBatchV1_1;
2637
2638 using namespace test::jtx;
2639 using namespace std::literals;
2640
2641 Env env{*this, amend};
2642 if (!BEAST_EXPECT(amend[featureBatchV1_1] == withBatch))
2643 return;
2644
2645 auto const alice = Account("alice");
2646 auto const bob = Account("bob");
2647
2648 env.fund(XRP(10000), alice, bob);
2649 env.close();
2650
2651 // Any transaction carrying tfInnerBatchTxn is rejected in checkValidity()
2652 // before it reaches the tx engine, regardless of its signing fields or
2653 // whether the amendment is enabled.
2654 auto submitAndValidate = [&](std::string caseName, Slice const& slice, int line) {
2655 testcase << testName << caseName;
2656 auto const jrr = env.rpc("submit", strHex(slice))[jss::result];
2657 expect(
2658 jrr[jss::status] == "error" && jrr[jss::error] == "invalidTransaction" &&
2659 jrr[jss::error_exception] ==
2660 "fails local checks: Batch inner transactions are never "
2661 "considered validly signed.",
2662 pretty(jrr),
2663 __FILE__,
2664 line);
2665 env.close();
2666 };
2667
2668 // Invalid RPC Submission: TxnSignature
2669 // + has `TxnSignature` field
2670 // - has no `SigningPubKey` field
2671 // - has no `Signers` field
2672 // + has `tfInnerBatchTxn` flag
2673 {
2674 auto txn = batch::Inner(pay(alice, bob, XRP(1)), env.seq(alice));
2675 txn[sfTxnSignature] = "DEADBEEF";
2676 STParsedJSONObject parsed("test", txn.getTxn());
2677 Serializer s;
2678 parsed.object->add(s); // NOLINT(bugprone-unchecked-optional-access)
2679 submitAndValidate("TxnSignature set", s.slice(), __LINE__);
2680 }
2681
2682 // Invalid RPC Submission: SigningPubKey
2683 // - has no `TxnSignature` field
2684 // + has `SigningPubKey` field
2685 // - has no `Signers` field
2686 // + has `tfInnerBatchTxn` flag
2687 {
2688 auto txn = batch::Inner(pay(alice, bob, XRP(1)), env.seq(alice));
2689 txn[sfSigningPubKey] = strHex(alice.pk());
2690 STParsedJSONObject parsed("test", txn.getTxn());
2691 Serializer s;
2692 parsed.object->add(s); // NOLINT(bugprone-unchecked-optional-access)
2693 submitAndValidate("SigningPubKey set", s.slice(), __LINE__);
2694 }
2695
2696 // Invalid RPC Submission: Signers
2697 // - has no `TxnSignature` field
2698 // + has empty `SigningPubKey` field
2699 // + has `Signers` field
2700 // + has `tfInnerBatchTxn` flag
2701 {
2702 auto txn = batch::Inner(pay(alice, bob, XRP(1)), env.seq(alice));
2703 txn[sfSigners] = json::ValueType::Array;
2704 STParsedJSONObject parsed("test", txn.getTxn());
2705 Serializer s;
2706 parsed.object->add(s); // NOLINT(bugprone-unchecked-optional-access)
2707 submitAndValidate("Signers set", s.slice(), __LINE__);
2708 }
2709
2710 {
2711 // Fully signed inner batch transaction
2712 auto const txn = batch::Inner(pay(alice, bob, XRP(1)), env.seq(alice));
2713 auto const jt = env.jt(txn.getTxn());
2714
2715 STParsedJSONObject parsed("test", jt.jv);
2716 Serializer s;
2717 parsed.object->add(s); // NOLINT(bugprone-unchecked-optional-access)
2718 submitAndValidate("Fully signed", s.slice(), __LINE__);
2719 }
2720
2721 // Invalid RPC Submission: tfInnerBatchTxn
2722 // - has no `TxnSignature` field
2723 // + has empty `SigningPubKey` field
2724 // - has no `Signers` field
2725 // + has `tfInnerBatchTxn` flag
2726 {
2727 auto txn = batch::Inner(pay(alice, bob, XRP(1)), env.seq(alice));
2728 STParsedJSONObject parsed("test", txn.getTxn());
2729 Serializer s;
2730 parsed.object->add(s); // NOLINT(bugprone-unchecked-optional-access)
2731 submitAndValidate("No signing fields set", s.slice(), __LINE__);
2732 }
2733
2734 // Invalid RPC Submission: tfInnerBatchTxn pseudo-transaction
2735 // - has no `TxnSignature` field
2736 // + has empty `SigningPubKey` field
2737 // - has no `Signers` field
2738 // + has `tfInnerBatchTxn` flag
2739 {
2740 STTx const amendTx(ttAMENDMENT, [seq = env.closed()->header().seq + 1](auto& obj) {
2741 obj.setAccountID(sfAccount, AccountID());
2742 obj.setFieldH256(sfAmendment, featureBatchV1_1);
2743 obj.setFieldU32(sfLedgerSequence, seq);
2744 obj.setFieldU32(sfFlags, tfInnerBatchTxn);
2745 });
2746 auto txn = batch::Inner(amendTx.getJson(JsonOptions::Values::None), env.seq(alice));
2747 STParsedJSONObject parsed("test", txn.getTxn());
2748 Serializer s;
2749 parsed.object->add(s); // NOLINT(bugprone-unchecked-optional-access)
2750 submitAndValidate("Pseudo-transaction", s.slice(), __LINE__);
2751 }
2752 }
2753
2754 void
2756 {
2757 for (bool const withBatch : {true, false})
2758 {
2759 doTestInnerSubmitRPC(features, withBatch);
2760 }
2761 }
2762
2763 void
2765 {
2766 testcase("account activation");
2767
2768 using namespace test::jtx;
2769 using namespace std::literals;
2770
2771 Env env{*this, features};
2772
2773 auto const alice = Account("alice");
2774 auto const bob = Account("bob");
2775 env.fund(XRP(10000), alice);
2776 env.close();
2777 env.memoize(bob);
2778
2779 auto const preAlice = env.balance(alice);
2780 auto const ledSeq = env.current()->seq();
2781 auto const seq = env.seq(alice);
2782 auto const batchFee = batch::calcBatchFee(env, 1, 2);
2783 auto const [txIDs, batchID] = submitBatch(
2784 env,
2785 tesSUCCESS,
2786 batch::outer(alice, seq, batchFee, tfAllOrNothing),
2787 batch::Inner(pay(alice, bob, XRP(1000)), seq + 1),
2788 batch::Inner(fset(bob, asfAllowTrustLineClawback), ledSeq),
2789 batch::Sig(bob));
2790 env.close();
2791
2792 std::vector<TestLedgerData> const testCases = {
2793 {.index = 0,
2794 .txType = "Batch",
2795 .result = "tesSUCCESS",
2796 .txHash = batchID,
2797 .batchID = std::nullopt},
2798 {.index = 1,
2799 .txType = "Payment",
2800 .result = "tesSUCCESS",
2801 .txHash = txIDs[0],
2802 .batchID = batchID},
2803 {.index = 2,
2804 .txType = "AccountSet",
2805 .result = "tesSUCCESS",
2806 .txHash = txIDs[1],
2807 .batchID = batchID},
2808 };
2809 validateClosedLedger(env, testCases);
2810
2811 // Alice consumes sequences (# of txns)
2812 BEAST_EXPECT(env.seq(alice) == seq + 2);
2813
2814 // Bob consumes sequences (# of txns)
2815 BEAST_EXPECT(env.seq(bob) == ledSeq + 1);
2816
2817 // Alice pays XRP & Fee; Bob receives XRP
2818 BEAST_EXPECT(env.balance(alice) == preAlice - XRP(1000) - batchFee);
2819 BEAST_EXPECT(env.balance(bob) == XRP(1000));
2820
2821 // An inner partial payment must never fund a new account. With
2822 // featureBatchV1_1 the partial-payment-to-create check runs on the
2823 // batch's closed view and rejects it (tefNO_DST_PARTIAL). A USD->XRP path is
2824 // set up so that, absent the check, the partial payment would deliver
2825 // XRP and create carol; the check must block exactly that. A
2826 // cross-currency shape (IOU SendMax, XRP Amount) also lets the inner
2827 // pass preflight and reach the preclaim check.
2828 {
2829 Env env{*this, features};
2830
2831 auto const alice = Account("alice");
2832 auto const gw = Account("gw");
2833 auto const carol = Account("carol"); // never funded
2834 auto const usd = gw["USD"];
2835 env.fund(XRP(10000), alice, gw);
2836 env.close();
2837 env.memoize(carol);
2838
2839 env(trust(alice, usd(100000)));
2840 env(pay(gw, alice, usd(10000)));
2841 env(offer(gw, usd(2000), XRP(2000))); // USD -> XRP liquidity
2842 env.close();
2843
2844 auto const seq = env.seq(alice);
2845 auto const batchFee = batch::calcBatchFee(env, 0, 2);
2846
2847 auto pp = pay(alice, carol, XRP(1000));
2848 pp[jss::SendMax] = usd(2000).value().getJson(JsonOptions::Values::None);
2849 pp[jss::Flags] = tfPartialPayment;
2850
2851 env(batch::outer(alice, seq, batchFee, tfIndependent),
2852 batch::Inner(pp, seq + 1),
2853 batch::Inner(noop(alice), seq + 2),
2854 Ter(tesSUCCESS));
2855 env.close();
2856
2857 // carol was not created despite the available path.
2858 BEAST_EXPECT(!env.le(carol));
2859 }
2860 }
2861
2862 void
2864 {
2865 testcase("check all signatures");
2866
2867 using namespace test::jtx;
2868 using namespace std::literals;
2869
2870 // Verifies that checkBatchSign validates all signers even when an
2871 // unfunded account (signed with its master key) appears first in the
2872 // sorted signer list. A funded account with an invalid signature must
2873 // still be rejected with tefBAD_AUTH.
2874
2875 Env env{*this, features};
2876
2877 auto const alice = Account("alice");
2878 // "aaa" sorts before other accounts alphabetically, ensuring the
2879 // unfunded account is checked first in the sorted signer list
2880 auto const unfunded = Account("aaa");
2881 auto const carol = Account("carol");
2882 env.fund(XRP(10000), alice, carol);
2883 env.close();
2884
2885 // Verify sort order: unfunded.id() < carol.id()
2886 BEAST_EXPECT(unfunded.id() < carol.id());
2887
2888 auto const seq = env.seq(alice);
2889 auto const ledSeq = env.current()->seq();
2890 auto const batchFee = batch::calcBatchFee(env, 2, 3);
2891
2892 // The batch includes:
2893 // 1. alice pays unfunded (to create unfunded's account)
2894 // 2. unfunded does a noop (signed by unfunded's master key - valid)
2895 // 3. carol pays alice (signed by alice's key - INVALID since alice is
2896 // not carol's regular key)
2897 //
2898 // checkBatchSign must validate all signers regardless of order.
2899 // This must fail with tefBAD_AUTH.
2900 env(batch::outer(alice, seq, batchFee, tfAllOrNothing),
2901 batch::Inner(pay(alice, unfunded, XRP(100)), seq + 1),
2902 batch::Inner(noop(unfunded), ledSeq),
2903 batch::Inner(pay(carol, alice, XRP(1000)), env.seq(carol)),
2904 batch::Sig(unfunded, Reg{carol, alice}),
2905 Ter(tefBAD_AUTH));
2906 env.close();
2907 }
2908
2909 void
2911 {
2912 testcase("account set");
2913
2914 using namespace test::jtx;
2915 using namespace std::literals;
2916
2917 Env env{*this, features};
2918
2919 auto const alice = Account("alice");
2920 auto const bob = Account("bob");
2921 env.fund(XRP(10000), alice, bob);
2922 env.close();
2923
2924 auto const preAlice = env.balance(alice);
2925 auto const preBob = env.balance(bob);
2926
2927 auto const seq = env.seq(alice);
2928 auto const batchFee = batch::calcBatchFee(env, 0, 2);
2929 auto tx1 = batch::Inner(noop(alice), seq + 1);
2930 std::string domain = "example.com";
2931 tx1[sfDomain] = strHex(domain);
2932 auto const [txIDs, batchID] = submitBatch(
2933 env,
2934 tesSUCCESS,
2935 batch::outer(alice, seq, batchFee, tfAllOrNothing),
2936 tx1,
2937 batch::Inner(pay(alice, bob, XRP(1)), seq + 2));
2938 env.close();
2939
2940 std::vector<TestLedgerData> const testCases = {
2941 {.index = 0,
2942 .txType = "Batch",
2943 .result = "tesSUCCESS",
2944 .txHash = batchID,
2945 .batchID = std::nullopt},
2946 {.index = 1,
2947 .txType = "AccountSet",
2948 .result = "tesSUCCESS",
2949 .txHash = txIDs[0],
2950 .batchID = batchID},
2951 {.index = 2,
2952 .txType = "Payment",
2953 .result = "tesSUCCESS",
2954 .txHash = txIDs[1],
2955 .batchID = batchID},
2956 };
2957 validateClosedLedger(env, testCases);
2958
2959 auto const sle = env.le(keylet::account(alice));
2960 BEAST_EXPECT(sle);
2961 BEAST_EXPECT(sle->getFieldVL(sfDomain) == Blob(domain.begin(), domain.end()));
2962
2963 // Alice consumes sequences (# of txns)
2964 BEAST_EXPECT(env.seq(alice) == seq + 3);
2965
2966 // Alice pays XRP & Fee; Bob receives XRP
2967 BEAST_EXPECT(env.balance(alice) == preAlice - XRP(1) - batchFee);
2968 BEAST_EXPECT(env.balance(bob) == preBob + XRP(1));
2969 }
2970
2971 void
2973 {
2974 testcase("account delete");
2975
2976 using namespace test::jtx;
2977 using namespace std::literals;
2978
2979 // tfIndependent: account delete success
2980 {
2981 Env env{*this, features};
2982
2983 auto const alice = Account("alice");
2984 auto const bob = Account("bob");
2985 env.fund(XRP(10000), alice, bob);
2986 env.close();
2987
2988 incLgrSeqForAccDel(env, alice);
2989 for (int i = 0; i < 5; ++i)
2990 env.close();
2991
2992 auto const preAlice = env.balance(alice);
2993 auto const preBob = env.balance(bob);
2994
2995 auto const seq = env.seq(alice);
2996 auto const batchFee = batch::calcBatchFee(env, 0, 2) + env.current()->fees().increment;
2997 auto const [txIDs, batchID] = submitBatch(
2998 env,
2999 tesSUCCESS,
3000 batch::outer(alice, seq, batchFee, tfIndependent),
3001 batch::Inner(pay(alice, bob, XRP(1)), seq + 1),
3002 batch::Inner(acctdelete(alice, bob), seq + 2),
3003 // terNO_ACCOUNT: alice does not exist
3004 batch::Inner(pay(alice, bob, XRP(2)), seq + 3));
3005 env.close();
3006
3007 std::vector<TestLedgerData> const testCases = {
3008 {.index = 0,
3009 .txType = "Batch",
3010 .result = "tesSUCCESS",
3011 .txHash = batchID,
3012 .batchID = std::nullopt},
3013 {.index = 1,
3014 .txType = "Payment",
3015 .result = "tesSUCCESS",
3016 .txHash = txIDs[0],
3017 .batchID = batchID},
3018 {.index = 2,
3019 .txType = "AccountDelete",
3020 .result = "tesSUCCESS",
3021 .txHash = txIDs[1],
3022 .batchID = batchID},
3023 };
3024 validateClosedLedger(env, testCases);
3025
3026 // Alice does not exist; Bob receives Alice's XRP
3027 BEAST_EXPECT(!env.le(keylet::account(alice)));
3028 BEAST_EXPECT(env.balance(bob) == preBob + (preAlice - batchFee));
3029 }
3030
3031 // tfIndependent: account delete fails
3032 {
3033 Env env{*this, features};
3034
3035 auto const alice = Account("alice");
3036 auto const bob = Account("bob");
3037 env.fund(XRP(10000), alice, bob);
3038 env.close();
3039
3040 incLgrSeqForAccDel(env, alice);
3041 for (int i = 0; i < 5; ++i)
3042 env.close();
3043
3044 auto const preAlice = env.balance(alice);
3045 auto const preBob = env.balance(bob);
3046
3047 env.trust(bob["USD"](1000), alice);
3048 env.close();
3049
3050 auto const seq = env.seq(alice);
3051 auto const batchFee = batch::calcBatchFee(env, 0, 2) + env.current()->fees().increment;
3052 auto const [txIDs, batchID] = submitBatch(
3053 env,
3054 tesSUCCESS,
3055 batch::outer(alice, seq, batchFee, tfIndependent),
3056 batch::Inner(pay(alice, bob, XRP(1)), seq + 1),
3057 // tecHAS_OBLIGATIONS: alice has obligations
3058 batch::Inner(acctdelete(alice, bob), seq + 2),
3059 batch::Inner(pay(alice, bob, XRP(2)), seq + 3));
3060 env.close();
3061
3062 std::vector<TestLedgerData> const testCases = {
3063 {.index = 0,
3064 .txType = "Batch",
3065 .result = "tesSUCCESS",
3066 .txHash = batchID,
3067 .batchID = std::nullopt},
3068 {.index = 1,
3069 .txType = "Payment",
3070 .result = "tesSUCCESS",
3071 .txHash = txIDs[0],
3072 .batchID = batchID},
3073 {.index = 2,
3074 .txType = "AccountDelete",
3075 .result = "tecHAS_OBLIGATIONS",
3076 .txHash = txIDs[1],
3077 .batchID = batchID},
3078 {.index = 3,
3079 .txType = "Payment",
3080 .result = "tesSUCCESS",
3081 .txHash = txIDs[2],
3082 .batchID = batchID},
3083 };
3084 validateClosedLedger(env, testCases);
3085
3086 // Alice does not exist; Bob receives XRP
3087 BEAST_EXPECT(env.le(keylet::account(alice)));
3088 BEAST_EXPECT(env.balance(bob) == preBob + XRP(3));
3089 }
3090
3091 // tfAllOrNothing: account delete fails
3092 {
3093 Env env{*this, features};
3094
3095 auto const alice = Account("alice");
3096 auto const bob = Account("bob");
3097 env.fund(XRP(10000), alice, bob);
3098 env.close();
3099
3100 incLgrSeqForAccDel(env, alice);
3101 for (int i = 0; i < 5; ++i)
3102 env.close();
3103
3104 auto const preAlice = env.balance(alice);
3105 auto const preBob = env.balance(bob);
3106
3107 auto const seq = env.seq(alice);
3108 auto const batchFee = batch::calcBatchFee(env, 0, 2) + env.current()->fees().increment;
3109 auto const [txIDs, batchID] = submitBatch(
3110 env,
3111 tesSUCCESS,
3112 batch::outer(alice, seq, batchFee, tfAllOrNothing),
3113 batch::Inner(pay(alice, bob, XRP(1)), seq + 1),
3114 batch::Inner(acctdelete(alice, bob), seq + 2),
3115 // terNO_ACCOUNT: alice does not exist
3116 batch::Inner(pay(alice, bob, XRP(2)), seq + 3));
3117 env.close();
3118
3119 std::vector<TestLedgerData> const testCases = {
3120 {.index = 0,
3121 .txType = "Batch",
3122 .result = "tesSUCCESS",
3123 .txHash = batchID,
3124 .batchID = std::nullopt},
3125 };
3126 validateClosedLedger(env, testCases);
3127
3128 // Alice still exists; Bob is unchanged
3129 BEAST_EXPECT(env.le(keylet::account(alice)));
3130 BEAST_EXPECT(env.balance(bob) == preBob);
3131 }
3132 }
3133
3134 void
3136 {
3137 testcase("loan");
3138
3139 bool const lendingBatchEnabled = features[featureLendingProtocolV1_2];
3140
3141 using namespace test::jtx;
3142
3143 Env env{*this, features};
3144
3145 Account const issuer{"issuer"};
3146 // For simplicity, lender will be the sole actor for the vault &
3147 // brokers.
3148 Account const lender{"lender"};
3149 // Borrower only wants to borrow
3150 Account const borrower{"borrower"};
3151
3152 // Fund the accounts and trust lines with the same amount so that tests
3153 // can use the same values regardless of the asset.
3154 env.fund(XRP(100'000), issuer, noripple(lender, borrower));
3155 env.close();
3156
3157 // Just use an XRP asset
3158 PrettyAsset const asset{xrpIssue(), 1'000'000};
3159
3160 Vault const vault{env};
3161
3162 auto const deposit = asset(50'000);
3163 auto const debtMaximumValue = asset(25'000).value();
3164 auto const coverDepositValue = asset(1000).value();
3165
3166 // Under featureLendingProtocolV1_1 LoanBrokerSet::preclaim only
3167 // accepts closed-ended vaults, so build one with a subscription
3168 // window that lets the lender deposit now, then advance the clock
3169 // past SubscriptionDate before creating loans.
3170 auto [tx, vaultKeylet, subscriptionDate] =
3171 vault.createClosedEnded({.owner = lender, .asset = asset});
3172 env(tx);
3173 env.close();
3174 BEAST_EXPECT(env.le(vaultKeylet));
3175
3176 env(vault.deposit({.depositor = lender, .id = vaultKeylet.key, .amount = deposit}));
3177 env.close();
3178
3179 // Move into the Investment phase before creating loans.
3180 vault.closePastSubscription(subscriptionDate);
3181
3182 auto const brokerKeylet =
3183 keylet::loanBroker(lender.id(), SeqProxy::rawSequence(env.seq(lender)));
3184
3185 {
3186 using namespace loan_broker;
3187 env(set(lender, vaultKeylet.key),
3188 kManagementFeeRate(TenthBips16(100)),
3189 kDebtMaximum(debtMaximumValue),
3190 kCoverRateMinimum(TenthBips32(percentageToTenthBips(10))),
3191 kCoverRateLiquidation(TenthBips32(percentageToTenthBips(25))));
3192
3193 env(coverDeposit(lender, brokerKeylet.key, coverDepositValue));
3194
3195 env.close();
3196 }
3197
3198 {
3199 using namespace loan;
3200 using namespace std::chrono_literals;
3201
3202 auto const lenderSeq = env.seq(lender);
3203 auto const batchFee = batch::calcBatchFee(env, 0, 2);
3204
3205 auto const loanKeylet = keylet::loan(brokerKeylet.key, SeqProxy::rawSequence(1));
3206 {
3207 auto const [txIDs, batchID] = submitBatch(
3208 env,
3209 lendingBatchEnabled ? temBAD_SIGNATURE : temINVALID_INNER_BATCH,
3210 batch::outer(lender, lenderSeq, batchFee, tfAllOrNothing),
3212 env.json(
3213 set(lender, brokerKeylet.key, asset(1000).value()),
3214 // Not allowed to include the counterparty signature
3215 Sig(sfCounterpartySignature, borrower),
3216 Sig(kNone),
3217 Fee(kNone),
3218 Seq(kNone)),
3219 lenderSeq + 1),
3221 pay(lender, loanKeylet.key, STAmount{asset, asset(500).value()}),
3222 lenderSeq + 2));
3223 }
3224 {
3225 auto const [txIDs, batchID] = submitBatch(
3226 env,
3228 batch::outer(lender, lenderSeq, batchFee, tfAllOrNothing),
3230 env.json(
3231 set(lender, brokerKeylet.key, asset(1000).value()),
3232 // Counterparty must be set
3233 Sig(kNone),
3234 Fee(kNone),
3235 Seq(kNone)),
3236 lenderSeq + 1),
3238 pay(lender, loanKeylet.key, STAmount{asset, asset(500).value()}),
3239 lenderSeq + 2));
3240 }
3241 {
3242 auto const [txIDs, batchID] = submitBatch(
3243 env,
3244 lendingBatchEnabled ? temBAD_SIGNER : temINVALID_INNER_BATCH,
3245 batch::outer(lender, lenderSeq, batchFee, tfAllOrNothing),
3247 env.json(
3248 set(lender, brokerKeylet.key, asset(1000).value()),
3249 // Counterparty must sign the outer transaction
3250 kCounterparty(borrower.id()),
3251 Sig(kNone),
3252 Fee(kNone),
3253 Seq(kNone)),
3254 lenderSeq + 1),
3256 pay(lender, loanKeylet.key, STAmount{asset, asset(500).value()}),
3257 lenderSeq + 2));
3258 }
3259 {
3260 // LoanSet normally charges at least 2x base fee, but since the
3261 // signature check is done by the batch, it only charges the
3262 // base fee.
3263 auto const batchFee = batch::calcBatchFee(env, 1, 2);
3264 auto const [txIDs, batchID] = submitBatch(
3265 env,
3266 lendingBatchEnabled ? TER(tesSUCCESS) : TER(temINVALID_INNER_BATCH),
3267 batch::outer(lender, lenderSeq, batchFee, tfAllOrNothing),
3269 env.json(
3270 set(lender, brokerKeylet.key, asset(1000).value()),
3271 kCounterparty(borrower.id()),
3272 Sig(kNone),
3273 Fee(kNone),
3274 Seq(kNone)),
3275 lenderSeq + 1),
3277 pay(
3278 // However, this inner transaction will fail,
3279 // because the lender is not allowed to draw the
3280 // transaction
3281 lender,
3282 loanKeylet.key,
3283 STAmount{asset, asset(500).value()}),
3284 lenderSeq + 2),
3285 batch::Sig(borrower));
3286 }
3287 env.close();
3288 BEAST_EXPECT(!env.le(loanKeylet));
3289 auto const brokerSleBefore = env.le(brokerKeylet);
3290 if (!BEAST_EXPECT(brokerSleBefore))
3291 return;
3292 auto const coverAvailableBefore = brokerSleBefore->at(sfCoverAvailable);
3293 auto const coverDepositAmount = asset(100).value();
3294 {
3295 // LoanSet normally charges at least 2x base fee, but since the
3296 // signature check is done by the batch, it only charges the
3297 // base fee.
3298 auto const lenderSeq = env.seq(lender);
3299 auto const batchFee = batch::calcBatchFee(env, 1, 2);
3300 auto const [txIDs, batchID] = submitBatch(
3301 env,
3302 lendingBatchEnabled ? TER(tesSUCCESS) : TER(temINVALID_INNER_BATCH),
3303 batch::outer(lender, lenderSeq, batchFee, tfAllOrNothing),
3305 env.json(
3306 set(lender, brokerKeylet.key, asset(1000).value()),
3307 kCounterparty(borrower.id()),
3308 Sig(kNone),
3309 Fee(kNone),
3310 Seq(kNone)),
3311 lenderSeq + 1),
3313 loan_broker::coverDeposit(lender, brokerKeylet.key, coverDepositAmount),
3314 lenderSeq + 2),
3315 batch::Sig(borrower));
3316 }
3317 env.close();
3318 BEAST_EXPECT(static_cast<bool>(env.le(loanKeylet)) == lendingBatchEnabled);
3319 if (auto const brokerSle = env.le(brokerKeylet); BEAST_EXPECT(brokerSle))
3320 {
3321 auto const expectedCover = lendingBatchEnabled
3322 ? coverAvailableBefore + coverDepositAmount
3323 : coverAvailableBefore;
3324 BEAST_EXPECT(brokerSle->at(sfCoverAvailable) == expectedCover);
3325 }
3326 }
3327 }
3328
3329 void
3331 {
3332 testcase("lending amendment");
3333
3334 using namespace test::jtx;
3335
3336 // Before LendingProtocolV1_2 a Vault inner transaction rejects the whole batch.
3337 auto const checkVaultBatch = [this](FeatureBitset amendments) {
3338 bool const lendingBatchEnabled = amendments[featureLendingProtocolV1_2];
3339 Env env{*this, amendments};
3340
3341 Account const payer{"payer"};
3342 Account const lender{"lender"};
3343 env.fund(XRP(100'000), payer, lender);
3344 env.close();
3345
3346 Vault const vault{env};
3347 auto [create, vaultKeylet] = vault.create({.owner = lender, .asset = xrpIssue()});
3348
3349 auto const payerSeq = env.seq(payer);
3350 auto const lenderSeq = env.seq(lender);
3351 auto const batchFee = batch::calcBatchFee(env, 1, 2);
3353 env,
3354 lendingBatchEnabled ? TER{tesSUCCESS} : TER{temINVALID_INNER_BATCH},
3355 batch::outer(payer, payerSeq, batchFee, tfAllOrNothing),
3356 batch::Inner(create, lenderSeq),
3358 vault.deposit(
3359 {.depositor = lender, .id = vaultKeylet.key, .amount = XRP(1'000)}),
3360 lenderSeq + 1),
3361 batch::Sig(lender));
3362 env.close();
3363
3364 BEAST_EXPECT(static_cast<bool>(env.le(vaultKeylet)) == lendingBatchEnabled);
3365 };
3366
3367 checkVaultBatch(features - featureLendingProtocolV1_2);
3368 checkVaultBatch(features);
3369 }
3370
3371 void
3373 {
3374 testcase("object create w/ sequence");
3375
3376 using namespace test::jtx;
3377 using namespace std::literals;
3378
3379 Env env{*this, features};
3380
3381 auto const alice = Account("alice");
3382 auto const bob = Account("bob");
3383 auto const gw = Account("gw");
3384 auto const usd = gw["USD"];
3385
3386 env.fund(XRP(10000), alice, bob, gw);
3387 env.close();
3388
3389 env.trust(usd(1000), alice, bob);
3390 env(pay(gw, alice, usd(100)));
3391 env(pay(gw, bob, usd(100)));
3392 env.close();
3393
3394 // success
3395 {
3396 auto const aliceSeq = env.seq(alice);
3397 auto const bobSeq = env.seq(bob);
3398 auto const preAlice = env.balance(alice);
3399 auto const preBob = env.balance(bob);
3400 auto const preAliceUSD = env.balance(alice, usd.issue());
3401 auto const preBobUSD = env.balance(bob, usd.issue());
3402
3403 auto const batchFee = batch::calcBatchFee(env, 1, 2);
3404 UInt256 const chkID{getCheckIndex(bob, env.seq(bob))};
3405 auto const [txIDs, batchID] = submitBatch(
3406 env,
3407 tesSUCCESS,
3408 batch::outer(alice, aliceSeq, batchFee, tfAllOrNothing),
3409 batch::Inner(check::create(bob, alice, usd(10)), bobSeq),
3410 batch::Inner(check::cash(alice, chkID, usd(10)), aliceSeq + 1),
3411 batch::Sig(bob));
3412 env.close();
3413
3414 std::vector<TestLedgerData> const testCases = {
3415 {.index = 0,
3416 .txType = "Batch",
3417 .result = "tesSUCCESS",
3418 .txHash = batchID,
3419 .batchID = std::nullopt},
3420 {.index = 1,
3421 .txType = "CheckCreate",
3422 .result = "tesSUCCESS",
3423 .txHash = txIDs[0],
3424 .batchID = batchID},
3425 {.index = 2,
3426 .txType = "CheckCash",
3427 .result = "tesSUCCESS",
3428 .txHash = txIDs[1],
3429 .batchID = batchID},
3430 };
3431 validateClosedLedger(env, testCases);
3432
3433 // Alice consumes sequences (# of txns)
3434 BEAST_EXPECT(env.seq(alice) == aliceSeq + 2);
3435
3436 // Alice consumes sequences (# of txns)
3437 BEAST_EXPECT(env.seq(bob) == bobSeq + 1);
3438
3439 // Alice pays Fee; Bob XRP Unchanged
3440 BEAST_EXPECT(env.balance(alice) == preAlice - batchFee);
3441 BEAST_EXPECT(env.balance(bob) == preBob);
3442
3443 // Alice pays USD & Bob receives USD
3444 BEAST_EXPECT(env.balance(alice, usd.issue()) == preAliceUSD + usd(10));
3445 BEAST_EXPECT(env.balance(bob, usd.issue()) == preBobUSD - usd(10));
3446 }
3447
3448 // failure
3449 {
3450 env(fset(alice, asfRequireDest));
3451 env.close();
3452
3453 auto const aliceSeq = env.seq(alice);
3454 auto const bobSeq = env.seq(bob);
3455 auto const preAlice = env.balance(alice);
3456 auto const preBob = env.balance(bob);
3457 auto const preAliceUSD = env.balance(alice, usd.issue());
3458 auto const preBobUSD = env.balance(bob, usd.issue());
3459
3460 auto const batchFee = batch::calcBatchFee(env, 1, 2);
3461 UInt256 const chkID{getCheckIndex(bob, env.seq(bob))};
3462 auto const [txIDs, batchID] = submitBatch(
3463 env,
3464 tesSUCCESS,
3465 batch::outer(alice, aliceSeq, batchFee, tfIndependent),
3466 // tecDST_TAG_NEEDED - alice has enabled asfRequireDest
3467 batch::Inner(check::create(bob, alice, usd(10)), bobSeq),
3468 batch::Inner(check::cash(alice, chkID, usd(10)), aliceSeq + 1),
3469 batch::Sig(bob));
3470 env.close();
3471
3472 std::vector<TestLedgerData> const testCases = {
3473 {.index = 0,
3474 .txType = "Batch",
3475 .result = "tesSUCCESS",
3476 .txHash = batchID,
3477 .batchID = std::nullopt},
3478 {.index = 1,
3479 .txType = "CheckCreate",
3480 .result = "tecDST_TAG_NEEDED",
3481 .txHash = txIDs[0],
3482 .batchID = batchID},
3483 {.index = 2,
3484 .txType = "CheckCash",
3485 .result = "tecNO_ENTRY",
3486 .txHash = txIDs[1],
3487 .batchID = batchID},
3488 };
3489 validateClosedLedger(env, testCases);
3490
3491 // Alice consumes sequences (# of txns)
3492 BEAST_EXPECT(env.seq(alice) == aliceSeq + 2);
3493
3494 // Bob consumes sequences (# of txns)
3495 BEAST_EXPECT(env.seq(bob) == bobSeq + 1);
3496
3497 // Alice pays Fee; Bob XRP Unchanged
3498 BEAST_EXPECT(env.balance(alice) == preAlice - batchFee);
3499 BEAST_EXPECT(env.balance(bob) == preBob);
3500
3501 // Alice pays USD & Bob receives USD
3502 BEAST_EXPECT(env.balance(alice, usd.issue()) == preAliceUSD);
3503 BEAST_EXPECT(env.balance(bob, usd.issue()) == preBobUSD);
3504 }
3505 }
3506
3507 void
3509 {
3510 testcase("object create w/ ticket");
3511
3512 using namespace test::jtx;
3513 using namespace std::literals;
3514
3515 Env env{*this, features};
3516
3517 auto const alice = Account("alice");
3518 auto const bob = Account("bob");
3519 auto const gw = Account("gw");
3520 auto const usd = gw["USD"];
3521
3522 env.fund(XRP(10000), alice, bob, gw);
3523 env.close();
3524
3525 env.trust(usd(1000), alice, bob);
3526 env(pay(gw, alice, usd(100)));
3527 env(pay(gw, bob, usd(100)));
3528 env.close();
3529
3530 auto const aliceSeq = env.seq(alice);
3531 auto const bobSeq = env.seq(bob);
3532 auto const preAlice = env.balance(alice);
3533 auto const preBob = env.balance(bob);
3534 auto const preAliceUSD = env.balance(alice, usd.issue());
3535 auto const preBobUSD = env.balance(bob, usd.issue());
3536
3537 auto const batchFee = batch::calcBatchFee(env, 1, 3);
3538 UInt256 const chkID{getCheckIndex(bob, bobSeq + 1)};
3539 auto const [txIDs, batchID] = submitBatch(
3540 env,
3541 tesSUCCESS,
3542 batch::outer(alice, aliceSeq, batchFee, tfAllOrNothing),
3543 batch::Inner(ticket::create(bob, 10), bobSeq),
3544 batch::Inner(check::create(bob, alice, usd(10)), 0, bobSeq + 1),
3545 batch::Inner(check::cash(alice, chkID, usd(10)), aliceSeq + 1),
3546 batch::Sig(bob));
3547 env.close();
3548
3549 std::vector<TestLedgerData> const testCases = {
3550 {.index = 0,
3551 .txType = "Batch",
3552 .result = "tesSUCCESS",
3553 .txHash = batchID,
3554 .batchID = std::nullopt},
3555 {.index = 1,
3556 .txType = "TicketCreate",
3557 .result = "tesSUCCESS",
3558 .txHash = txIDs[0],
3559 .batchID = batchID},
3560 {.index = 2,
3561 .txType = "CheckCreate",
3562 .result = "tesSUCCESS",
3563 .txHash = txIDs[1],
3564 .batchID = batchID},
3565 {.index = 3,
3566 .txType = "CheckCash",
3567 .result = "tesSUCCESS",
3568 .txHash = txIDs[2],
3569 .batchID = batchID},
3570 };
3571 validateClosedLedger(env, testCases);
3572
3573 BEAST_EXPECT(env.seq(alice) == aliceSeq + 2);
3574 BEAST_EXPECT(env.seq(bob) == bobSeq + 10 + 1);
3575 BEAST_EXPECT(env.balance(alice) == preAlice - batchFee);
3576 BEAST_EXPECT(env.balance(bob) == preBob);
3577 BEAST_EXPECT(env.balance(alice, usd.issue()) == preAliceUSD + usd(10));
3578 BEAST_EXPECT(env.balance(bob, usd.issue()) == preBobUSD - usd(10));
3579 }
3580
3581 void
3583 {
3584 testcase("object create w/ 3rd party");
3585
3586 using namespace test::jtx;
3587 using namespace std::literals;
3588
3589 Env env{*this, features};
3590
3591 auto const alice = Account("alice");
3592 auto const bob = Account("bob");
3593 auto const carol = Account("carol");
3594 auto const gw = Account("gw");
3595 auto const usd = gw["USD"];
3596
3597 env.fund(XRP(10000), alice, bob, carol, gw);
3598 env.close();
3599
3600 env.trust(usd(1000), alice, bob);
3601 env(pay(gw, alice, usd(100)));
3602 env(pay(gw, bob, usd(100)));
3603 env.close();
3604
3605 auto const aliceSeq = env.seq(alice);
3606 auto const bobSeq = env.seq(bob);
3607 auto const carolSeq = env.seq(carol);
3608 auto const preAlice = env.balance(alice);
3609 auto const preBob = env.balance(bob);
3610 auto const preCarol = env.balance(carol);
3611 auto const preAliceUSD = env.balance(alice, usd.issue());
3612 auto const preBobUSD = env.balance(bob, usd.issue());
3613
3614 auto const batchFee = batch::calcBatchFee(env, 2, 2);
3615 UInt256 const chkID{getCheckIndex(bob, env.seq(bob))};
3616 auto const [txIDs, batchID] = submitBatch(
3617 env,
3618 tesSUCCESS,
3619 batch::outer(carol, carolSeq, batchFee, tfAllOrNothing),
3620 batch::Inner(check::create(bob, alice, usd(10)), bobSeq),
3621 batch::Inner(check::cash(alice, chkID, usd(10)), aliceSeq),
3622 batch::Sig(alice, bob));
3623 env.close();
3624
3625 std::vector<TestLedgerData> const testCases = {
3626 {.index = 0,
3627 .txType = "Batch",
3628 .result = "tesSUCCESS",
3629 .txHash = batchID,
3630 .batchID = std::nullopt},
3631 {.index = 1,
3632 .txType = "CheckCreate",
3633 .result = "tesSUCCESS",
3634 .txHash = txIDs[0],
3635 .batchID = batchID},
3636 {.index = 2,
3637 .txType = "CheckCash",
3638 .result = "tesSUCCESS",
3639 .txHash = txIDs[1],
3640 .batchID = batchID},
3641 };
3642 validateClosedLedger(env, testCases);
3643
3644 BEAST_EXPECT(env.seq(alice) == aliceSeq + 1);
3645 BEAST_EXPECT(env.seq(bob) == bobSeq + 1);
3646 BEAST_EXPECT(env.seq(carol) == carolSeq + 1);
3647 BEAST_EXPECT(env.balance(alice) == preAlice);
3648 BEAST_EXPECT(env.balance(bob) == preBob);
3649 BEAST_EXPECT(env.balance(carol) == preCarol - batchFee);
3650 BEAST_EXPECT(env.balance(alice, usd.issue()) == preAliceUSD + usd(10));
3651 BEAST_EXPECT(env.balance(bob, usd.issue()) == preBobUSD - usd(10));
3652 }
3653
3654 void
3656 {
3657 {
3658 testcase("tickets outer");
3659
3660 using namespace test::jtx;
3661 using namespace std::literals;
3662
3663 Env env{*this, features};
3664
3665 auto const alice = Account("alice");
3666 auto const bob = Account("bob");
3667
3668 env.fund(XRP(10000), alice, bob);
3669 env.close();
3670
3671 std::uint32_t const aliceTicketSeq{env.seq(alice) + 1};
3672 env(ticket::create(alice, 10));
3673 env.close();
3674
3675 auto const aliceSeq = env.seq(alice);
3676 auto const preAlice = env.balance(alice);
3677 auto const preBob = env.balance(bob);
3678
3679 auto const batchFee = batch::calcBatchFee(env, 0, 2);
3680 auto const [txIDs, batchID] = submitBatch(
3681 env,
3682 tesSUCCESS,
3683 batch::outer(alice, 0, batchFee, tfAllOrNothing),
3684 batch::Inner(pay(alice, bob, XRP(1)), aliceSeq + 0),
3685 batch::Inner(pay(alice, bob, XRP(2)), aliceSeq + 1),
3686 ticket::Use(aliceTicketSeq));
3687 env.close();
3688
3689 std::vector<TestLedgerData> const testCases = {
3690 {.index = 0,
3691 .txType = "Batch",
3692 .result = "tesSUCCESS",
3693 .txHash = batchID,
3694 .batchID = std::nullopt},
3695 {.index = 1,
3696 .txType = "Payment",
3697 .result = "tesSUCCESS",
3698 .txHash = txIDs[0],
3699 .batchID = batchID},
3700 {.index = 2,
3701 .txType = "Payment",
3702 .result = "tesSUCCESS",
3703 .txHash = txIDs[1],
3704 .batchID = batchID},
3705 };
3706 validateClosedLedger(env, testCases);
3707
3708 auto const sle = env.le(keylet::account(alice));
3709 BEAST_EXPECT(sle);
3710 BEAST_EXPECT(sle->getFieldU32(sfOwnerCount) == 9);
3711 BEAST_EXPECT(sle->getFieldU32(sfTicketCount) == 9);
3712
3713 BEAST_EXPECT(env.seq(alice) == aliceSeq + 2);
3714 BEAST_EXPECT(env.balance(alice) == preAlice - XRP(3) - batchFee);
3715 BEAST_EXPECT(env.balance(bob) == preBob + XRP(3));
3716 }
3717
3718 {
3719 testcase("tickets inner");
3720
3721 using namespace test::jtx;
3722 using namespace std::literals;
3723
3724 Env env{*this, features};
3725
3726 auto const alice = Account("alice");
3727 auto const bob = Account("bob");
3728
3729 env.fund(XRP(10000), alice, bob);
3730 env.close();
3731
3732 std::uint32_t aliceTicketSeq{env.seq(alice) + 1};
3733 env(ticket::create(alice, 10));
3734 env.close();
3735
3736 auto const aliceSeq = env.seq(alice);
3737 auto const preAlice = env.balance(alice);
3738 auto const preBob = env.balance(bob);
3739
3740 auto const batchFee = batch::calcBatchFee(env, 0, 2);
3741 auto const [txIDs, batchID] = submitBatch(
3742 env,
3743 tesSUCCESS,
3744 batch::outer(alice, aliceSeq, batchFee, tfAllOrNothing),
3745 batch::Inner(pay(alice, bob, XRP(1)), 0, aliceTicketSeq),
3746 batch::Inner(pay(alice, bob, XRP(2)), 0, aliceTicketSeq + 1));
3747 env.close();
3748
3749 std::vector<TestLedgerData> const testCases = {
3750 {.index = 0,
3751 .txType = "Batch",
3752 .result = "tesSUCCESS",
3753 .txHash = batchID,
3754 .batchID = std::nullopt},
3755 {.index = 1,
3756 .txType = "Payment",
3757 .result = "tesSUCCESS",
3758 .txHash = txIDs[0],
3759 .batchID = batchID},
3760 {.index = 2,
3761 .txType = "Payment",
3762 .result = "tesSUCCESS",
3763 .txHash = txIDs[1],
3764 .batchID = batchID},
3765 };
3766 validateClosedLedger(env, testCases);
3767
3768 auto const sle = env.le(keylet::account(alice));
3769 BEAST_EXPECT(sle);
3770 BEAST_EXPECT(sle->getFieldU32(sfOwnerCount) == 8);
3771 BEAST_EXPECT(sle->getFieldU32(sfTicketCount) == 8);
3772
3773 BEAST_EXPECT(env.seq(alice) == aliceSeq + 1);
3774 BEAST_EXPECT(env.balance(alice) == preAlice - XRP(3) - batchFee);
3775 BEAST_EXPECT(env.balance(bob) == preBob + XRP(3));
3776 }
3777
3778 {
3779 testcase("tickets outer inner");
3780
3781 using namespace test::jtx;
3782 using namespace std::literals;
3783
3784 Env env{*this, features};
3785
3786 auto const alice = Account("alice");
3787 auto const bob = Account("bob");
3788
3789 env.fund(XRP(10000), alice, bob);
3790 env.close();
3791
3792 std::uint32_t const aliceTicketSeq{env.seq(alice) + 1};
3793 env(ticket::create(alice, 10));
3794 env.close();
3795
3796 auto const aliceSeq = env.seq(alice);
3797 auto const preAlice = env.balance(alice);
3798 auto const preBob = env.balance(bob);
3799
3800 auto const batchFee = batch::calcBatchFee(env, 0, 2);
3801 auto const [txIDs, batchID] = submitBatch(
3802 env,
3803 tesSUCCESS,
3804 batch::outer(alice, 0, batchFee, tfAllOrNothing),
3805 batch::Inner(pay(alice, bob, XRP(1)), 0, aliceTicketSeq + 1),
3806 batch::Inner(pay(alice, bob, XRP(2)), aliceSeq),
3807 ticket::Use(aliceTicketSeq));
3808 env.close();
3809
3810 std::vector<TestLedgerData> const testCases = {
3811 {.index = 0,
3812 .txType = "Batch",
3813 .result = "tesSUCCESS",
3814 .txHash = batchID,
3815 .batchID = std::nullopt},
3816 {.index = 1,
3817 .txType = "Payment",
3818 .result = "tesSUCCESS",
3819 .txHash = txIDs[0],
3820 .batchID = batchID},
3821 {.index = 2,
3822 .txType = "Payment",
3823 .result = "tesSUCCESS",
3824 .txHash = txIDs[1],
3825 .batchID = batchID},
3826 };
3827 validateClosedLedger(env, testCases);
3828
3829 auto const sle = env.le(keylet::account(alice));
3830 BEAST_EXPECT(sle);
3831 BEAST_EXPECT(sle->getFieldU32(sfOwnerCount) == 8);
3832 BEAST_EXPECT(sle->getFieldU32(sfTicketCount) == 8);
3833
3834 BEAST_EXPECT(env.seq(alice) == aliceSeq + 1);
3835 BEAST_EXPECT(env.balance(alice) == preAlice - XRP(3) - batchFee);
3836 BEAST_EXPECT(env.balance(bob) == preBob + XRP(3));
3837 }
3838 }
3839
3840 void
3842 {
3843 testcase("sequence open ledger");
3844
3845 using namespace test::jtx;
3846 using namespace std::literals;
3847
3848 auto const alice = Account("alice");
3849 auto const bob = Account("bob");
3850 auto const carol = Account("carol");
3851
3852 // Before Batch Txn w/ retry following ledger
3853 {
3854 // IMPORTANT: The batch txn is applied first, then the noop txn.
3855 // Because of this ordering, the noop txn is not applied and is
3856 // overwritten by the payment in the batch transaction. Because the
3857 // terPRE_SEQ is outside of the batch this noop transaction will ge
3858 // reapplied in the following ledger
3859 Env env{*this, features};
3860 env.fund(XRP(10000), alice, bob, carol);
3861 env.close();
3862
3863 auto const aliceSeq = env.seq(alice);
3864 auto const carolSeq = env.seq(carol);
3865
3866 // AccountSet Txn
3867 auto const noopTxn = env.jt(noop(alice), Seq(aliceSeq + 2));
3868 auto const noopTxnID = to_string(noopTxn.stx->getTransactionID());
3869 env(noopTxn, Ter(terPRE_SEQ));
3870
3871 // Batch Txn
3872 auto const batchFee = batch::calcBatchFee(env, 1, 2);
3873 auto const [txIDs, batchID] = submitBatch(
3874 env,
3875 tesSUCCESS,
3876 batch::outer(carol, carolSeq, batchFee, tfAllOrNothing),
3877 batch::Inner(pay(alice, bob, XRP(1)), aliceSeq),
3878 batch::Inner(pay(alice, bob, XRP(2)), aliceSeq + 1),
3879 batch::Sig(alice));
3880 env.close();
3881
3882 {
3883 std::vector<TestLedgerData> const testCases = {
3884 {.index = 0,
3885 .txType = "Batch",
3886 .result = "tesSUCCESS",
3887 .txHash = batchID,
3888 .batchID = std::nullopt},
3889 {.index = 1,
3890 .txType = "Payment",
3891 .result = "tesSUCCESS",
3892 .txHash = txIDs[0],
3893 .batchID = batchID},
3894 {.index = 2,
3895 .txType = "Payment",
3896 .result = "tesSUCCESS",
3897 .txHash = txIDs[1],
3898 .batchID = batchID},
3899 };
3900 validateClosedLedger(env, testCases);
3901 }
3902
3903 env.close();
3904 {
3905 // next ledger contains noop txn
3906 std::vector<TestLedgerData> const testCases = {
3907 {.index = 0,
3908 .txType = "AccountSet",
3909 .result = "tesSUCCESS",
3910 .txHash = noopTxnID,
3911 .batchID = std::nullopt},
3912 };
3913 validateClosedLedger(env, testCases);
3914 }
3915 }
3916
3917 // Before Batch Txn w/ same sequence
3918 {
3919 // IMPORTANT: The batch txn is applied first, then the noop txn.
3920 // Because of this ordering, the noop txn is not applied and is
3921 // overwritten by the payment in the batch transaction.
3922 Env env{*this, features};
3923 env.fund(XRP(10000), alice, bob);
3924 env.close();
3925
3926 auto const aliceSeq = env.seq(alice);
3927
3928 // AccountSet Txn
3929 auto const noopTxn = env.jt(noop(alice), Seq(aliceSeq + 1));
3930 env(noopTxn, Ter(terPRE_SEQ));
3931
3932 // Batch Txn
3933 auto const batchFee = batch::calcBatchFee(env, 0, 2);
3934 auto const [txIDs, batchID] = submitBatch(
3935 env,
3936 tesSUCCESS,
3937 batch::outer(alice, aliceSeq, batchFee, tfAllOrNothing),
3938 batch::Inner(pay(alice, bob, XRP(1)), aliceSeq + 1),
3939 batch::Inner(pay(alice, bob, XRP(2)), aliceSeq + 2));
3940 env.close();
3941
3942 {
3943 std::vector<TestLedgerData> const testCases = {
3944 {.index = 0,
3945 .txType = "Batch",
3946 .result = "tesSUCCESS",
3947 .txHash = batchID,
3948 .batchID = std::nullopt},
3949 {.index = 1,
3950 .txType = "Payment",
3951 .result = "tesSUCCESS",
3952 .txHash = txIDs[0],
3953 .batchID = batchID},
3954 {.index = 2,
3955 .txType = "Payment",
3956 .result = "tesSUCCESS",
3957 .txHash = txIDs[1],
3958 .batchID = batchID},
3959 };
3960 validateClosedLedger(env, testCases);
3961 }
3962
3963 env.close();
3964 {
3965 // next ledger is empty
3966 std::vector<TestLedgerData> const testCases = {};
3967 validateClosedLedger(env, testCases);
3968 }
3969 }
3970
3971 // After Batch Txn w/ same sequence
3972 {
3973 // IMPORTANT: The batch txn is applied first, then the noop txn.
3974 // Because of this ordering, the noop txn is not applied and is
3975 // overwritten by the payment in the batch transaction.
3976 Env env{*this, features};
3977 env.fund(XRP(10000), alice, bob);
3978 env.close();
3979
3980 auto const aliceSeq = env.seq(alice);
3981 auto const batchFee = batch::calcBatchFee(env, 0, 2);
3982 auto const [txIDs, batchID] = submitBatch(
3983 env,
3984 tesSUCCESS,
3985 batch::outer(alice, aliceSeq, batchFee, tfAllOrNothing),
3986 batch::Inner(pay(alice, bob, XRP(1)), aliceSeq + 1),
3987 batch::Inner(pay(alice, bob, XRP(2)), aliceSeq + 2));
3988
3989 auto const noopTxn = env.jt(noop(alice), Seq(aliceSeq + 1));
3990 env(noopTxn, Ter(tesSUCCESS));
3991 env.close();
3992
3993 {
3994 std::vector<TestLedgerData> const testCases = {
3995 {.index = 0,
3996 .txType = "Batch",
3997 .result = "tesSUCCESS",
3998 .txHash = batchID,
3999 .batchID = std::nullopt},
4000 {.index = 1,
4001 .txType = "Payment",
4002 .result = "tesSUCCESS",
4003 .txHash = txIDs[0],
4004 .batchID = batchID},
4005 {.index = 2,
4006 .txType = "Payment",
4007 .result = "tesSUCCESS",
4008 .txHash = txIDs[1],
4009 .batchID = batchID},
4010 };
4011 validateClosedLedger(env, testCases);
4012 }
4013
4014 env.close();
4015 {
4016 // next ledger is empty
4017 std::vector<TestLedgerData> const testCases = {};
4018 validateClosedLedger(env, testCases);
4019 }
4020 }
4021
4022 // Outer Batch terPRE_SEQ
4023 {
4024 Env env{*this, features};
4025 env.fund(XRP(10000), alice, bob, carol);
4026 env.close();
4027
4028 auto const aliceSeq = env.seq(alice);
4029 auto const carolSeq = env.seq(carol);
4030
4031 // Batch Txn
4032 auto const batchFee = batch::calcBatchFee(env, 1, 2);
4033 auto const [txIDs, batchID] = submitBatch(
4034 env,
4035 terPRE_SEQ,
4036 batch::outer(carol, carolSeq + 1, batchFee, tfAllOrNothing),
4037 batch::Inner(pay(alice, bob, XRP(1)), aliceSeq),
4038 batch::Inner(pay(alice, bob, XRP(2)), aliceSeq + 1),
4039 batch::Sig(alice));
4040
4041 // AccountSet Txn
4042 auto const noopTxn = env.jt(noop(carol), Seq(carolSeq));
4043 auto const noopTxnID = to_string(noopTxn.stx->getTransactionID());
4044 env(noopTxn, Ter(tesSUCCESS));
4045 env.close();
4046
4047 {
4048 std::vector<TestLedgerData> const testCases = {
4049 {.index = 0,
4050 .txType = "AccountSet",
4051 .result = "tesSUCCESS",
4052 .txHash = noopTxnID,
4053 .batchID = std::nullopt},
4054 {.index = 1,
4055 .txType = "Batch",
4056 .result = "tesSUCCESS",
4057 .txHash = batchID,
4058 .batchID = std::nullopt},
4059 {.index = 2,
4060 .txType = "Payment",
4061 .result = "tesSUCCESS",
4062 .txHash = txIDs[0],
4063 .batchID = batchID},
4064 {.index = 3,
4065 .txType = "Payment",
4066 .result = "tesSUCCESS",
4067 .txHash = txIDs[1],
4068 .batchID = batchID},
4069 };
4070 validateClosedLedger(env, testCases);
4071 }
4072
4073 env.close();
4074 {
4075 // next ledger contains no transactions
4076 std::vector<TestLedgerData> const testCases = {};
4077 validateClosedLedger(env, testCases);
4078 }
4079 }
4080 }
4081
4082 void
4084 {
4085 testcase("tickets open ledger");
4086
4087 using namespace test::jtx;
4088 using namespace std::literals;
4089
4090 auto const alice = Account("alice");
4091 auto const bob = Account("bob");
4092
4093 // Before Batch Txn w/ same ticket
4094 {
4095 // IMPORTANT: The batch txn is applied first, then the noop txn.
4096 // Because of this ordering, the noop txn is not applied and is
4097 // overwritten by the payment in the batch transaction.
4098 Env env{*this, features};
4099 env.fund(XRP(10000), alice, bob);
4100 env.close();
4101
4102 std::uint32_t const aliceTicketSeq{env.seq(alice) + 1};
4103 env(ticket::create(alice, 10));
4104 env.close();
4105
4106 auto const aliceSeq = env.seq(alice);
4107
4108 // AccountSet Txn
4109 auto const noopTxn = env.jt(noop(alice), ticket::Use(aliceTicketSeq + 1));
4110 env(noopTxn, Ter(tesSUCCESS));
4111
4112 // Batch Txn
4113 auto const batchFee = batch::calcBatchFee(env, 0, 2);
4114 auto const [txIDs, batchID] = submitBatch(
4115 env,
4116 tesSUCCESS,
4117 batch::outer(alice, 0, batchFee, tfAllOrNothing),
4118 batch::Inner(pay(alice, bob, XRP(1)), 0, aliceTicketSeq + 1),
4119 batch::Inner(pay(alice, bob, XRP(2)), aliceSeq),
4120 ticket::Use(aliceTicketSeq));
4121 env.close();
4122
4123 {
4124 std::vector<TestLedgerData> const testCases = {
4125 {.index = 0,
4126 .txType = "Batch",
4127 .result = "tesSUCCESS",
4128 .txHash = batchID,
4129 .batchID = std::nullopt},
4130 {.index = 1,
4131 .txType = "Payment",
4132 .result = "tesSUCCESS",
4133 .txHash = txIDs[0],
4134 .batchID = batchID},
4135 {.index = 2,
4136 .txType = "Payment",
4137 .result = "tesSUCCESS",
4138 .txHash = txIDs[1],
4139 .batchID = batchID},
4140 };
4141 validateClosedLedger(env, testCases);
4142 }
4143
4144 env.close();
4145 {
4146 // next ledger is empty
4147 std::vector<TestLedgerData> const testCases = {};
4148 validateClosedLedger(env, testCases);
4149 }
4150 }
4151
4152 // After Batch Txn w/ same ticket
4153 {
4154 // IMPORTANT: The batch txn is applied first, then the noop txn.
4155 // Because of this ordering, the noop txn is not applied and is
4156 // overwritten by the payment in the batch transaction.
4157 Env env{*this, features};
4158 env.fund(XRP(10000), alice, bob);
4159 env.close();
4160
4161 std::uint32_t const aliceTicketSeq{env.seq(alice) + 1};
4162 env(ticket::create(alice, 10));
4163 env.close();
4164
4165 auto const aliceSeq = env.seq(alice);
4166
4167 // Batch Txn
4168 auto const batchFee = batch::calcBatchFee(env, 0, 2);
4169 auto const [txIDs, batchID] = submitBatch(
4170 env,
4171 tesSUCCESS,
4172 batch::outer(alice, 0, batchFee, tfAllOrNothing),
4173 batch::Inner(pay(alice, bob, XRP(1)), 0, aliceTicketSeq + 1),
4174 batch::Inner(pay(alice, bob, XRP(2)), aliceSeq),
4175 ticket::Use(aliceTicketSeq));
4176
4177 // AccountSet Txn
4178 auto const noopTxn = env.jt(noop(alice), ticket::Use(aliceTicketSeq + 1));
4179 env(noopTxn);
4180
4181 env.close();
4182 {
4183 std::vector<TestLedgerData> const testCases = {
4184 {.index = 0,
4185 .txType = "Batch",
4186 .result = "tesSUCCESS",
4187 .txHash = batchID,
4188 .batchID = std::nullopt},
4189 {.index = 1,
4190 .txType = "Payment",
4191 .result = "tesSUCCESS",
4192 .txHash = txIDs[0],
4193 .batchID = batchID},
4194 {.index = 2,
4195 .txType = "Payment",
4196 .result = "tesSUCCESS",
4197 .txHash = txIDs[1],
4198 .batchID = batchID},
4199 };
4200 validateClosedLedger(env, testCases);
4201 }
4202
4203 env.close();
4204 {
4205 // next ledger is empty
4206 std::vector<TestLedgerData> const testCases = {};
4207 validateClosedLedger(env, testCases);
4208 }
4209 }
4210 }
4211
4212 void
4214 {
4215 testcase("objects open ledger");
4216
4217 using namespace test::jtx;
4218 using namespace std::literals;
4219
4220 auto const alice = Account("alice");
4221 auto const bob = Account("bob");
4222
4223 // Consume Object Before Batch Txn
4224 {
4225 // IMPORTANT: The initial result of `CheckCash` is tecNO_ENTRY
4226 // because the create transaction has not been applied because the
4227 // batch will run in the close ledger process. The batch will be
4228 // allied and then retry this transaction in the current ledger.
4229
4230 Env env{*this, features};
4231 env.fund(XRP(10000), alice, bob);
4232 env.close();
4233
4234 std::uint32_t const aliceTicketSeq{env.seq(alice) + 1};
4235 env(ticket::create(alice, 10));
4236 env.close();
4237
4238 auto const aliceSeq = env.seq(alice);
4239
4240 // CheckCash Txn
4241 UInt256 const chkID{getCheckIndex(alice, aliceSeq)};
4242 auto const objTxn = env.jt(check::cash(bob, chkID, XRP(10)));
4243 auto const objTxnID = to_string(objTxn.stx->getTransactionID());
4244 env(objTxn, Ter(tecNO_ENTRY));
4245
4246 // Batch Txn
4247 auto const batchFee = batch::calcBatchFee(env, 0, 2);
4248 auto const [txIDs, batchID] = submitBatch(
4249 env,
4250 tesSUCCESS,
4251 batch::outer(alice, 0, batchFee, tfAllOrNothing),
4252 batch::Inner(check::create(alice, bob, XRP(10)), aliceSeq),
4253 batch::Inner(pay(alice, bob, XRP(1)), 0, aliceTicketSeq + 1),
4254 ticket::Use(aliceTicketSeq));
4255
4256 env.close();
4257 {
4258 std::vector<TestLedgerData> const testCases = {
4259 {.index = 0,
4260 .txType = "Batch",
4261 .result = "tesSUCCESS",
4262 .txHash = batchID,
4263 .batchID = std::nullopt},
4264 {.index = 1,
4265 .txType = "CheckCreate",
4266 .result = "tesSUCCESS",
4267 .txHash = txIDs[0],
4268 .batchID = batchID},
4269 {.index = 2,
4270 .txType = "Payment",
4271 .result = "tesSUCCESS",
4272 .txHash = txIDs[1],
4273 .batchID = batchID},
4274 {.index = 3,
4275 .txType = "CheckCash",
4276 .result = "tesSUCCESS",
4277 .txHash = objTxnID,
4278 .batchID = std::nullopt},
4279 };
4280 validateClosedLedger(env, testCases);
4281 }
4282
4283 env.close();
4284 {
4285 // next ledger is empty
4286 std::vector<TestLedgerData> const testCases = {};
4287 validateClosedLedger(env, testCases);
4288 }
4289 }
4290
4291 // Create Object Before Batch Txn
4292 {
4293 Env env{*this, features};
4294 env.fund(XRP(10000), alice, bob);
4295 env.close();
4296
4297 std::uint32_t const aliceTicketSeq{env.seq(alice) + 1};
4298 env(ticket::create(alice, 10));
4299 env.close();
4300
4301 auto const aliceSeq = env.seq(alice);
4302 auto const bobSeq = env.seq(bob);
4303
4304 // CheckCreate Txn
4305 UInt256 const chkID{getCheckIndex(alice, aliceSeq)};
4306 auto const objTxn = env.jt(check::create(alice, bob, XRP(10)));
4307 auto const objTxnID = to_string(objTxn.stx->getTransactionID());
4308 env(objTxn, Ter(tesSUCCESS));
4309
4310 // Batch Txn
4311 auto const batchFee = batch::calcBatchFee(env, 1, 2);
4312 auto const [txIDs, batchID] = submitBatch(
4313 env,
4314 tesSUCCESS,
4315 batch::outer(alice, 0, batchFee, tfAllOrNothing),
4316 batch::Inner(check::cash(bob, chkID, XRP(10)), bobSeq),
4317 batch::Inner(pay(alice, bob, XRP(1)), 0, aliceTicketSeq + 1),
4318 ticket::Use(aliceTicketSeq),
4319 batch::Sig(bob));
4320
4321 env.close();
4322 {
4323 std::vector<TestLedgerData> const testCases = {
4324 {.index = 0,
4325 .txType = "CheckCreate",
4326 .result = "tesSUCCESS",
4327 .txHash = objTxnID,
4328 .batchID = std::nullopt},
4329 {.index = 1,
4330 .txType = "Batch",
4331 .result = "tesSUCCESS",
4332 .txHash = batchID,
4333 .batchID = std::nullopt},
4334 {.index = 2,
4335 .txType = "CheckCash",
4336 .result = "tesSUCCESS",
4337 .txHash = txIDs[0],
4338 .batchID = batchID},
4339 {.index = 3,
4340 .txType = "Payment",
4341 .result = "tesSUCCESS",
4342 .txHash = txIDs[1],
4343 .batchID = batchID},
4344 };
4345 validateClosedLedger(env, testCases);
4346 }
4347 }
4348
4349 // After Batch Txn
4350 {
4351 // IMPORTANT: The initial result of `CheckCash` is tecNO_ENTRY
4352 // because the create transaction has not been applied because the
4353 // batch will run in the close ledger process. The batch will be
4354 // applied and then retry this transaction in the current ledger.
4355
4356 Env env{*this, features};
4357 env.fund(XRP(10000), alice, bob);
4358 env.close();
4359
4360 std::uint32_t const aliceTicketSeq{env.seq(alice) + 1};
4361 env(ticket::create(alice, 10));
4362 env.close();
4363
4364 auto const aliceSeq = env.seq(alice);
4365
4366 // Batch Txn
4367 auto const batchFee = batch::calcBatchFee(env, 0, 2);
4368 UInt256 const chkID{getCheckIndex(alice, aliceSeq)};
4369 auto const [txIDs, batchID] = submitBatch(
4370 env,
4371 tesSUCCESS,
4372 batch::outer(alice, 0, batchFee, tfAllOrNothing),
4373 batch::Inner(check::create(alice, bob, XRP(10)), aliceSeq),
4374 batch::Inner(pay(alice, bob, XRP(1)), 0, aliceTicketSeq + 1),
4375 ticket::Use(aliceTicketSeq));
4376
4377 // CheckCash Txn
4378 auto const objTxn = env.jt(check::cash(bob, chkID, XRP(10)));
4379 auto const objTxnID = to_string(objTxn.stx->getTransactionID());
4380 env(objTxn, Ter(tecNO_ENTRY));
4381
4382 env.close();
4383 {
4384 std::vector<TestLedgerData> const testCases = {
4385 {.index = 0,
4386 .txType = "Batch",
4387 .result = "tesSUCCESS",
4388 .txHash = batchID,
4389 .batchID = std::nullopt},
4390 {.index = 1,
4391 .txType = "CheckCreate",
4392 .result = "tesSUCCESS",
4393 .txHash = txIDs[0],
4394 .batchID = batchID},
4395 {.index = 2,
4396 .txType = "Payment",
4397 .result = "tesSUCCESS",
4398 .txHash = txIDs[1],
4399 .batchID = batchID},
4400 {.index = 3,
4401 .txType = "CheckCash",
4402 .result = "tesSUCCESS",
4403 .txHash = objTxnID,
4404 .batchID = std::nullopt},
4405 };
4406 validateClosedLedger(env, testCases);
4407 }
4408 }
4409 }
4410
4411 void
4413 {
4414 testcase("pseudo txn with tfInnerBatchTxn");
4415
4416 using namespace test::jtx;
4417 using namespace std::literals;
4418
4419 Env env{*this, features};
4420
4421 auto const alice = Account("alice");
4422 auto const bob = Account("bob");
4423 env.fund(XRP(10000), alice, bob);
4424 env.close();
4425
4426 STTx const stx = STTx(ttAMENDMENT, [&](auto& obj) {
4427 obj.setAccountID(sfAccount, AccountID());
4428 obj.setFieldH256(sfAmendment, UInt256(2));
4429 obj.setFieldU32(sfLedgerSequence, env.seq(alice));
4430 obj.setFieldU32(sfFlags, tfInnerBatchTxn);
4431 });
4432
4433 std::string reason;
4434 BEAST_EXPECT(isPseudoTx(stx));
4435 BEAST_EXPECT(!passesLocalChecks(stx, reason));
4436 BEAST_EXPECT(reason == "Cannot submit pseudo transactions.");
4437 env.app().getOpenLedger().modify([&](OpenView& view, beast::Journal j) {
4438 auto const result = xrpl::apply(env.app(), view, stx, TapNone, j);
4439 BEAST_EXPECT(!result.applied && result.ter == temINVALID_FLAG);
4440 return result.applied;
4441 });
4442 }
4443
4444 void
4446 {
4447 testcase("batch open ledger");
4448 // IMPORTANT: When a transaction is submitted outside of a batch and
4449 // another transaction is part of the batch, the batch might fail
4450 // because the sequence is out of order. This is because the canonical
4451 // order of transactions is determined by the account first. So in this
4452 // case, alice's batch comes after bob's self submitted transaction even
4453 // though the payment was submitted after the batch.
4454
4455 using namespace test::jtx;
4456 using namespace std::literals;
4457
4458 Env env{*this, features};
4459 XRPAmount const baseFee = env.current()->fees().base;
4460
4461 auto const alice = Account("alice");
4462 auto const bob = Account("bob");
4463
4464 env.fund(XRP(10000), alice, bob);
4465 env.close();
4466
4467 env(noop(bob), Ter(tesSUCCESS));
4468 env.close();
4469
4470 auto const aliceSeq = env.seq(alice);
4471 auto const preAlice = env.balance(alice);
4472 auto const preBob = env.balance(bob);
4473 auto const bobSeq = env.seq(bob);
4474
4475 // Alice Pays Bob (Open Ledger)
4476 auto const payTxn1 = env.jt(pay(alice, bob, XRP(10)), Seq(aliceSeq));
4477 auto const payTxn1ID = to_string(payTxn1.stx->getTransactionID());
4478 env(payTxn1, Ter(tesSUCCESS));
4479
4480 // Alice & Bob Atomic Batch
4481 auto const batchFee = batch::calcBatchFee(env, 1, 2);
4482 auto const [txIDs, batchID] = submitBatch(
4483 env,
4484 tesSUCCESS,
4485 batch::outer(alice, aliceSeq + 1, batchFee, tfAllOrNothing),
4486 batch::Inner(pay(alice, bob, XRP(10)), aliceSeq + 2),
4487 batch::Inner(pay(bob, alice, XRP(5)), bobSeq),
4488 batch::Sig(bob));
4489
4490 // Bob pays Alice (Open Ledger)
4491 auto const payTxn2 = env.jt(pay(bob, alice, XRP(5)), Seq(bobSeq + 1));
4492 auto const payTxn2ID = to_string(payTxn2.stx->getTransactionID());
4493 env(payTxn2, Ter(terPRE_SEQ));
4494 env.close();
4495
4496 std::vector<TestLedgerData> const testCases = {
4497 {.index = 0,
4498 .txType = "Payment",
4499 .result = "tesSUCCESS",
4500 .txHash = payTxn1ID,
4501 .batchID = std::nullopt},
4502 {.index = 1,
4503 .txType = "Batch",
4504 .result = "tesSUCCESS",
4505 .txHash = batchID,
4506 .batchID = std::nullopt},
4507 {.index = 2,
4508 .txType = "Payment",
4509 .result = "tesSUCCESS",
4510 .txHash = txIDs[0],
4511 .batchID = batchID},
4512 {.index = 3,
4513 .txType = "Payment",
4514 .result = "tesSUCCESS",
4515 .txHash = txIDs[1],
4516 .batchID = batchID},
4517 };
4518 validateClosedLedger(env, testCases);
4519
4520 env.close();
4521 {
4522 // next ledger includes the payment txn
4523 std::vector<TestLedgerData> const testCases = {
4524 {.index = 0,
4525 .txType = "Payment",
4526 .result = "tesSUCCESS",
4527 .txHash = payTxn2ID,
4528 .batchID = std::nullopt},
4529 };
4530 validateClosedLedger(env, testCases);
4531 }
4532
4533 // Alice consumes sequences (# of txns)
4534 BEAST_EXPECT(env.seq(alice) == aliceSeq + 3);
4535
4536 // Alice consumes sequences (# of txns)
4537 BEAST_EXPECT(env.seq(bob) == bobSeq + 2);
4538
4539 // Alice pays XRP & Fee; Bob receives XRP & pays Fee
4540 BEAST_EXPECT(env.balance(alice) == preAlice - XRP(10) - batchFee - baseFee);
4541 BEAST_EXPECT(env.balance(bob) == preBob + XRP(10) - baseFee);
4542 }
4543
4544 void
4546 {
4547 testcase("batch tx queue");
4548
4549 using namespace test::jtx;
4550 using namespace std::literals;
4551
4552 // A Batch is never queued. Under open-ledger congestion a Batch that
4553 // pays only its base fee cannot apply directly and, unlike an ordinary
4554 // transaction, is rejected outright rather than held in the queue. A
4555 // Batch that pays the escalated open-ledger fee applies directly.
4556 {
4557 Env env{
4558 *this,
4560 features,
4561 nullptr,
4563
4564 auto alice = Account("alice");
4565 auto bob = Account("bob");
4566 auto carol = Account("carol");
4567
4568 // Fund across several ledgers so the TxQ metrics stay restricted.
4569 env.fund(XRP(10000), noripple(alice, bob));
4570 env.close(env.now() + 5s, 10000ms);
4571 env.fund(XRP(10000), noripple(carol));
4572 env.close(env.now() + 5s, 10000ms);
4573
4574 // Fill the open ledger so escalation is active.
4575 env(noop(alice));
4576 env(noop(alice));
4577 env(noop(alice));
4578 checkMetrics(*this, env, 0, std::nullopt, 3, 2);
4579
4580 env(noop(carol), Ter(terQUEUED));
4581 checkMetrics(*this, env, 1, std::nullopt, 3, 2);
4582
4583 auto const aliceSeq = env.seq(alice);
4584 auto const bobSeq = env.seq(bob);
4585 auto const batchFee = batch::calcBatchFee(env, 1, 2);
4586
4587 // Paying only the base fee, the Batch cannot enter the congested
4588 // open ledger and is rejected rather than queued.
4589 env(batch::outer(alice, aliceSeq, batchFee, tfAllOrNothing),
4590 batch::Inner(pay(alice, bob, XRP(10)), aliceSeq + 1),
4591 batch::Inner(pay(bob, alice, XRP(5)), bobSeq),
4592 batch::Sig(bob),
4594
4595 // The Batch was not queued; only carol's transaction is queued.
4596 checkMetrics(*this, env, 1, std::nullopt, 3, 2);
4597
4598 // Paying the escalated open-ledger fee, the Batch applies directly.
4599 env(batch::outer(alice, aliceSeq, openLedgerFee(env, batchFee), tfAllOrNothing),
4600 batch::Inner(pay(alice, bob, XRP(10)), aliceSeq + 1),
4601 batch::Inner(pay(bob, alice, XRP(5)), bobSeq),
4602 batch::Sig(bob),
4603 Ter(tesSUCCESS));
4604 }
4605
4606 // inner batch transactions are counter towards the ledger tx count
4607 {
4608 Env env{
4609 *this,
4611 features,
4612 nullptr,
4614
4615 auto alice = Account("alice");
4616 auto bob = Account("bob");
4617 auto carol = Account("carol");
4618
4619 // Fund across several ledgers so the TxQ metrics stay restricted.
4620 env.fund(XRP(10000), noripple(alice, bob));
4621 env.close(env.now() + 5s, 10000ms);
4622 env.fund(XRP(10000), noripple(carol));
4623 env.close(env.now() + 5s, 10000ms);
4624
4625 // Fill the ledger leaving room for 1 queued transaction
4626 env(noop(alice));
4627 env(noop(alice));
4628 checkMetrics(*this, env, 0, std::nullopt, 2, 2);
4629
4630 auto const aliceSeq = env.seq(alice);
4631 auto const bobSeq = env.seq(bob);
4632 auto const batchFee = batch::calcBatchFee(env, 1, 2);
4633
4634 // Batch Successful
4635 {
4636 env(batch::outer(alice, aliceSeq, batchFee, tfAllOrNothing),
4637 batch::Inner(pay(alice, bob, XRP(10)), aliceSeq + 1),
4638 batch::Inner(pay(bob, alice, XRP(5)), bobSeq),
4639 batch::Sig(bob),
4640 Ter(tesSUCCESS));
4641 }
4642
4643 checkMetrics(*this, env, 0, std::nullopt, 3, 2);
4644
4645 env(noop(carol), Ter(terQUEUED));
4646 checkMetrics(*this, env, 1, std::nullopt, 3, 2);
4647 }
4648
4649 // A Batch is never queued, so it also cannot sit behind the account's
4650 // already-queued transactions: with a sequence gap it cannot apply
4651 // directly and is rejected rather than queued.
4652 {
4653 Env env{
4654 *this,
4656 features,
4657 nullptr,
4659
4660 auto alice = Account("alice");
4661 auto bob = Account("bob");
4662
4663 env.fund(XRP(10000), noripple(alice, bob));
4664 env.close(env.now() + 5s, 10000ms);
4665
4666 // Fill the open ledger so subsequent transactions queue.
4667 env(noop(alice));
4668 env(noop(alice));
4669 env(noop(alice));
4670 checkMetrics(*this, env, 0, std::nullopt, 3, 2);
4671
4672 auto const aliceSeq = env.seq(alice);
4673
4674 // Queue two normal transactions for alice.
4675 env(noop(alice), Seq(aliceSeq + 0), Ter(terQUEUED));
4676 env(noop(alice), Seq(aliceSeq + 1), Ter(terQUEUED));
4677 checkMetrics(*this, env, 2, std::nullopt, 3, 2);
4678
4679 // The Batch's sequence sits behind the queued transactions, so it
4680 // cannot apply directly and is rejected rather than queued.
4681 auto const bobSeq = env.seq(bob);
4682 auto const batchFee = batch::calcBatchFee(env, 1, 2);
4683 env(batch::outer(alice, aliceSeq + 2, batchFee, tfAllOrNothing),
4684 batch::Inner(pay(alice, bob, XRP(10)), aliceSeq + 3),
4685 batch::Inner(pay(bob, alice, XRP(5)), bobSeq),
4686 batch::Sig(bob),
4688
4689 // The two queued transactions are untouched.
4690 checkMetrics(*this, env, 2, std::nullopt, 3, 2);
4691 }
4692 }
4693
4694 void
4696 {
4697 testcase("batch network ops");
4698
4699 using namespace test::jtx;
4700 using namespace std::literals;
4701
4702 Env env(*this, envconfig(), features, nullptr, beast::Severity::Disabled);
4703
4704 auto alice = Account("alice");
4705 auto bob = Account("bob");
4706 env.fund(XRP(10000), alice, bob);
4707 env.close();
4708
4709 auto submitTx = [&](std::uint32_t flags) -> UInt256 {
4710 auto jt = env.jt(pay(alice, bob, XRP(1)), Txflags(flags));
4711 Serializer s;
4712 jt.stx->add(s);
4713 env.app().getOPs().submitTransaction(jt.stx);
4714 return jt.stx->getTransactionID();
4715 };
4716
4717 auto processTxn = [&](std::uint32_t flags) -> UInt256 {
4718 auto jt = env.jt(pay(alice, bob, XRP(1)), Txflags(flags));
4719 Serializer s;
4720 jt.stx->add(s);
4721 std::string reason;
4722 auto transaction = std::make_shared<Transaction>(jt.stx, reason, env.app());
4724 transaction, false, true, NetworkOPs::FailHard::Yes);
4725 return transaction->getID();
4726 };
4727
4728 // Validate: NetworkOPs::submitTransaction()
4729 {
4730 // Submit a tx with tfInnerBatchTxn
4731 UInt256 const txBad = submitTx(tfInnerBatchTxn);
4732 BEAST_EXPECT(env.app().getHashRouter().getFlags(txBad) == HashRouterFlags::UNDEFINED);
4733 }
4734
4735 // Validate: NetworkOPs::processTransaction()
4736 {
4737 UInt256 const txid = processTxn(tfInnerBatchTxn);
4738 // HashRouter::getFlags() should return LedgerFlags::BAD
4739 BEAST_EXPECT(env.app().getHashRouter().getFlags(txid) == HashRouterFlags::BAD);
4740 }
4741 }
4742
4743 void
4745 {
4746 testcase("batch delegate consent");
4747
4748 using namespace test::jtx;
4749 using namespace std::literals;
4750
4751 // Delegate consent bypass.
4752 //
4753 // Alice delegates Payment to Bob. A Batch carries an inner Payment with
4754 // Delegate=Bob, but Bob never signs the batch (and, because the inner
4755 // Account == the outer Account, no BatchSigners are required at all).
4756 // Batch waives the per-inner signature check, and preflightSigValidated
4757 // derives required signers from sfAccount only -- it ignores sfDelegate
4758 // -- so Bob's delegated authority is exercised without Bob's consent.
4759 //
4760 // SECURE EXPECTATION: the delegate (Bob) must authorize the inner txn,
4761 // so the batch must be rejected (temBAD_SIGNER) when Bob does not sign.
4762 {
4763 Env env{*this, features};
4764
4765 auto const alice = Account("alice");
4766 auto const bob = Account("bob");
4767 auto const mallory = Account("mallory");
4768 env.fund(XRP(10000), alice, bob, mallory);
4769 env.close();
4770
4771 env(delegate::set(alice, bob, {"Payment"}));
4772 env.close();
4773
4774 auto const preMallory = env.balance(mallory);
4775
4776 auto const batchFee = batch::calcBatchFee(env, 0, 2);
4777 auto const seq = env.seq(alice);
4778
4779 auto inner = batch::Inner(pay(alice, mallory, XRP(1000)), seq + 1);
4780 inner[jss::Delegate] = bob.human();
4781
4782 // Bob is NOT among the batch signers; he never consents.
4783 env(batch::outer(alice, seq, batchFee, tfAllOrNothing),
4784 inner,
4785 batch::Inner(pay(alice, mallory, XRP(1)), seq + 2),
4787 env.close();
4788
4789 // No funds should have moved to Mallory.
4790 BEAST_EXPECT(env.balance(mallory) == preMallory);
4791 }
4792
4793 // Self-grant attribution forgery.
4794 //
4795 // With no pre-existing delegation, Alice places DelegateSet(authorize
4796 // Bob) and a Delegate=Bob action in the SAME batch. The second inner
4797 // reads the delegation created by the first from the batch's running
4798 // view, and Bob never signs -- manufacturing on-chain attribution of
4799 // the action to Bob without his consent.
4800 //
4801 // SECURE EXPECTATION: rejected (temBAD_SIGNER) -- Bob must consent, and
4802 // a grant cannot be created and exercised within the same atomic batch.
4803 {
4804 Env env{*this, features};
4805
4806 auto const alice = Account("alice");
4807 auto const bob = Account("bob");
4808 auto const mallory = Account("mallory");
4809 env.fund(XRP(10000), alice, bob, mallory);
4810 env.close();
4811
4812 auto const preMallory = env.balance(mallory);
4813
4814 auto const batchFee = batch::calcBatchFee(env, 0, 2);
4815 auto const seq = env.seq(alice);
4816
4817 auto inner = batch::Inner(pay(alice, mallory, XRP(1000)), seq + 2);
4818 inner[jss::Delegate] = bob.human();
4819
4820 env(batch::outer(alice, seq, batchFee, tfAllOrNothing),
4821 batch::Inner(delegate::set(alice, bob, {"Payment"}), seq + 1),
4822 inner,
4824 env.close();
4825
4826 BEAST_EXPECT(env.balance(mallory) == preMallory);
4827 }
4828
4829 // Legitimate counterpart: the same atomic grant-and-use is allowed when
4830 // the delegate co-signs the batch -- consent is present, so it succeeds.
4831 {
4832 Env env{*this, features};
4833
4834 auto const alice = Account("alice");
4835 auto const bob = Account("bob");
4836 auto const mallory = Account("mallory");
4837 env.fund(XRP(10000), alice, bob, mallory);
4838 env.close();
4839
4840 auto const preMallory = env.balance(mallory);
4841
4842 auto const batchFee = batch::calcBatchFee(env, 1, 2);
4843 auto const seq = env.seq(alice);
4844
4845 auto inner = batch::Inner(pay(alice, mallory, XRP(1000)), seq + 2);
4846 inner[jss::Delegate] = bob.human();
4847
4848 env(batch::outer(alice, seq, batchFee, tfAllOrNothing),
4849 batch::Inner(delegate::set(alice, bob, {"Payment"}), seq + 1),
4850 inner,
4851 batch::Sig(bob),
4852 Ter(tesSUCCESS));
4853 env.close();
4854
4855 BEAST_EXPECT(env.balance(mallory) == preMallory + XRP(1000));
4856 }
4857
4858 // Multi-account: a delegated inner from a non-outer account also
4859 // requires the delegate's signature (not the account holder's).
4860 {
4861 Env env{*this, features};
4862
4863 auto const alice = Account("alice");
4864 auto const bob = Account("bob");
4865 auto const carol = Account("carol");
4866 env.fund(XRP(10000), alice, bob, carol);
4867 env.close();
4868
4869 env(delegate::set(bob, carol, {"Payment"}));
4870 env.close();
4871
4872 auto const preAlice = env.balance(alice);
4873
4874 auto const batchFee = batch::calcBatchFee(env, 1, 2);
4875 auto const aliceSeq = env.seq(alice);
4876 auto const bobSeq = env.seq(bob);
4877
4878 auto inner = batch::Inner(pay(bob, alice, XRP(1)), bobSeq);
4879 inner[jss::Delegate] = carol.human();
4880
4881 // Carol (the delegate) does not sign.
4882 env(batch::outer(alice, aliceSeq, batchFee, tfAllOrNothing),
4883 inner,
4884 batch::Inner(pay(alice, bob, XRP(2)), aliceSeq + 1),
4886 env.close();
4887
4888 BEAST_EXPECT(env.balance(alice) == preAlice);
4889 }
4890
4891 // Wrong signer: a batch signature from someone other than the named
4892 // delegate does not satisfy the requirement.
4893 {
4894 Env env{*this, features};
4895
4896 auto const alice = Account("alice");
4897 auto const bob = Account("bob");
4898 auto const carol = Account("carol");
4899 auto const mallory = Account("mallory");
4900 env.fund(XRP(10000), alice, bob, carol, mallory);
4901 env.close();
4902
4903 env(delegate::set(alice, bob, {"Payment"}));
4904 env.close();
4905
4906 auto const preMallory = env.balance(mallory);
4907
4908 auto const batchFee = batch::calcBatchFee(env, 1, 2);
4909 auto const seq = env.seq(alice);
4910
4911 auto inner = batch::Inner(pay(alice, mallory, XRP(1000)), seq + 1);
4912 inner[jss::Delegate] = bob.human();
4913
4914 // Carol signs instead of the named delegate Bob.
4915 env(batch::outer(alice, seq, batchFee, tfAllOrNothing),
4916 inner,
4917 batch::Inner(pay(alice, mallory, XRP(1)), seq + 2),
4918 batch::Sig(carol),
4920 env.close();
4921
4922 BEAST_EXPECT(env.balance(mallory) == preMallory);
4923 }
4924
4925 // Delegate is the outer account: the outer signature already provides
4926 // the delegate's consent, so no BatchSigners are required.
4927 {
4928 Env env{*this, features};
4929
4930 auto const alice = Account("alice");
4931 auto const bob = Account("bob");
4932 env.fund(XRP(10000), alice, bob);
4933 env.close();
4934
4935 // Bob delegates Payment to Alice, who is also the batch submitter.
4936 env(delegate::set(bob, alice, {"Payment"}));
4937 env.close();
4938
4939 auto const preBob = env.balance(bob);
4940
4941 auto const batchFee = batch::calcBatchFee(env, 0, 2);
4942 auto const aliceSeq = env.seq(alice);
4943 auto const bobSeq = env.seq(bob);
4944
4945 auto inner = batch::Inner(pay(bob, alice, XRP(1)), bobSeq);
4946 inner[jss::Delegate] = alice.human();
4947
4948 env(batch::outer(alice, aliceSeq, batchFee, tfAllOrNothing),
4949 inner,
4950 batch::Inner(pay(alice, bob, XRP(2)), aliceSeq + 1),
4951 Ter(tesSUCCESS));
4952 env.close();
4953
4954 // Net: Bob sends 1 to Alice, receives 2 from Alice.
4955 BEAST_EXPECT(env.balance(bob) == preBob + XRP(1));
4956 }
4957
4958 // Principal signs instead of the delegate. Bob delegates Payment to
4959 // Carol and an inner pays from Bob with Delegate=Carol, so Carol -- not
4960 // Bob -- is the required batch signer. Bob's own signature (the account
4961 // holder) does not satisfy the delegate-consent requirement.
4962 {
4963 Env env{*this, features};
4964
4965 auto const alice = Account("alice");
4966 auto const bob = Account("bob");
4967 auto const carol = Account("carol");
4968 env.fund(XRP(10000), alice, bob, carol);
4969 env.close();
4970
4971 env(delegate::set(bob, carol, {"Payment"}));
4972 env.close();
4973
4974 auto const preAlice = env.balance(alice);
4975
4976 auto const batchFee = batch::calcBatchFee(env, 1, 2);
4977 auto const aliceSeq = env.seq(alice);
4978 auto const bobSeq = env.seq(bob);
4979
4980 auto inner = batch::Inner(pay(bob, alice, XRP(1)), bobSeq);
4981 inner[jss::Delegate] = carol.human();
4982
4983 // Bob (the principal) signs in place of Carol (the delegate).
4984 env(batch::outer(alice, aliceSeq, batchFee, tfAllOrNothing),
4985 inner,
4986 batch::Inner(pay(alice, bob, XRP(2)), aliceSeq + 1),
4987 batch::Sig(bob),
4989 env.close();
4990
4991 BEAST_EXPECT(env.balance(alice) == preAlice);
4992 }
4993
4994 // Control for the revocation case below: identical setup, but inner 1
4995 // is a benign payment instead of a revocation. With Alice's delegation
4996 // intact, the delegated inner 2 succeeds -- proving the failure in the
4997 // revocation case is caused by the revocation, not the setup.
4998 {
4999 Env env{*this, features};
5000
5001 auto const alice = Account("alice");
5002 auto const bob = Account("bob");
5003 auto const mallory = Account("mallory");
5004 env.fund(XRP(10000), alice, bob, mallory);
5005 env.close();
5006
5007 env(delegate::set(bob, alice, {"Payment"}));
5008 env.close();
5009
5010 auto const preMallory = env.balance(mallory);
5011
5012 auto const batchFee = batch::calcBatchFee(env, 1, 2);
5013 auto const seq = env.seq(bob);
5014
5015 auto inner2 = batch::Inner(pay(bob, mallory, XRP(1000)), seq + 2);
5016 inner2[jss::Delegate] = alice.human();
5017
5018 // Inner 1 is Bob's own payment (no revocation); Alice co-signs for
5019 // the delegated inner 2.
5020 auto const [txIDs, batchID] = submitBatch(
5021 env,
5022 tesSUCCESS,
5023 batch::outer(bob, seq, batchFee, tfIndependent),
5024 batch::Inner(pay(bob, alice, XRP(1)), seq + 1),
5025 inner2,
5026 batch::Sig(alice));
5027 env.close();
5028
5029 std::vector<TestLedgerData> const testCases = {
5030 {.index = 0,
5031 .txType = "Batch",
5032 .result = "tesSUCCESS",
5033 .txHash = batchID,
5034 .batchID = std::nullopt},
5035 {.index = 1,
5036 .txType = "Payment",
5037 .result = "tesSUCCESS",
5038 .txHash = txIDs[0],
5039 .batchID = batchID},
5040 {.index = 2,
5041 .txType = "Payment",
5042 .result = "tesSUCCESS",
5043 .txHash = txIDs[1],
5044 .batchID = batchID},
5045 };
5046 validateClosedLedger(env, testCases);
5047
5048 // The delegated payment applied: Mallory received the funds.
5049 BEAST_EXPECT(env.balance(mallory) == preMallory + XRP(1000));
5050 }
5051
5052 // Revocation within the same batch. Bob grants Alice Payment permission
5053 // beforehand; an earlier inner revokes it before a later delegated
5054 // inner tries to use it. Alice must still co-sign (delegate consent is
5055 // derived from the static tx), but the revoked permission makes the
5056 // delegated inner fail at apply time -- a grant cannot be used after it
5057 // is removed earlier in the same batch.
5058 {
5059 Env env{*this, features};
5060
5061 auto const alice = Account("alice");
5062 auto const bob = Account("bob");
5063 auto const mallory = Account("mallory");
5064 env.fund(XRP(10000), alice, bob, mallory);
5065 env.close();
5066
5067 env(delegate::set(bob, alice, {"Payment"}));
5068 env.close();
5069
5070 auto const preMallory = env.balance(mallory);
5071
5072 auto const batchFee = batch::calcBatchFee(env, 1, 2);
5073 auto const seq = env.seq(bob);
5074
5075 // Inner 2: Alice acts as Bob's delegate, so Alice is the required
5076 // batch signer.
5077 auto inner2 = batch::Inner(pay(bob, mallory, XRP(1000)), seq + 2);
5078 inner2[jss::Delegate] = alice.human();
5079
5080 // tfIndependent: inner 1 (the revocation) applies; inner 2 then
5081 // fails for lack of permission without reverting inner 1. An empty
5082 // permission list deletes the Delegate object.
5083 auto const [txIDs, batchID] = submitBatch(
5084 env,
5085 tesSUCCESS,
5086 batch::outer(bob, seq, batchFee, tfIndependent),
5087 batch::Inner(delegate::set(bob, alice, {}), seq + 1),
5088 inner2,
5089 batch::Sig(alice));
5090 env.close();
5091
5092 std::vector<TestLedgerData> const testCases = {
5093 {.index = 0,
5094 .txType = "Batch",
5095 .result = "tesSUCCESS",
5096 .txHash = batchID,
5097 .batchID = std::nullopt},
5098 {.index = 1,
5099 .txType = "DelegateSet",
5100 .result = "tesSUCCESS",
5101 .txHash = txIDs[0],
5102 .batchID = batchID},
5103 // inner 2 fails: Alice's permission was revoked in inner 1.
5104 };
5105 validateClosedLedger(env, testCases);
5106
5107 // The delegated payment never applied.
5108 BEAST_EXPECT(env.balance(mallory) == preMallory);
5109 BEAST_EXPECT(env.rpc("tx", txIDs[1])[jss::result][jss::error] == "txnNotFound");
5110 }
5111 }
5112
5113 void
5115 {
5116 testcase("batch delegate");
5117
5118 using namespace test::jtx;
5119 using namespace std::literals;
5120
5121 // delegated non atomic inner
5122 {
5123 Env env{*this, features};
5124
5125 auto const alice = Account("alice");
5126 auto const bob = Account("bob");
5127 auto const gw = Account("gw");
5128 auto const usd = gw["USD"];
5129 env.fund(XRP(10000), alice, bob, gw);
5130 env.close();
5131
5132 env(delegate::set(alice, bob, {"Payment"}));
5133 env.close();
5134
5135 auto const preAlice = env.balance(alice);
5136 auto const preBob = env.balance(bob);
5137
5138 auto const batchFee = batch::calcBatchFee(env, 1, 2);
5139 auto const seq = env.seq(alice);
5140
5141 auto tx = batch::Inner(pay(alice, bob, XRP(1)), seq + 1);
5142 tx[jss::Delegate] = bob.human();
5143 // The delegate (Bob) authorizes the delegated inner, so Bob must
5144 // provide the batch signature.
5145 auto const [txIDs, batchID] = submitBatch(
5146 env,
5147 tesSUCCESS,
5148 batch::outer(alice, seq, batchFee, tfAllOrNothing),
5149 tx,
5150 batch::Inner(pay(alice, bob, XRP(2)), seq + 2),
5151 batch::Sig(bob));
5152 env.close();
5153
5154 std::vector<TestLedgerData> const testCases = {
5155 {.index = 0,
5156 .txType = "Batch",
5157 .result = "tesSUCCESS",
5158 .txHash = batchID,
5159 .batchID = std::nullopt},
5160 {.index = 1,
5161 .txType = "Payment",
5162 .result = "tesSUCCESS",
5163 .txHash = txIDs[0],
5164 .batchID = batchID},
5165 {.index = 2,
5166 .txType = "Payment",
5167 .result = "tesSUCCESS",
5168 .txHash = txIDs[1],
5169 .batchID = batchID},
5170 };
5171 validateClosedLedger(env, testCases);
5172
5173 // Alice consumes sequences (# of txns)
5174 BEAST_EXPECT(env.seq(alice) == seq + 3);
5175
5176 // Alice pays XRP & Fee; Bob receives XRP
5177 BEAST_EXPECT(env.balance(alice) == preAlice - XRP(3) - batchFee);
5178 BEAST_EXPECT(env.balance(bob) == preBob + XRP(3));
5179 }
5180
5181 // delegated atomic inner
5182 {
5183 Env env{*this, features};
5184
5185 auto const alice = Account("alice");
5186 auto const bob = Account("bob");
5187 auto const carol = Account("carol");
5188 auto const gw = Account("gw");
5189 auto const usd = gw["USD"];
5190 env.fund(XRP(10000), alice, bob, carol, gw);
5191 env.close();
5192
5193 env(delegate::set(bob, carol, {"Payment"}));
5194 env.close();
5195
5196 auto const preAlice = env.balance(alice);
5197 auto const preBob = env.balance(bob);
5198 auto const preCarol = env.balance(carol);
5199
5200 auto const batchFee = batch::calcBatchFee(env, 1, 2);
5201 auto const aliceSeq = env.seq(alice);
5202 auto const bobSeq = env.seq(bob);
5203
5204 auto tx = batch::Inner(pay(bob, alice, XRP(1)), bobSeq);
5205 tx[jss::Delegate] = carol.human();
5206 // Carol is the delegate authorizing the inner txn on Bob's behalf,
5207 // so Carol -- not Bob -- must provide the batch signature.
5208 auto const [txIDs, batchID] = submitBatch(
5209 env,
5210 tesSUCCESS,
5211 batch::outer(alice, aliceSeq, batchFee, tfAllOrNothing),
5212 tx,
5213 batch::Inner(pay(alice, bob, XRP(2)), aliceSeq + 1),
5214 batch::Sig(carol));
5215 env.close();
5216
5217 std::vector<TestLedgerData> const testCases = {
5218 {.index = 0,
5219 .txType = "Batch",
5220 .result = "tesSUCCESS",
5221 .txHash = batchID,
5222 .batchID = std::nullopt},
5223 {.index = 1,
5224 .txType = "Payment",
5225 .result = "tesSUCCESS",
5226 .txHash = txIDs[0],
5227 .batchID = batchID},
5228 {.index = 2,
5229 .txType = "Payment",
5230 .result = "tesSUCCESS",
5231 .txHash = txIDs[1],
5232 .batchID = batchID},
5233 };
5234 validateClosedLedger(env, testCases);
5235
5236 BEAST_EXPECT(env.seq(alice) == aliceSeq + 2);
5237 BEAST_EXPECT(env.seq(bob) == bobSeq + 1);
5238 BEAST_EXPECT(env.balance(alice) == preAlice - XRP(1) - batchFee);
5239 BEAST_EXPECT(env.balance(bob) == preBob + XRP(1));
5240 // NOTE: Carol would normally pay the fee for delegated txns, but
5241 // because the batch is atomic, the fee is paid by the batch
5242 BEAST_EXPECT(env.balance(carol) == preCarol);
5243 }
5244
5245 // delegated non atomic inner (AccountSet)
5246 // this also makes sure tfInnerBatchTxn won't block delegated AccountSet
5247 // with granular permission
5248 {
5249 Env env{*this, features};
5250
5251 auto const alice = Account("alice");
5252 auto const bob = Account("bob");
5253 auto const gw = Account("gw");
5254 auto const usd = gw["USD"];
5255 env.fund(XRP(10000), alice, bob, gw);
5256 env.close();
5257
5258 env(delegate::set(alice, bob, {"AccountDomainSet"}));
5259 env.close();
5260
5261 auto const preAlice = env.balance(alice);
5262 auto const preBob = env.balance(bob);
5263
5264 auto const batchFee = batch::calcBatchFee(env, 1, 2);
5265 auto const seq = env.seq(alice);
5266
5267 auto tx = batch::Inner(noop(alice), seq + 1);
5268 std::string const domain = "example.com";
5269 tx[sfDomain.jsonName] = strHex(domain);
5270 tx[jss::Delegate] = bob.human();
5271 // Bob is the delegate authorizing the inner AccountSet, so Bob must
5272 // provide the batch signature.
5273 auto const [txIDs, batchID] = submitBatch(
5274 env,
5275 tesSUCCESS,
5276 batch::outer(alice, seq, batchFee, tfAllOrNothing),
5277 tx,
5278 batch::Inner(pay(alice, bob, XRP(2)), seq + 2),
5279 batch::Sig(bob));
5280 env.close();
5281
5282 std::vector<TestLedgerData> const testCases = {
5283 {.index = 0,
5284 .txType = "Batch",
5285 .result = "tesSUCCESS",
5286 .txHash = batchID,
5287 .batchID = std::nullopt},
5288 {.index = 1,
5289 .txType = "AccountSet",
5290 .result = "tesSUCCESS",
5291 .txHash = txIDs[0],
5292 .batchID = batchID},
5293 {.index = 2,
5294 .txType = "Payment",
5295 .result = "tesSUCCESS",
5296 .txHash = txIDs[1],
5297 .batchID = batchID},
5298 };
5299 validateClosedLedger(env, testCases);
5300
5301 // Alice consumes sequences (# of txns)
5302 BEAST_EXPECT(env.seq(alice) == seq + 3);
5303
5304 // Alice pays XRP & Fee; Bob receives XRP
5305 BEAST_EXPECT(env.balance(alice) == preAlice - XRP(2) - batchFee);
5306 BEAST_EXPECT(env.balance(bob) == preBob + XRP(2));
5307 }
5308
5309 // delegated non atomic inner (MPTokenIssuanceSet)
5310 // this also makes sure tfInnerBatchTxn won't block delegated
5311 // MPTokenIssuanceSet with granular permission
5312 {
5313 Env env{*this, features};
5314 Account const alice{"alice"};
5315 Account const bob{"bob"};
5316 env.fund(XRP(100000), alice, bob);
5317 env.close();
5318
5319 auto const mptID = makeMptID(env.seq(alice), alice);
5320 MPTTester mpt(env, alice, {.fund = false});
5321 env.close();
5322 mpt.create({.flags = tfMPTCanLock});
5323 env.close();
5324
5325 // alice gives granular permission to bob of MPTokenIssuanceLock
5326 env(delegate::set(alice, bob, {"MPTokenIssuanceLock", "MPTokenIssuanceUnlock"}));
5327 env.close();
5328
5329 auto const seq = env.seq(alice);
5330 auto const batchFee = batch::calcBatchFee(env, 1, 2);
5331
5332 json::Value jv1;
5333 jv1[sfTransactionType] = jss::MPTokenIssuanceSet;
5334 jv1[sfAccount] = alice.human();
5335 jv1[sfDelegate] = bob.human();
5336 jv1[sfSequence] = seq + 1;
5337 jv1[sfMPTokenIssuanceID] = to_string(mptID);
5338 jv1[sfFlags] = tfMPTLock;
5339
5340 json::Value jv2;
5341 jv2[sfTransactionType] = jss::MPTokenIssuanceSet;
5342 jv2[sfAccount] = alice.human();
5343 jv2[sfDelegate] = bob.human();
5344 jv2[sfSequence] = seq + 2;
5345 jv2[sfMPTokenIssuanceID] = to_string(mptID);
5346 jv2[sfFlags] = tfMPTUnlock;
5347
5348 // Both inners are delegated to Bob, so Bob must provide the batch
5349 // signature (one signer covers both).
5350 auto const [txIDs, batchID] = submitBatch(
5351 env,
5352 tesSUCCESS,
5353 batch::outer(alice, seq, batchFee, tfAllOrNothing),
5354 batch::Inner(jv1, seq + 1),
5355 batch::Inner(jv2, seq + 2),
5356 batch::Sig(bob));
5357 env.close();
5358
5359 std::vector<TestLedgerData> const testCases = {
5360 {.index = 0,
5361 .txType = "Batch",
5362 .result = "tesSUCCESS",
5363 .txHash = batchID,
5364 .batchID = std::nullopt},
5365 {.index = 1,
5366 .txType = "MPTokenIssuanceSet",
5367 .result = "tesSUCCESS",
5368 .txHash = txIDs[0],
5369 .batchID = batchID},
5370 {.index = 2,
5371 .txType = "MPTokenIssuanceSet",
5372 .result = "tesSUCCESS",
5373 .txHash = txIDs[1],
5374 .batchID = batchID},
5375 };
5376 validateClosedLedger(env, testCases);
5377 }
5378
5379 // delegated non atomic inner (TrustSet)
5380 // this also makes sure tfInnerBatchTxn won't block delegated TrustSet
5381 // with granular permission
5382 {
5383 Env env{*this, features};
5384 Account const gw{"gw"};
5385 Account const alice{"alice"};
5386 Account const bob{"bob"};
5387 env.fund(XRP(10000), gw, alice, bob);
5388 env(fset(gw, asfRequireAuth));
5389 env.close();
5390 env(trust(alice, gw["USD"](50)));
5391 env.close();
5392
5393 env(delegate::set(gw, bob, {"TrustlineAuthorize", "TrustlineFreeze"}));
5394 env.close();
5395
5396 auto const seq = env.seq(gw);
5397 auto const batchFee = batch::calcBatchFee(env, 1, 2);
5398
5399 auto jv1 = trust(gw, gw["USD"](0), alice, tfSetfAuth);
5400 jv1[sfDelegate] = bob.human();
5401 auto jv2 = trust(gw, gw["USD"](0), alice, tfSetFreeze);
5402 jv2[sfDelegate] = bob.human();
5403
5404 // Both inners are delegated to Bob, so Bob must provide the batch
5405 // signature.
5406 auto const [txIDs, batchID] = submitBatch(
5407 env,
5408 tesSUCCESS,
5409 batch::outer(gw, seq, batchFee, tfAllOrNothing),
5410 batch::Inner(jv1, seq + 1),
5411 batch::Inner(jv2, seq + 2),
5412 batch::Sig(bob));
5413 env.close();
5414
5415 std::vector<TestLedgerData> const testCases = {
5416 {.index = 0,
5417 .txType = "Batch",
5418 .result = "tesSUCCESS",
5419 .txHash = batchID,
5420 .batchID = std::nullopt},
5421 {.index = 1,
5422 .txType = "TrustSet",
5423 .result = "tesSUCCESS",
5424 .txHash = txIDs[0],
5425 .batchID = batchID},
5426 {.index = 2,
5427 .txType = "TrustSet",
5428 .result = "tesSUCCESS",
5429 .txHash = txIDs[1],
5430 .batchID = batchID},
5431 };
5432 validateClosedLedger(env, testCases);
5433 }
5434
5435 // inner transaction not authorized by the delegating account.
5436 {
5437 Env env{*this, features};
5438 Account const gw{"gw"};
5439 Account const alice{"alice"};
5440 Account const bob{"bob"};
5441 env.fund(XRP(10000), gw, alice, bob);
5442 env(fset(gw, asfRequireAuth));
5443 env.close();
5444 env(trust(alice, gw["USD"](50)));
5445 env.close();
5446
5447 env(delegate::set(gw, bob, {"TrustlineAuthorize", "TrustlineFreeze"}));
5448 env.close();
5449
5450 auto const seq = env.seq(gw);
5451 auto const batchFee = batch::calcBatchFee(env, 1, 2);
5452
5453 auto jv1 = trust(gw, gw["USD"](0), alice, tfSetFreeze);
5454 jv1[sfDelegate] = bob.human();
5455 auto jv2 = trust(gw, gw["USD"](0), alice, tfClearFreeze);
5456 jv2[sfDelegate] = bob.human();
5457
5458 // Both inners are delegated to Bob, so Bob must provide the batch
5459 // signature; jv2 still fails preclaim for lack of permission.
5460 auto const [txIDs, batchID] = submitBatch(
5461 env,
5462 tesSUCCESS,
5463 batch::outer(gw, seq, batchFee, tfIndependent),
5464 batch::Inner(jv1, seq + 1),
5465 // terNO_DELEGATE_PERMISSION: not authorized to clear freeze
5466 batch::Inner(jv2, seq + 2),
5467 batch::Sig(bob));
5468 env.close();
5469
5470 std::vector<TestLedgerData> const testCases = {
5471 {.index = 0,
5472 .txType = "Batch",
5473 .result = "tesSUCCESS",
5474 .txHash = batchID,
5475 .batchID = std::nullopt},
5476 {.index = 1,
5477 .txType = "TrustSet",
5478 .result = "tesSUCCESS",
5479 .txHash = txIDs[0],
5480 .batchID = batchID},
5481 // jv2 fails with terNO_DELEGATE_PERMISSION.
5482 };
5483 validateClosedLedger(env, testCases);
5484
5485 // verify jv2 is not present in the closed ledger.
5486 BEAST_EXPECT(env.rpc("tx", txIDs[1])[jss::result][jss::error] == "txnNotFound");
5487 }
5488 }
5489
5490 void
5492 {
5493 // Verifying that the RPC response from submit includes
5494 // the account_sequence_available, account_sequence_next,
5495 // open_ledger_cost and validated_ledger_index fields.
5496 testcase("Validate RPC response");
5497
5498 using namespace jtx;
5499 Env env(*this, features);
5500 Account const alice("alice");
5501 Account const bob("bob");
5502 env.fund(XRP(10000), alice, bob);
5503 env.close();
5504
5505 // tes
5506 {
5507 auto const baseFee = env.current()->fees().base;
5508 auto const aliceSeq = env.seq(alice);
5509 auto jtx = env.jt(pay(alice, bob, XRP(1)));
5510
5511 Serializer s;
5512 jtx.stx->add(s);
5513 auto const jr = env.rpc("submit", strHex(s.slice()))[jss::result];
5514 env.close();
5515
5516 BEAST_EXPECT(jr.isMember(jss::account_sequence_available));
5517 BEAST_EXPECT(jr[jss::account_sequence_available].asUInt() == aliceSeq + 1);
5518 BEAST_EXPECT(jr.isMember(jss::account_sequence_next));
5519 BEAST_EXPECT(jr[jss::account_sequence_next].asUInt() == aliceSeq + 1);
5520 BEAST_EXPECT(jr.isMember(jss::open_ledger_cost));
5521 BEAST_EXPECT(jr[jss::open_ledger_cost] == to_string(baseFee));
5522 BEAST_EXPECT(jr.isMember(jss::validated_ledger_index));
5523 }
5524
5525 // tec failure
5526 {
5527 auto const baseFee = env.current()->fees().base;
5528 auto const aliceSeq = env.seq(alice);
5529 env(fset(bob, asfRequireDest));
5530 auto jtx = env.jt(pay(alice, bob, XRP(1)), Seq(aliceSeq));
5531
5532 Serializer s;
5533 jtx.stx->add(s);
5534 auto const jr = env.rpc("submit", strHex(s.slice()))[jss::result];
5535 env.close();
5536
5537 BEAST_EXPECT(jr.isMember(jss::account_sequence_available));
5538 BEAST_EXPECT(jr[jss::account_sequence_available].asUInt() == aliceSeq + 1);
5539 BEAST_EXPECT(jr.isMember(jss::account_sequence_next));
5540 BEAST_EXPECT(jr[jss::account_sequence_next].asUInt() == aliceSeq + 1);
5541 BEAST_EXPECT(jr.isMember(jss::open_ledger_cost));
5542 BEAST_EXPECT(jr[jss::open_ledger_cost] == to_string(baseFee));
5543 BEAST_EXPECT(jr.isMember(jss::validated_ledger_index));
5544 }
5545
5546 // tem failure
5547 {
5548 auto const baseFee = env.current()->fees().base;
5549 auto const aliceSeq = env.seq(alice);
5550 auto jtx = env.jt(pay(alice, bob, XRP(1)), Seq(aliceSeq + 1));
5551
5552 Serializer s;
5553 jtx.stx->add(s);
5554 auto const jr = env.rpc("submit", strHex(s.slice()))[jss::result];
5555 env.close();
5556
5557 BEAST_EXPECT(jr.isMember(jss::account_sequence_available));
5558 BEAST_EXPECT(jr[jss::account_sequence_available].asUInt() == aliceSeq);
5559 BEAST_EXPECT(jr.isMember(jss::account_sequence_next));
5560 BEAST_EXPECT(jr[jss::account_sequence_next].asUInt() == aliceSeq);
5561 BEAST_EXPECT(jr.isMember(jss::open_ledger_cost));
5562 BEAST_EXPECT(jr[jss::open_ledger_cost] == to_string(baseFee));
5563 BEAST_EXPECT(jr.isMember(jss::validated_ledger_index));
5564 }
5565 }
5566
5567 void
5569 {
5570 using namespace jtx;
5571 Env env(*this, features);
5572 Account const alice("alice");
5573 Account const bob("bob");
5574 Account const carol("carol");
5575 env.fund(XRP(10000), alice, bob, carol);
5576 env.close();
5577
5578 auto getBaseFee = [&](JTx const& jtx) -> XRPAmount {
5579 Serializer s;
5580 jtx.stx->add(s);
5581 return Batch::calculateBaseFee(*env.current(), *jtx.stx);
5582 };
5583
5584 // bad: a batch may not contain a batch - the nested inner fails STTx
5585 // construction, so the transaction cannot be built.
5586 {
5587 auto const seq = env.seq(alice);
5588 XRPAmount const batchFee = batch::calcBatchFee(env, 0, 2);
5589 auto jtx = env.jt(
5590 batch::outer(alice, seq, batchFee, tfAllOrNothing),
5591 batch::Inner(batch::outer(alice, seq, batchFee, tfAllOrNothing), seq),
5592 batch::Inner(pay(alice, bob, XRP(1)), seq + 2));
5593 BEAST_EXPECT(jtx.stx == nullptr);
5594 }
5595
5596 // bad: an oversized batch (more than kMaxBatchTxCount inners) fails
5597 // STTx construction, so it cannot be built.
5598 {
5599 auto const seq = env.seq(alice);
5600 XRPAmount const batchFee = batch::calcBatchFee(env, 0, 2);
5601
5602 auto jtx = env.jt(
5603 batch::outer(alice, seq, batchFee, tfAllOrNothing),
5604 batch::Inner(pay(alice, bob, XRP(1)), seq + 1),
5605 batch::Inner(pay(alice, bob, XRP(1)), seq + 2),
5606 batch::Inner(pay(alice, bob, XRP(1)), seq + 3),
5607 batch::Inner(pay(alice, bob, XRP(1)), seq + 4),
5608 batch::Inner(pay(alice, bob, XRP(1)), seq + 5),
5609 batch::Inner(pay(alice, bob, XRP(1)), seq + 6),
5610 batch::Inner(pay(alice, bob, XRP(1)), seq + 7),
5611 batch::Inner(pay(alice, bob, XRP(1)), seq + 8),
5612 batch::Inner(pay(alice, bob, XRP(1)), seq + 9));
5613
5614 BEAST_EXPECT(jtx.stx == nullptr);
5615 }
5616
5617 // bad: Signers array exceeds max entries.
5618 {
5619 auto const seq = env.seq(alice);
5620 XRPAmount const batchFee = batch::calcBatchFee(env, 0, 2);
5621
5622 auto jtx = env.jt(
5623 batch::outer(alice, seq, batchFee, tfAllOrNothing),
5624 batch::Inner(pay(alice, bob, XRP(10)), seq + 1),
5625 batch::Inner(pay(alice, bob, XRP(5)), seq + 2),
5627 // Failure paths fall back to the ledger base fee.
5628 XRPAmount const txBaseFee = getBaseFee(jtx);
5629 BEAST_EXPECT(txBaseFee == env.current()->fees().base);
5630 }
5631
5632 // good:
5633 {
5634 auto const seq = env.seq(alice);
5635 XRPAmount const batchFee = batch::calcBatchFee(env, 0, 2);
5636 auto jtx = env.jt(
5637 batch::outer(alice, seq, batchFee, tfAllOrNothing),
5638 batch::Inner(pay(alice, bob, XRP(1)), seq + 1),
5639 batch::Inner(pay(bob, alice, XRP(2)), seq + 2));
5640 XRPAmount const txBaseFee = getBaseFee(jtx);
5641 BEAST_EXPECT(txBaseFee == batchFee);
5642 }
5643 }
5644
5645 void
5647 {
5648 testcase("standalone tx with tfInnerBatchTxn rejected");
5649
5650 using namespace test::jtx;
5651 using namespace std::literals;
5652
5653 // A standalone Payment with tfInnerBatchTxn must be rejected.
5654 // Without proper guards this would bypass signature verification
5655 // in preflight2.
5656 {
5657 Env env{*this, features};
5658
5659 auto const alice = Account("alice");
5660 auto const bob = Account("bob");
5661 env.fund(XRP(10000), alice, bob);
5662 env.close();
5663
5664 // Submit a normal Payment with tfInnerBatchTxn flag.
5665 // preflight1 must reject with temINVALID_INNER_BATCH because
5666 // the flag is set but no parentBatchId exists.
5667 env(pay(alice, bob, XRP(1)), Txflags(tfInnerBatchTxn), Ter(telENV_RPC_FAILED));
5668 env.close();
5669
5670 // Verify via direct apply path (bypassing RPC layer)
5671 env.app().getOpenLedger().modify([&](OpenView& view, beast::Journal j) {
5672 // Construct a Payment STTx with tfInnerBatchTxn,
5673 // empty signing pub key, and no signature — mimicking
5674 // what an attacker would send to skip sig verification.
5675 STTx const stx = STTx(ttPAYMENT, [&](auto& obj) {
5676 obj.setAccountID(sfAccount, alice.id());
5677 obj.setAccountID(sfDestination, bob.id());
5678 obj.setFieldAmount(sfAmount, XRP(1));
5679 obj.setFieldAmount(sfFee, XRP(0));
5680 obj.setFieldU32(sfSequence, env.seq(alice));
5681 obj.setFieldU32(sfFlags, tfInnerBatchTxn);
5682 });
5683
5684 auto const result = xrpl::apply(env.app(), view, stx, TapNone, j);
5685 // Must NOT be applied — signature was never checked
5686 BEAST_EXPECT(!result.applied);
5687 return false;
5688 });
5689 }
5690 }
5691
5692 void
5694 {
5695 testcase("outer binding");
5696
5697 using namespace test::jtx;
5698
5699 // Signatures captured from one outer account cannot be replayed
5700 // under a different outer account.
5701 {
5702 Env env{*this, features};
5703
5704 auto const alice = Account("alice");
5705 auto const bob = Account("bob");
5706 auto const carol = Account("carol");
5707 auto const eve = Account("eve");
5708 env.fund(XRP(10000), alice, bob, carol, eve);
5709 env.close();
5710
5711 auto const preCarol = env.balance(carol);
5712 auto const bobSeq = env.seq(bob);
5713 auto const carolSeq = env.seq(carol);
5714
5715 auto const aliceSeq = env.seq(alice);
5716 auto const batchFee1 = batch::calcBatchFee(env, 2, 2);
5717 auto jt1 = env.jt(
5718 batch::outer(alice, aliceSeq, batchFee1, tfOnlyOne),
5719 batch::Inner(pay(bob, alice, XRP(100)), bobSeq),
5720 batch::Inner(pay(carol, alice, XRP(50)), carolSeq),
5721 batch::Sig(bob, carol));
5722
5723 auto const capturedSigners = jt1.jv[sfBatchSigners.jsonName];
5724
5725 env(jt1, Ter(tesSUCCESS));
5726 env.close();
5727 BEAST_EXPECT(env.seq(bob) == bobSeq + 1);
5728 BEAST_EXPECT(env.seq(carol) == carolSeq);
5729
5730 auto const batchFee2 = batch::calcBatchFee(env, 2, 2);
5731 auto jt2 = env.jtnofill(
5732 batch::outer(eve, env.seq(eve), batchFee2, tfOnlyOne),
5733 batch::Inner(pay(bob, alice, XRP(100)), bobSeq),
5734 batch::Inner(pay(carol, alice, XRP(50)), carolSeq));
5735
5736 jt2.jv[sfBatchSigners.jsonName] = capturedSigners;
5737 env(jt2.jv, Ter(telENV_RPC_FAILED));
5738 env.close();
5739
5740 BEAST_EXPECT(env.seq(carol) == carolSeq);
5741 BEAST_EXPECT(env.balance(carol) == preCarol);
5742 }
5743
5744 // Signatures are bound to the outer sequence; replaying them
5745 // at a higher sequence must fail.
5746 {
5747 Env env{*this, features};
5748
5749 auto const alice = Account("alice");
5750 auto const bob = Account("bob");
5751 auto const carol = Account("carol");
5752 env.fund(XRP(10000), alice, bob, carol);
5753 env.close();
5754
5755 auto const preCarol = env.balance(carol);
5756 auto const bobSeq = env.seq(bob);
5757 auto const carolSeq = env.seq(carol);
5758
5759 auto const aliceSeq1 = env.seq(alice);
5760 auto const batchFee1 = batch::calcBatchFee(env, 2, 2);
5761 auto jt1 = env.jt(
5762 batch::outer(alice, aliceSeq1, batchFee1, tfOnlyOne),
5763 batch::Inner(pay(bob, alice, XRP(500)), bobSeq),
5764 batch::Inner(pay(carol, alice, XRP(500)), carolSeq),
5765 batch::Sig(bob, carol));
5766
5767 auto const capturedSigners = jt1.jv[sfBatchSigners.jsonName];
5768
5769 env(jt1, Ter(tesSUCCESS));
5770 env.close();
5771 BEAST_EXPECT(env.seq(bob) == bobSeq + 1);
5772 BEAST_EXPECT(env.seq(carol) == carolSeq);
5773
5774 auto const batchFee2 = batch::calcBatchFee(env, 2, 2);
5775 auto jt2 = env.jtnofill(
5776 batch::outer(alice, env.seq(alice), batchFee2, tfOnlyOne),
5777 batch::Inner(pay(bob, alice, XRP(500)), bobSeq),
5778 batch::Inner(pay(carol, alice, XRP(500)), carolSeq));
5779
5780 jt2.jv[sfBatchSigners.jsonName] = capturedSigners;
5781 env(jt2.jv, Ter(telENV_RPC_FAILED));
5782 env.close();
5783
5784 BEAST_EXPECT(env.balance(carol) == preCarol);
5785 BEAST_EXPECT(env.seq(carol) == carolSeq);
5786 }
5787
5788 // Multi-signed batch signer entries are bound to their account;
5789 // reusing inner signatures under a different batch signer must fail.
5790 {
5791 Env env{*this, features};
5792
5793 auto const alice = Account("alice");
5794 auto const bob = Account("bob");
5795 auto const carol = Account("carol");
5796 auto const dave = Account("dave");
5797 auto const elsa = Account("elsa");
5798 env.fund(XRP(10000), alice, bob, carol, dave, elsa);
5799 env.close();
5800
5801 env(signers(bob, 2, {{dave, 1}, {elsa, 1}}));
5802 env.close();
5803 env(signers(carol, 2, {{dave, 1}, {elsa, 1}}));
5804 env.close();
5805
5806 auto const seq = env.seq(alice);
5807 auto const batchFee = batch::calcBatchFee(env, 3, 2);
5808 auto jt1 = env.jt(
5809 batch::outer(alice, seq, batchFee, tfAllOrNothing),
5810 batch::Inner(pay(alice, bob, XRP(10)), seq + 1),
5811 batch::Inner(pay(bob, alice, XRP(5)), env.seq(bob)),
5812 batch::Msig(bob, {dave, elsa}),
5813 Ter(tesSUCCESS));
5814
5815 auto const bobSignerEntry = jt1.jv[sfBatchSigners.jsonName][0u];
5816
5817 env(jt1, Ter(tesSUCCESS));
5818 env.close();
5819
5820 auto const seq2 = env.seq(alice);
5821 auto const batchFee2 = batch::calcBatchFee(env, 3, 2);
5822 auto jt2 = env.jtnofill(
5823 batch::outer(alice, seq2, batchFee2, tfAllOrNothing),
5824 batch::Inner(pay(alice, carol, XRP(10)), seq2 + 1),
5825 batch::Inner(pay(carol, alice, XRP(5)), env.seq(carol)));
5826
5827 json::Value carolSigner;
5828 carolSigner[sfBatchSigner.jsonName][jss::Account] = carol.human();
5829 carolSigner[sfBatchSigner.jsonName][jss::SigningPubKey] = "";
5830 carolSigner[sfBatchSigner.jsonName][sfSigners.jsonName] =
5831 bobSignerEntry[sfBatchSigner.jsonName][sfSigners.jsonName];
5832
5833 jt2.jv[sfBatchSigners.jsonName][0u] = carolSigner;
5834 env(jt2.jv, Ter(telENV_RPC_FAILED));
5835 env.close();
5836 }
5837 }
5838
5839 void
5841 {
5842 testcase("unsorted batch signers");
5843
5844 using namespace test::jtx;
5845
5846 Env env{*this, features};
5847
5848 auto const alice = Account("alice");
5849 auto const bob = Account("bob");
5850 auto const carol = Account("carol");
5851 env.fund(XRP(10000), alice, bob, carol);
5852 env.close();
5853
5854 auto const seq = env.seq(alice);
5855 auto const bobSeq = env.seq(bob);
5856 auto const carolSeq = env.seq(carol);
5857 auto const batchFee = batch::calcBatchFee(env, 2, 2);
5858
5859 auto jt = env.jt(
5860 batch::outer(alice, seq, batchFee, tfAllOrNothing),
5861 batch::Inner(pay(bob, alice, XRP(10)), bobSeq),
5862 batch::Inner(pay(carol, alice, XRP(5)), carolSeq),
5863 batch::Sig(bob, carol));
5864
5865 auto const s0 = jt.jv[sfBatchSigners.jsonName][0u];
5866 auto const s1 = jt.jv[sfBatchSigners.jsonName][1u];
5867 jt.jv[sfBatchSigners.jsonName][0u] = s1;
5868 jt.jv[sfBatchSigners.jsonName][1u] = s0;
5869
5870 env(jt.jv, Ter(temBAD_SIGNER));
5871 env.close();
5872 }
5873
5874 void
5876 {
5877 testcase("batch signature caching");
5878
5879 using namespace test::jtx;
5880
5881 // Mirrors apply.cpp's file-local kSfSiggood (the standard signature-good
5882 // cache); batch signer sigs are now verified and cached alongside the
5883 // outer signature in checkSign/checkValidity.
5885
5886 // Valid batch: alice (outer) + an inner from bob, who co-signs.
5887 auto buildValidBatch = [](Env& env) {
5888 auto const alice = Account("alice");
5889 auto const bob = Account("bob");
5890 auto const seq = env.seq(alice);
5891 auto const batchFee = batch::calcBatchFee(env, 1, 2);
5892 return env.jt(
5893 batch::outer(alice, seq, batchFee, tfAllOrNothing),
5894 batch::Inner(pay(alice, bob, XRP(1)), seq + 1),
5895 batch::Inner(pay(bob, alice, XRP(2)), env.seq(bob)),
5896 batch::Sig(bob));
5897 };
5898
5899 // WRITE: a valid batch records "good" on its tx id.
5900 {
5901 Env env{*this, features};
5902 env.fund(XRP(10000), Account("alice"), Account("bob"));
5903 env.close();
5904
5905 auto jt = buildValidBatch(env);
5906 auto const txid = jt.stx->getTransactionID();
5907
5908 BEAST_EXPECT(!any(env.app().getHashRouter().getFlags(txid) & kSfSiggood));
5909 env(jt, Ter(tesSUCCESS));
5910 BEAST_EXPECT(any(env.app().getHashRouter().getFlags(txid) & kSfSiggood));
5911 env.close();
5912 }
5913
5914 // READ: corrupt only a signer's signature (outer sig + signer key
5915 // untouched), so just the crypto would fail. Caught when uncached...
5916 {
5917 Env env{*this, features};
5918 env.fund(XRP(10000), Account("alice"), Account("bob"));
5919 env.close();
5920
5921 auto jt = buildValidBatch(env);
5922 jt.jv[sfBatchSigners.jsonName][0u][sfBatchSigner.jsonName][sfTxnSignature.jsonName] =
5923 "00";
5924 env(jt.jv, Ter(telENV_RPC_FAILED));
5925 env.close();
5926 }
5927 {
5928 // ...but a planted "good" skips the crypto, so it applies.
5929 Env env{*this, features};
5930 env.fund(XRP(10000), Account("alice"), Account("bob"));
5931 env.close();
5932
5933 auto jt = buildValidBatch(env);
5934 jt.jv[sfBatchSigners.jsonName][0u][sfBatchSigner.jsonName][sfTxnSignature.jsonName] =
5935 "00";
5936 auto const txid = STTx{parse(jt.jv)}.getTransactionID();
5937 env.app().getHashRouter().setFlags(txid, kSfSiggood);
5938 env(jt.jv, Ter(tesSUCCESS));
5939 env.close();
5940 }
5941 }
5942
5943 void
5945 {
5946 testEnable(features);
5947 testPreflight(features);
5948 testPreclaim(features);
5949 testBadRawTxn(features);
5950 testBadSequence(features);
5951 testBadOuterFee(features);
5952 testCalculateBaseFee(features);
5953 testAllOrNothing(features);
5954 testOnlyOne(features);
5955 testUntilFailure(features);
5956 testIndependent(features);
5957 testInnerSubmitRPC(features);
5958 testAccountActivation(features);
5959 testCheckAllSignatures(features);
5960 testAccountSet(features);
5961 testAccountDelete(features);
5962 testLendingAmendment(features);
5963 testLoan(features);
5964 testObjectCreateSequence(features);
5965 testObjectCreateTicket(features);
5966 testObjectCreate3rdParty(features);
5967 testTickets(features);
5968 testSequenceOpenLedger(features);
5969 testTicketsOpenLedger(features);
5970 testObjectsOpenLedger(features);
5971 testPseudoTxn(features);
5972 testOpenLedger(features);
5973 testBatchTxQueue(features);
5974 testBatchNetworkOps(features);
5975 testBatchDelegate(features);
5976 testBatchDelegateConsent(features);
5977 testValidateRPCResponse(features);
5978 testBatchCalculateBaseFee(features);
5980 testOuterBinding(features);
5981 testUnsortedBatchSigners(features);
5982 testBatchSigCache(features);
5983 }
5984
5985public:
5986 void
5987 run() override
5988 {
5989 using namespace test::jtx;
5990
5991 auto const sa = testableAmendments();
5992 testWithFeats(sa);
5993 }
5994};
5995
5997
5998} // namespace xrpl::test
T begin(T... args)
A generic endpoint for log messages.
Definition Journal.h:44
A testsuite class.
Definition suite.h:52
bool expect(Condition const &shouldBeTrue)
Evaluate a test condition.
Definition suite.h:235
void fail(String const &reason, char const *file, int line)
Record a failure.
Definition suite.h:554
TestcaseT testcase
Memberspace for declaring test cases.
Definition suite.h:155
Lightweight wrapper to tag static string.
Definition json_value.h:48
Represents a JSON value.
Definition json_value.h:117
static XRPAmount calculateBaseFee(ReadView const &view, STTx const &tx)
Definition Batch.cpp:161
bool setFlags(UInt256 const &key, HashRouterFlags flags)
Set the flags on a hash.
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(ModifyType const &f)
Modify the open ledger.
Writable ledger view that accumulates state and tx changes.
Definition OpenView.h:59
json::Value getJson(JsonOptions=JsonOptions::Values::None) const override
Definition STAmount.cpp:735
Holds the serialized result of parsing an input JSON object.
std::optional< STObject > object
The STObject if the parse was successful.
UInt256 getTransactionID() const
Definition STTx.h:262
json::Value getJson(JsonOptions options) const override
Definition STTx.cpp:351
static constexpr SeqProxy rawSequence(std::uint32_t v)
Factory function to return a sequence-based SeqProxy.
Definition SeqProxy.h:62
Slice slice() const noexcept
Definition Serializer.h:141
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:28
static NotTEC invokePreflight(PreflightContext const &ctx)
Definition Transactor.h:648
Metrics getMetrics(OpenView const &view) const
Returns fee metrics in reference fee level units.
Definition TxQ.cpp:1786
void testOpenLedger(FeatureBitset features)
void testLendingAmendment(FeatureBitset features)
void testBatchDelegateConsent(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 testStandaloneInnerBatchFlag(FeatureBitset features)
void testSequenceOpenLedger(FeatureBitset features)
void testEnable(FeatureBitset features)
void testPreflight(FeatureBitset features)
void testObjectCreate3rdParty(FeatureBitset features)
static UInt256 getCheckIndex(AccountID const &account, std::uint32_t uSequence)
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 testCheckAllSignatures(FeatureBitset features)
void testValidateRPCResponse(FeatureBitset features)
void testWithFeats(FeatureBitset features)
void testBatchCalculateBaseFee(FeatureBitset features)
void testLoan(FeatureBitset features)
void testOuterBinding(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)
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 testUnsortedBatchSigners(FeatureBitset features)
void testPseudoTxn(FeatureBitset features)
void testBadOuterFee(FeatureBitset features)
void testBatchSigCache(FeatureBitset features)
Immutable cryptographic account descriptor.
Definition jtx/Account.h:21
std::string const & human() const
Returns the human readable public key.
AccountID id() const
Returns the Account ID.
A transaction testing environment.
Definition Env.h:161
Application & app()
Definition Env.h:300
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:752
SLE::const_pointer le(Account const &account) const
Return an account root.
Definition Env.cpp:311
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:323
ParseFailureGuard getParseFailureGuard(bool b)
Gets an RAII guard to set and restore the parse failure flag.
Definition Env.h:596
std::uint32_t seq(Account const &account) const
Returns the next sequence number on account.
Definition Env.cpp:302
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:1058
JTx jt(JsonValue &&jv, FN const &... fN)
Create a JTx from parameters.
Definition Env.h:723
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:354
JTx jtnofill(JsonValue &&jv, FN const &... fN)
Create a JTx from parameters.
Definition Env.h:737
void memoize(Account const &account)
Associate AccountID with account.
Definition Env.cpp:174
beast::Journal const journal
Definition Env.h:204
std::shared_ptr< OpenView const > current() const
Returns the current ledger.
Definition Env.h:377
NetClock::time_point now()
Returns the current network time.
Definition Env.h:326
Set the fee on a JTx.
Definition fee.h:20
Test helper for creating, mutating, and asserting MPT and confidential MPT ledger state.
Definition mpt.h:512
Set a multisignature on a JTx.
Definition multisign.h:53
Set the regular signature on a JTx.
Definition sig.h:19
Set the expected result code for a JTx The test will fail if the code doesn't match.
Definition ter.h:18
Set the flags on a JTx.
Definition txflags.h:14
Adds an inner Batch transaction to a JTx and autofills it.
Definition batch.h:91
Sets a nested multi-signature for a Batch transaction on a JTx.
Definition batch.h:167
Sets the Batch transaction signers on a JTx.
Definition batch.h:142
Set a ticket sequence on a JTx.
Definition ticket.h:36
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:28
@ Object
object value (collection of name/value pairs).
Definition json_value.h:29
TER valid(STTx const &tx, ReadView const &view, AccountID const &src, beast::Journal j)
Keylet check(AccountID const &id, SeqProxy const &seq) noexcept
A Check.
Definition Indexes.cpp:360
Keylet loanBroker(AccountID const &owner, SeqProxy const &seq) noexcept
Definition Indexes.cpp:597
Keylet account(AccountID const &id) noexcept
AccountID root.
Definition Indexes.cpp:220
Keylet loan(UInt256 const &loanBrokerID, SeqProxy const &loanSeq) noexcept
Definition Indexes.cpp:603
void validateInnerTxn(Env &env, std::string const &batchID, std::string const &txHash, std::string const &txType, std::string const &result, std::source_location const &loc=std::source_location::current())
Expect an inner Batch transaction to be recorded with the given type and result, and with ParentBatch...
Definition batch.cpp:68
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:55
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:37
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 create(A const &account, A const &dest, STAmount const &sendMax)
Create a check.
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:16
json::Value coverDeposit(AccountID const &account, UInt256 const &brokerID, STAmount const &amount, uint32_t flags)
json::Value create(AccountID const &account, AccountID const &to, STAmount const &amount, NetClock::duration const &settleDelay, PublicKey const &pk, std::optional< NetClock::time_point > const &cancelAfter, std::optional< std::uint32_t > const &dstTag)
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:14
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:92
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:86
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:38
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:54
Use hash_* containers for keys that do not need a cryptographically secure hashing algorithm.
Definition algorithm.h:5
@ telINSUF_FEE_P
Definition TER.h:43
@ telENV_RPC_FAILED
Definition TER.h:54
@ telCAN_NOT_QUEUE
Definition TER.h:45
@ terPRE_SEQ
Definition TER.h:222
@ terQUEUED
Definition TER.h:226
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:108
constexpr FlagValue tfInnerBatchTxn
Definition TxFlags.h:44
constexpr std::size_t kMaxBatchSigners
The maximum number of batch signers.
Definition Protocol.h:460
@ SigBad
Signature is bad.
Definition apply.h:28
ApplyResult apply(ServiceRegistry &registry, OpenView &view, STTx const &tx, ApplyFlags flags, beast::Journal journal)
Apply a transaction to an OpenView.
Definition apply.cpp:181
std::string strHex(FwdIt begin, FwdIt end)
Definition strHex.h:13
std::pair< Validity, std::string > checkValidity(HashRouter &router, STTx const &tx, Rules const &rules)
Checks transaction signature and local checks.
Definition apply.cpp:62
constexpr TenthBips32 percentageToTenthBips(std::uint32_t percentage)
Definition Protocol.h:127
constexpr std::size_t kMaxBatchTxCount
The maximum number of transactions that can be in a batch.
Definition Protocol.h:455
constexpr HashRouterFlags kSfSiggood
Definition apply.cpp:30
@ tefBAD_QUORUM
Definition TER.h:175
@ tefMASTER_DISABLED
Definition TER.h:172
@ tefBAD_SIGNATURE
Definition TER.h:174
@ tefBAD_AUTH
Definition TER.h:164
@ tefNOT_MULTI_SIGNING
Definition TER.h:176
UInt256 TxID
A transaction identifier.
Definition Protocol.h:403
XRPAmount toDrops(FeeLevel< T > const &level, XRPAmount baseFee)
Definition TxQ.h:1004
TenthBips< std::uint32_t > TenthBips32
Definition Units.h:454
TenthBips< std::uint16_t > TenthBips16
Definition Units.h:453
std::string to_string(BaseUInt< Bits, Tag > const &a)
Definition base_uint.h:657
bool passesLocalChecks(STTx const &tx, std::string &)
Definition STTx.cpp:850
void finishMultiSigningData(AccountID const &signingID, Serializer &s)
Definition Sign.h:128
BaseUInt< 256 > UInt256
Definition base_uint.h:580
HashRouterFlags
Definition HashRouter.h:20
MPTID makeMptID(std::uint32_t const sequence, AccountID const &account)
Definition Indexes.cpp:206
@ TapNone
Definition ApplyView.h:28
@ TapBatch
Definition ApplyView.h:42
BaseUInt< 160, detail::AccountIDTag > AccountID
A 160-bit unsigned that uniquely identifies an account.
Definition AccountID.h:34
@ temBAD_REGKEY
Definition TER.h:86
@ temARRAY_TOO_LARGE
Definition TER.h:129
@ temBAD_FEE
Definition TER.h:80
@ temINVALID_FLAG
Definition TER.h:99
@ temMALFORMED
Definition TER.h:75
@ temARRAY_EMPTY
Definition TER.h:128
@ temSEQ_AND_TICKET
Definition TER.h:114
@ temDISABLED
Definition TER.h:102
@ temBAD_SIGNATURE
Definition TER.h:93
@ temINVALID_INNER_BATCH
Definition TER.h:131
@ temREDUNDANT
Definition TER.h:100
@ temBAD_SIGNER
Definition TER.h:103
Buffer sign(PublicKey const &pk, SecretKey const &sk, Slice const &message)
Generate a signature for a message.
TERSubset< CanCvtToTER > TER
Definition TER.h:654
@ tecNO_ENTRY
Definition TER.h:314
std::vector< unsigned char > Blob
Storage for linear binary data.
Definition Blob.h:11
void serializeBatch(Serializer &msg, AccountID const &outerAccount, std::uint32_t outerSeqValue, std::uint32_t const &flags, std::vector< UInt256 > const &txids)
bool isPseudoTx(STObject const &tx)
Check whether a transaction is a pseudo-transaction.
Definition STTx.cpp:889
@ tesSUCCESS
Definition TER.h:250
constexpr bool any(HashRouterFlags flags)
Definition HashRouter.h:74
T parse(T... args)
T push_back(T... args)
T reserve(T... args)
T size(T... args)
UInt256 key
Definition Keylet.h:21
static constexpr auto kMinLedgersToComputeSizeLimit
Definition Constants.h:129
static constexpr auto kRetrySequencePercent
Definition Constants.h:152
static constexpr auto kMaxLedgerCountsToStore
Definition Constants.h:121
static constexpr auto kMinimumQueueSize
Definition Constants.h:132
static constexpr auto kNormalConsensusIncreasePercent
Definition Constants.h:135
static constexpr auto kLedgersInQueue
Definition Constants.h:117
static constexpr auto kMinimumTxnInLedgerStandalone
Definition Constants.h:134
State information when preflighting a tx.
Definition Transactor.h:39
static constexpr auto kTransactionQueue
Definition Constants.h:66
std::optional< std::string > batchID
Execution context for applying a JSON transaction.
Definition JTx.h:27
std::shared_ptr< STTx const > stx
Definition JTx.h:37
json::Value jv
Definition JTx.h:28
Thrown when parse fails.
Definition utility.h:23
Set the sequence number on a JTx.
Definition seq.h:16