xrpld
Loading...
Searching...
No Matches
tests/libxrpl/tx/AccountSet.cpp
1#include <xrpl/protocol_autogen/transactions/AccountSet.h>
2
3#include <xrpl/basics/Slice.h>
4#include <xrpl/basics/base_uint.h>
5#include <xrpl/beast/utility/Zero.h>
6#include <xrpl/core/ServiceRegistry.h>
7#include <xrpl/ledger/helpers/DirectoryHelpers.h>
8#include <xrpl/protocol/Indexes.h>
9#include <xrpl/protocol/KeyType.h>
10#include <xrpl/protocol/LedgerFormats.h>
11#include <xrpl/protocol/Quality.h>
12#include <xrpl/protocol/Rate.h>
13#include <xrpl/protocol/SField.h>
14#include <xrpl/protocol/STArray.h>
15#include <xrpl/protocol/STObject.h>
16#include <xrpl/protocol/STTx.h>
17#include <xrpl/protocol/SecretKey.h>
18#include <xrpl/protocol/SeqProxy.h>
19#include <xrpl/protocol/TER.h>
20#include <xrpl/protocol/TxFlags.h>
21#include <xrpl/protocol_autogen/ledger_entries/AccountRoot.h>
22#include <xrpl/protocol_autogen/transactions/Payment.h>
23#include <xrpl/protocol_autogen/transactions/SetRegularKey.h>
24#include <xrpl/protocol_autogen/transactions/SignerListSet.h>
25#include <xrpl/protocol_autogen/transactions/TicketCreate.h>
26#include <xrpl/protocol_autogen/transactions/TrustSet.h>
27
28#include <gtest/gtest.h>
29#include <helpers/Account.h>
30#include <helpers/IOU.h>
31#include <helpers/TxTest.h>
32
33#include <algorithm>
34#include <cstddef>
35#include <cstdint>
36#include <initializer_list>
37#include <limits>
38#include <memory>
39#include <string>
40#include <string_view>
41#include <utility>
42#include <vector>
43
44namespace xrpl::test {
45
46TEST(AccountSet, NullAccountSet)
47{
48 TxTest env;
49
50 Account const alice("alice");
51 env.createAccount(alice, XRP(10));
52
53 auto& view = env.getOpenLedger();
54
55 // ask for the ledger entry - account root, to check its flags
56 auto sle = view.read(keylet::account(alice));
57
58 EXPECT_NE(sle, nullptr);
59 ledger_entries::AccountRoot const accountRoot(sle);
60 EXPECT_EQ(accountRoot.getFlags(), 0);
61}
62
63TEST(AccountSet, MostFlags)
64{
65 Account const alice("alice");
66
67 TxTest env;
68 env.createAccount(alice, XRP(10000));
69
70 // Give alice a regular key so she can legally set and clear
71 // her asfDisableMaster flag.
72 Account const aliceRegularKey{"aliceRegularKey", KeyType::Secp256k1};
73
74 env.createAccount(aliceRegularKey, XRP(10000));
75 env.close();
76
77 EXPECT_EQ(
78 env.submit(transactions::SetRegularKeyBuilder{alice}.setRegularKey(aliceRegularKey), alice)
79 .ter,
81 env.close();
82
83 auto testFlags = [&alice, &aliceRegularKey, &env](
85 std::uint32_t const origFlags = env.getAccountRoot(alice).getFlags();
86 for (std::uint32_t flag{1u}; flag < std::numeric_limits<std::uint32_t>::digits; ++flag)
87 {
88 if (flag == asfNoFreeze)
89 {
90 // The asfNoFreeze flag can't be cleared. It is tested
91 // elsewhere.
92 continue;
93 }
94 if (flag == asfAuthorizedNFTokenMinter)
95 {
96 // The asfAuthorizedNFTokenMinter flag requires the
97 // presence or absence of the sfNFTokenMinter field in
98 // the transaction. It is tested elsewhere.
99 continue;
100 }
101
102 if (flag == asfDisallowIncomingCheck || flag == asfDisallowIncomingPayChan ||
103 flag == asfDisallowIncomingNFTokenOffer || flag == asfDisallowIncomingTrustline)
104 {
105 // These flags are part of the DisallowIncoming amendment
106 // and are tested elsewhere
107 continue;
108 }
109 if (flag == asfAllowTrustLineClawback)
110 {
111 // The asfAllowTrustLineClawback flag can't be cleared. It
112 // is tested elsewhere.
113 continue;
114 }
115 if (flag == asfAllowTrustLineLocking)
116 {
117 // These flags are part of the AllowTokenLocking amendment
118 // and are tested elsewhere
119 continue;
120 }
121 if (std::ranges::find(goodFlags, flag) != goodFlags.end())
122 {
123 // Good flag
124 EXPECT_FALSE(env.getAccountRoot(alice).isFlag(asfToLsf(flag)));
125
126 EXPECT_EQ(
127 env.submit(transactions::AccountSetBuilder{alice}.setSetFlag(flag), alice).ter,
128 tesSUCCESS);
129 env.close();
130
131 EXPECT_TRUE(env.getAccountRoot(alice).isFlag(asfToLsf(flag)));
132
133 EXPECT_EQ(
134 env.submit(
136 aliceRegularKey)
137 .ter,
138 tesSUCCESS);
139 env.close();
140
141 EXPECT_FALSE(env.getAccountRoot(alice).isFlag(asfToLsf(flag)));
142
143 std::uint32_t const nowFlags = env.getAccountRoot(alice).getFlags();
144 EXPECT_EQ(nowFlags, origFlags);
145 }
146 else
147 {
148 // Bad flag
149 EXPECT_EQ(env.getAccountRoot(alice).getFlags(), origFlags);
150 EXPECT_EQ(
151 env.submit(transactions::AccountSetBuilder{alice}.setSetFlag(flag), alice).ter,
152 tesSUCCESS);
153 env.close();
154 EXPECT_EQ(env.getAccountRoot(alice).getFlags(), origFlags);
155
156 EXPECT_EQ(
157 env.submit(
159 aliceRegularKey)
160 .ter,
161 tesSUCCESS);
162 env.close();
163 EXPECT_EQ(env.getAccountRoot(alice).getFlags(), origFlags);
164 }
165 }
166 };
167 testFlags({
168 asfRequireDest,
169 asfRequireAuth,
170 asfDisallowXRP,
171 asfGlobalFreeze,
172 asfDisableMaster,
173 asfDefaultRipple,
174 asfDepositAuth,
175 });
176}
177
178TEST(AccountSet, SetAndResetAccountTxnID)
179{
180 TxTest env;
181 Account const alice("alice");
182
183 env.createAccount(alice, XRP(10000));
184
185 std::uint32_t const origFlags = env.getAccountRoot(alice).getFlags();
186
187 // asfAccountTxnID is special and not actually set as a flag,
188 // so we check the field presence instead
189 EXPECT_FALSE(env.getAccountRoot(alice).hasAccountTxnID());
190
191 EXPECT_EQ(
192 env.submit(transactions::AccountSetBuilder{alice}.setSetFlag(asfAccountTxnID), alice).ter,
193 tesSUCCESS);
194 env.close();
195
196 EXPECT_TRUE(env.getAccountRoot(alice).hasAccountTxnID());
197
198 EXPECT_EQ(
199 env.submit(transactions::AccountSetBuilder{alice}.setClearFlag(asfAccountTxnID), alice).ter,
200 tesSUCCESS);
201 env.close();
202
203 EXPECT_FALSE(env.getAccountRoot(alice).hasAccountTxnID());
204
205 std::uint32_t const nowFlags = env.getAccountRoot(alice).getFlags();
206 EXPECT_EQ(nowFlags, origFlags);
207}
208
209TEST(AccountSet, SetNoFreeze)
210{
211 TxTest env;
212 Account const alice("alice");
213 Account const eric("eric");
214
215 env.createAccount(alice, XRP(10000));
216 env.close();
217
218 // Set eric as alice's regular key (eric doesn't need to be funded)
219 EXPECT_EQ(
221 tesSUCCESS);
222 env.close();
223
224 // Verify alice doesn't have NoFreeze flag
225 EXPECT_FALSE(env.getAccountRoot(alice).isFlag(lsfNoFreeze));
226
227 // Setting NoFreeze with regular key should fail - requires master key
228 EXPECT_EQ(
229 env.submit(transactions::AccountSetBuilder{alice}.setSetFlag(asfNoFreeze), eric).ter,
231 env.close();
232
233 // Setting NoFreeze with master key should succeed
234 EXPECT_EQ(
235 env.submit(transactions::AccountSetBuilder{alice}.setSetFlag(asfNoFreeze), alice).ter,
236 tesSUCCESS);
237 env.close();
238
239 // Verify alice now has NoFreeze flag
240 EXPECT_TRUE(env.getAccountRoot(alice).isFlag(lsfNoFreeze));
241
242 // Try to clear NoFreeze - transaction succeeds but flag remains set
243 EXPECT_EQ(
244 env.submit(transactions::AccountSetBuilder{alice}.setClearFlag(asfNoFreeze), alice).ter,
245 tesSUCCESS);
246 env.close();
247
248 // Verify flag is still set (NoFreeze cannot be cleared once set)
249 EXPECT_TRUE(env.getAccountRoot(alice).isFlag(lsfNoFreeze));
250}
251
253{
254 TxTest env;
255 Account const alice("alice");
256
257 env.createAccount(alice, XRP(10000));
258 env.close();
259
260 // The Domain field is represented as the hex string of the lowercase
261 // ASCII of the domain. For example, the domain example.com would be
262 // represented as "6578616d706c652e636f6d".
263 //
264 // To remove the Domain field from an account, send an AccountSet with
265 // the Domain set to an empty string.
266 std::string const domain = "example.com";
267
268 // Set domain
269 EXPECT_EQ(
270 env.submit(transactions::AccountSetBuilder{alice}.setDomain(makeSlice(domain)), alice).ter,
271 tesSUCCESS);
272 env.close();
273
274 EXPECT_TRUE(env.getAccountRoot(alice).hasDomain());
275 // NOLINTNEXTLINE(bugprone-unchecked-optional-access)
276 EXPECT_EQ(*env.getAccountRoot(alice).getDomain(), makeSlice(domain));
277
278 // Clear domain by setting empty
279 EXPECT_EQ(
281 tesSUCCESS);
282 env.close();
283
284 EXPECT_FALSE(env.getAccountRoot(alice).hasDomain());
285
286 // The upper limit on the length is 256 bytes
287 // (defined as DOMAIN_BYTES_MAX in SetAccount)
288 // test the edge cases: 255, 256, 257.
289 std::size_t const maxLength = 256;
290 for (std::size_t len = maxLength - 1; len <= maxLength + 1; ++len)
291 {
292 std::string const domain2 = std::string(len - domain.length() - 1, 'a') + "." + domain;
293
294 EXPECT_EQ(domain2.length(), len);
295
296 if (len <= maxLength)
297 {
298 EXPECT_EQ(
299 env.submit(
301 .ter,
302 tesSUCCESS);
303 env.close();
304
305 // NOLINTNEXTLINE(bugprone-unchecked-optional-access)
306 EXPECT_EQ(*env.getAccountRoot(alice).getDomain(), makeSlice(domain2));
307 }
308 else
309 {
310 EXPECT_EQ(
311 env.submit(
313 .ter,
315 env.close();
316 }
317 }
318}
319
320TEST(AccountSet, MessageKey)
321{
322 TxTest env;
323 Account const alice("alice");
324
325 env.createAccount(alice, XRP(10000));
326 env.close();
327
328 // Generate a random ed25519 key pair for the message key
329 auto const rkp = randomKeyPair(KeyType::Ed25519);
330
331 // Set the message key
332 EXPECT_EQ(
333 env.submit(transactions::AccountSetBuilder{alice}.setMessageKey(rkp.first.slice()), alice)
334 .ter,
335 tesSUCCESS);
336 env.close();
337
338 EXPECT_TRUE(env.getAccountRoot(alice).hasMessageKey());
339 // NOLINTNEXTLINE(bugprone-unchecked-optional-access)
340 EXPECT_EQ(*env.getAccountRoot(alice).getMessageKey(), rkp.first.slice());
341
342 // Clear the message key by setting to empty
343 EXPECT_EQ(
345 tesSUCCESS);
346 env.close();
347
348 EXPECT_FALSE(env.getAccountRoot(alice).hasMessageKey());
349
350 // Try to set an invalid public key - should fail
351 using namespace std::string_literals;
352 EXPECT_EQ(
353 env.submit(
355 makeSlice("NOT_REALLY_A_PUBKEY"s)),
356 alice)
357 .ter,
359}
360
361TEST(AccountSet, WalletID)
362{
363 TxTest env;
364 Account const alice("alice");
365
366 env.createAccount(alice, XRP(10000));
367 env.close();
368
369 std::string_view const locator =
370 "9633EC8AF54F16B5286DB1D7B519EF49EEFC050C0C8AC4384F1D88ACD1BFDF05";
371 uint256 locatorHash{};
372 EXPECT_TRUE(locatorHash.parseHex(locator));
373
374 // Set the wallet locator
375 EXPECT_EQ(
376 env.submit(transactions::AccountSetBuilder{alice}.setWalletLocator(locatorHash), alice).ter,
377 tesSUCCESS);
378 env.close();
379
380 EXPECT_TRUE(env.getAccountRoot(alice).hasWalletLocator());
381 // NOLINTNEXTLINE(bugprone-unchecked-optional-access)
382 EXPECT_EQ(*env.getAccountRoot(alice).getWalletLocator(), locatorHash);
383
384 // Clear the wallet locator by setting to zero
385 EXPECT_EQ(
387 .ter,
388 tesSUCCESS);
389 env.close();
390
391 EXPECT_FALSE(env.getAccountRoot(alice).hasWalletLocator());
392}
393
394TEST(AccountSet, EmailHash)
395{
396 TxTest env;
397 Account const alice("alice");
398
399 env.createAccount(alice, XRP(10000));
400 env.close();
401
402 std::string_view const mh = "5F31A79367DC3137FADA860C05742EE6";
403 uint128 emailHash{};
404 EXPECT_TRUE(emailHash.parseHex(mh));
405
406 // Set the email hash
407 EXPECT_EQ(
408 env.submit(transactions::AccountSetBuilder{alice}.setEmailHash(emailHash), alice).ter,
409 tesSUCCESS);
410 env.close();
411
412 EXPECT_TRUE(env.getAccountRoot(alice).hasEmailHash());
413 // NOLINTNEXTLINE(bugprone-unchecked-optional-access)
414 EXPECT_EQ(*env.getAccountRoot(alice).getEmailHash(), emailHash);
415
416 // Clear the email hash by setting to zero
417 EXPECT_EQ(
419 tesSUCCESS);
420 env.close();
421
422 EXPECT_FALSE(env.getAccountRoot(alice).hasEmailHash());
423}
424
425TEST(AccountSet, TransferRate)
426{
427 struct TestCase
428 {
429 double set;
430 TER code;
431 double get;
432 };
433
434 // Test data: {rate to set, expected TER, expected stored rate}
435 std::vector<TestCase> const testData = {
436 {.set = 1.0, .code = tesSUCCESS, .get = 1.0},
437 {.set = 1.1, .code = tesSUCCESS, .get = 1.1},
438 {.set = 2.0, .code = tesSUCCESS, .get = 2.0},
439 {.set = 2.1, .code = temBAD_TRANSFER_RATE, .get = 2.0}, // > 2.0 is invalid
440 {.set = 0.0, .code = tesSUCCESS, .get = 1.0}, // 0 clears; default rate is 1.0
441 {.set = 2.0, .code = tesSUCCESS, .get = 2.0},
442 {.set = 0.9, .code = temBAD_TRANSFER_RATE, .get = 2.0}, // < 1.0 is invalid
443 };
444
445 TxTest env;
446 Account const alice("alice");
447
448 env.createAccount(alice, XRP(10000));
449 env.close();
450
451 for (auto const& r : testData)
452 {
453 auto const rateValue = static_cast<std::uint32_t>(QUALITY_ONE * r.set);
454
455 EXPECT_EQ(
456 env.submit(transactions::AccountSetBuilder{alice}.setTransferRate(rateValue), alice)
457 .ter,
458 r.code);
459 env.close();
460
461 // If the field is not present, expect the default value (1.0)
462 if (!env.getAccountRoot(alice).hasTransferRate())
463 {
464 EXPECT_EQ(r.get, 1.0);
465 }
466 else
467 {
468 // NOLINTNEXTLINE(bugprone-unchecked-optional-access)
469 EXPECT_EQ(
470 *env.getAccountRoot(alice).getTransferRate(),
471 static_cast<std::uint32_t>(r.get * QUALITY_ONE));
472 }
473 }
474}
475
476TEST(AccountSet, BadInputs)
477{
478 TxTest env;
479 Account const alice("alice");
480
481 env.createAccount(alice, XRP(10000));
482 env.close();
483
484 // Setting and clearing the same flag is invalid
485 EXPECT_EQ(
486 env.submit(
488 .setSetFlag(asfDisallowXRP)
489 .setClearFlag(asfDisallowXRP),
490 alice)
491 .ter,
493
494 EXPECT_EQ(
495 env.submit(
497 .setSetFlag(asfRequireAuth)
498 .setClearFlag(asfRequireAuth),
499 alice)
500 .ter,
502
503 EXPECT_EQ(
504 env.submit(
506 .setSetFlag(asfRequireDest)
507 .setClearFlag(asfRequireDest),
508 alice)
509 .ter,
511
512 // Setting asf flag while also using corresponding tf flag is invalid
513 EXPECT_EQ(
514 env.submit(
516 .setSetFlag(asfDisallowXRP)
517 .setFlags(tfAllowXRP),
518 alice)
519 .ter,
521
522 EXPECT_EQ(
523 env.submit(
525 .setSetFlag(asfRequireAuth)
526 .setFlags(tfOptionalAuth),
527 alice)
528 .ter,
530
531 EXPECT_EQ(
532 env.submit(
534 .setSetFlag(asfRequireDest)
535 .setFlags(tfOptionalDestTag),
536 alice)
537 .ter,
539
540 // Using invalid flags (mask) is invalid
541 EXPECT_EQ(
542 env.submit(
544 .setSetFlag(asfRequireDest)
545 .setFlags(tfAccountSetMask),
546 alice)
547 .ter,
549
550 // Disabling master key without an alternative key is invalid
551 EXPECT_EQ(
552 env.submit(transactions::AccountSetBuilder{alice}.setSetFlag(asfDisableMaster), alice).ter,
554}
555
556TEST(AccountSet, RequireAuthWithDir)
557{
558 TxTest env;
559 Account const alice("alice");
560 Account const bob("bob");
561
562 env.createAccount(alice, XRP(10000));
563 env.close();
564
565 // alice should have an empty directory
566 EXPECT_TRUE(dirIsEmpty(env.getClosedLedger(), keylet::ownerDir(alice.id())));
567
568 // Give alice a signer list, then there will be stuff in the directory
569 // Build the SignerEntries array
570 STArray signerEntries(1);
571 {
572 signerEntries.push_back(STObject::makeInnerObject(sfSignerEntry));
573 STObject& entry = signerEntries.back();
574 entry[sfAccount] = bob.id();
575 entry[sfSignerWeight] = std::uint16_t{1};
576 }
577
578 EXPECT_EQ(
579 env.submit(
580 transactions::SignerListSetBuilder{alice, 1}.setSignerEntries(signerEntries), alice)
581 .ter,
582 tesSUCCESS);
583 env.close();
584
585 EXPECT_FALSE(dirIsEmpty(env.getClosedLedger(), keylet::ownerDir(alice.id())));
586
587 // Setting RequireAuth should fail because alice has owner objects
588 EXPECT_EQ(
589 env.submit(transactions::AccountSetBuilder{alice}.setSetFlag(asfRequireAuth), alice).ter,
590 tecOWNERS);
591
592 // Remove the signer list (quorum = 0, no entries)
593 EXPECT_EQ(env.submit(transactions::SignerListSetBuilder{alice, 0}, alice).ter, tesSUCCESS);
594 env.close();
595
596 EXPECT_TRUE(dirIsEmpty(env.getClosedLedger(), keylet::ownerDir(alice.id())));
597
598 // Now setting RequireAuth should succeed
599 EXPECT_EQ(
600 env.submit(transactions::AccountSetBuilder{alice}.setSetFlag(asfRequireAuth), alice).ter,
601 tesSUCCESS);
602}
603
605{
606 TxTest env;
607 Account const alice("alice");
608
609 env.createAccount(alice, XRP(10000));
610 env.close();
611
612 // Get alice's current sequence - the ticket will be created at seq + 1
613 std::uint32_t const aliceSeqBefore = env.getAccountRoot(alice.id()).getSequence();
614 auto const ticketSeq = SeqProxy::rawTicket(aliceSeqBefore + 1);
615
616 // Create a ticket
617 EXPECT_EQ(env.submit(transactions::TicketCreateBuilder{alice, 1}, alice).ter, tesSUCCESS);
618 env.close();
619
620 // Verify alice has 1 owner object (the ticket)
621 EXPECT_EQ(env.getAccountRoot(alice.id()).getOwnerCount(), 1u);
622 // Verify ticket exists
623 EXPECT_TRUE(env.getClosedLedger().exists(keylet::ticket(alice.id(), ticketSeq)));
624
625 // Try using a ticket that alice doesn't have
626 EXPECT_EQ(
627 env.submit(
628 transactions::AccountSetBuilder{alice}.setTicketSequence(ticketSeq.value() + 1),
629 alice)
630 .ter,
632 env.close();
633
634 // Verify ticket still exists
635 EXPECT_TRUE(env.getClosedLedger().exists(keylet::ticket(alice.id(), ticketSeq)));
636
637 // Get alice's sequence before using the ticket
638 std::uint32_t const aliceSeq = env.getAccountRoot(alice.id()).getSequence();
639
640 // Actually use alice's ticket (noop AccountSet)
641 EXPECT_EQ(
642 env.submit(
643 transactions::AccountSetBuilder{alice}.setTicketSequence(ticketSeq.value()), alice)
644 .ter,
645 tesSUCCESS);
646 env.close();
647
648 // Verify ticket is consumed (no owner objects)
649 EXPECT_EQ(env.getAccountRoot(alice.id()).getOwnerCount(), 0u);
650 EXPECT_FALSE(env.getClosedLedger().exists(keylet::ticket(alice.id(), ticketSeq)));
651
652 // Verify alice's sequence did NOT advance (ticket use doesn't increment seq)
653 EXPECT_EQ(env.getAccountRoot(alice.id()).getSequence(), aliceSeq);
654
655 // Try re-using a ticket that alice already used
656 EXPECT_EQ(
657 env.submit(
658 transactions::AccountSetBuilder{alice}.setTicketSequence(ticketSeq.value()), alice)
659 .ter,
661}
662
663TEST(AccountSet, BadSigningKey)
664{
665 TxTest env;
666 Account const alice("alice");
667
668 env.createAccount(alice, XRP(10000));
669 env.close();
670
671 // Build a valid transaction first, then corrupt the signing key
672 auto stx = transactions::AccountSetBuilder{alice}
674 .setFee(XRPAmount{10})
675 .build(alice.pk(), alice.sk())
676 .getSTTx();
677
678 // Create a copy with a bad signing key
679 STObject obj = *stx;
680 obj.setFieldVL(sfSigningPubKey, makeSlice(std::string("badkey")));
681
682 auto result = env.submit(std::make_shared<STTx>(std::move(obj)));
683 EXPECT_EQ(result.ter, temBAD_SIGNATURE);
684 EXPECT_FALSE(result.applied);
685}
686
688{
689 Account const alice("alice");
690 Account const bob("bob");
691 Account const gw("gateway");
692 IOU const usd("USD", gw);
693
694 // Test gateway with a variety of allowed transfer rates
695 // NOLINTNEXTLINE(bugprone-float-loop-counter)
696 for (double transferRate = 1.0; transferRate <= 2.0; transferRate += 0.03125)
697 {
698 TxTest env;
699
700 env.createAccount(gw, XRP(10000), asfDefaultRipple);
701 env.createAccount(alice, XRP(10000), asfDefaultRipple);
702 env.createAccount(bob, XRP(10000), asfDefaultRipple);
703 env.close();
704
705 // Set up trust lines: alice and bob trust gw for USD
706 EXPECT_EQ(
707 env.submit(transactions::TrustSetBuilder{alice}.setLimitAmount(usd.amount(10)), alice)
708 .ter,
709 tesSUCCESS);
710 EXPECT_EQ(
711 env.submit(transactions::TrustSetBuilder{bob}.setLimitAmount(usd.amount(10)), bob).ter,
712 tesSUCCESS);
713 env.close();
714
715 // Set transfer rate on the gateway
716 EXPECT_EQ(
717 env.submit(
719 static_cast<std::uint32_t>(transferRate * QUALITY_ONE)),
720 gw)
721 .ter,
722 tesSUCCESS);
723 env.close();
724
725 // Calculate the amount with transfer rate applied
726 auto const amount = usd.amount(1);
727 Rate const rate(static_cast<std::uint32_t>(transferRate * QUALITY_ONE));
728 auto const amountWithRate = multiply(amount, rate);
729
730 // Gateway pays alice 10 USD
731 EXPECT_EQ(
732 env.submit(transactions::PaymentBuilder{gw, alice, usd.amount(10)}, gw).ter,
733 tesSUCCESS);
734 env.close();
735
736 // Alice pays bob 1 USD (with sendmax to cover transfer fee)
737 EXPECT_EQ(
738 env.submit(
739 transactions::PaymentBuilder{alice, bob, usd.amount(1)}.setSendMax(
740 usd.amount(10)),
741 alice)
742 .ter,
743 tesSUCCESS);
744 env.close();
745
746 // Check balances
747 EXPECT_EQ(env.getBalance(alice.id(), usd), usd.amount(10) - amountWithRate);
748 EXPECT_EQ(env.getBalance(bob.id(), usd), usd.amount(1));
749 }
750
751 // Test out-of-bounds legacy transfer rates (4.0 and 4.294967295)
752 // These require direct ledger modification since the transactor blocks them
753 for (std::uint32_t const transferRate : {4000000000U, 4294967295U})
754 {
755 TxTest env;
756 env.createAccount(gw, XRP(10000), asfDefaultRipple);
757 env.createAccount(alice, XRP(10000), asfDefaultRipple);
758 env.createAccount(bob, XRP(10000), asfDefaultRipple);
759 env.close();
760
761 // Set up trust lines
762 EXPECT_EQ(
763 env.submit(transactions::TrustSetBuilder{alice}.setLimitAmount(usd.amount(10)), alice)
764 .ter,
765 tesSUCCESS);
766 EXPECT_EQ(
767 env.submit(transactions::TrustSetBuilder{bob}.setLimitAmount(usd.amount(10)), bob).ter,
768 tesSUCCESS);
769 env.close();
770
771 // Set an acceptable transfer rate first (we'll hack it later)
772 EXPECT_EQ(
773 env.submit(
775 static_cast<std::uint32_t>(2.0 * QUALITY_ONE)),
776 gw)
777 .ter,
778 tesSUCCESS);
779 env.close();
780
781 // Directly modify the ledger to set an out-of-bounds transfer rate
782 // This bypasses the transactor's validation
783 auto& view = env.getOpenLedger();
784 auto slePtr = view.read(keylet::account(gw.id()));
785 ASSERT_NE(slePtr, nullptr);
786 auto sleCopy = std::make_shared<SLE>(*slePtr);
787 (*sleCopy)[sfTransferRate] = transferRate;
788 view.rawReplace(sleCopy);
789
790 // Calculate the amount with the legacy transfer rate
791 auto const amount = usd.amount(1);
792 auto const amountWithRate = multiply(amount, Rate(transferRate));
793
794 // Gateway pays alice 10 USD
795 EXPECT_EQ(
796 env.submit(transactions::PaymentBuilder{gw, alice, usd.amount(10)}, gw).ter,
797 tesSUCCESS);
798
799 // Alice pays bob 1 USD
800 EXPECT_EQ(
801 env.submit(
802 transactions::PaymentBuilder{alice, bob, amount}.setSendMax(usd.amount(10)),
803 alice)
804 .ter,
805 tesSUCCESS);
806
807 // Check balances
808 EXPECT_EQ(env.getBalance(alice.id(), usd), usd.amount(10) - amountWithRate);
809 EXPECT_EQ(env.getBalance(bob.id(), usd), amount);
810 }
811}
812
813} // namespace xrpl::test
constexpr bool parseHex(std::string_view sv)
Parse a hex string into a base_uint.
Definition base_uint.h:525
SLE::const_pointer read(Keylet const &k) const override
Return the state item associated with a key.
Definition OpenView.cpp:167
virtual bool exists(Keylet const &k) const =0
Determine if a state item exists.
void push_back(STObject const &object)
Definition STArray.h:79
STObject & back()
Definition STArray.h:193
void setFieldVL(SField const &field, Blob const &)
Definition STObject.cpp:791
static STObject makeInnerObject(SField const &name)
Definition STObject.cpp:79
static constexpr SeqProxy rawTicket(std::uint32_t v)
Factory function to return a ticket-based SeqProxy.
Definition SeqProxy.h:74
An immutable linear range of bytes.
Definition Slice.h:28
Ledger Entry: AccountRoot.
Definition AccountRoot.h:28
SF_UINT32::type::value_type getSequence() const
Get sfSequence (SoeRequired).
Definition AccountRoot.h:65
protocol_autogen::Optional< SF_VL::type::value_type > getDomain() const
Get sfDomain (SoeOptional).
bool hasWalletLocator() const
Check if sfWalletLocator is present.
bool hasAccountTxnID() const
Check if sfAccountTxnID is present.
bool hasDomain() const
Check if sfDomain is present.
bool hasEmailHash() const
Check if sfEmailHash is present.
protocol_autogen::Optional< SF_UINT32::type::value_type > getTransferRate() const
Get sfTransferRate (SoeOptional).
bool hasMessageKey() const
Check if sfMessageKey is present.
bool hasTransferRate() const
Check if sfTransferRate is present.
protocol_autogen::Optional< SF_UINT128::type::value_type > getEmailHash() const
Get sfEmailHash (SoeOptional).
SF_UINT32::type::value_type getOwnerCount() const
Get sfOwnerCount (SoeRequired).
Definition AccountRoot.h:87
protocol_autogen::Optional< SF_VL::type::value_type > getMessageKey() const
Get sfMessageKey (SoeOptional).
protocol_autogen::Optional< SF_UINT256::type::value_type > getWalletLocator() const
Get sfWalletLocator (SoeOptional).
bool isFlag(std::uint32_t f) const
Check if a specific flag is set.
std::uint32_t getFlags() const
Get the flags field (sfFlags).
A lightweight transaction testing harness.
Definition TxTest.h:185
void createAccount(Account const &account, XRPAmount xrp, uint32_t accountFlags=0)
Create a new account in the ledger.
Definition TxTest.cpp:125
OpenView & getOpenLedger()
Get the current open ledger view.
Definition TxTest.cpp:159
ReadView const & getClosedLedger() const
Get the closed (base) ledger view.
Definition TxTest.cpp:171
void close()
Close the current ledger.
Definition TxTest.cpp:177
STAmount getBalance(AccountID const &account, IOU const &iou) const
Get the balance of an IOU for an account.
Definition TxTest.cpp:234
ledger_entries::AccountRoot getAccountRoot(AccountID const &id) const
Get the account root object from the current open ledger.
Definition TxTest.cpp:150
TxResult submit(T &&builder, Account const &signer)
Submit a transaction from a builder.
Definition TxTest.h:227
Immutable cryptographic account descriptor.
Definition jtx/Account.h:21
SecretKey const & sk() const
Return the secret key.
Definition jtx/Account.h:93
PublicKey const & pk() const
Return the public key.
Definition jtx/Account.h:84
AccountID id() const
Returns the Account ID.
Set the domain on a JTx.
Definition domain.h:14
Converts to IOU Issue or STAmount.
AccountSetBuilder & setMessageKey(std::decay_t< typename SF_VL::type::value_type > const &value)
Set sfMessageKey (SoeOptional).
AccountSetBuilder & setSetFlag(std::decay_t< typename SF_UINT32::type::value_type > const &value)
Set sfSetFlag (SoeOptional).
AccountSetBuilder & setTransferRate(std::decay_t< typename SF_UINT32::type::value_type > const &value)
Set sfTransferRate (SoeOptional).
AccountSetBuilder & setEmailHash(std::decay_t< typename SF_UINT128::type::value_type > const &value)
Transaction-specific field setters.
AccountSetBuilder & setClearFlag(std::decay_t< typename SF_UINT32::type::value_type > const &value)
Set sfClearFlag (SoeOptional).
AccountSetBuilder & setDomain(std::decay_t< typename SF_VL::type::value_type > const &value)
Set sfDomain (SoeOptional).
AccountSetBuilder & setWalletLocator(std::decay_t< typename SF_UINT256::type::value_type > const &value)
Set sfWalletLocator (SoeOptional).
PaymentBuilder & setSendMax(std::decay_t< typename SF_AMOUNT::type::value_type > const &value)
Set sfSendMax (SoeOptional).
SetRegularKeyBuilder & setRegularKey(std::decay_t< typename SF_ACCOUNT::type::value_type > const &value)
Transaction-specific field setters.
SignerListSetBuilder & setSignerEntries(STArray const &value)
Set sfSignerEntries (SoeOptional).
Derived & setFee(STAmount const &value)
Set the transaction fee.
Derived & setFlags(std::uint32_t const &value)
Set transaction flags.
Derived & setTicketSequence(std::uint32_t const &value)
Set the ticket sequence to use for this transaction.
Derived & setSequence(std::uint32_t const &value)
Set the sequence number.
TrustSetBuilder & setLimitAmount(std::decay_t< typename SF_AMOUNT::type::value_type > const &value)
Transaction-specific field setters.
T find(T... args)
T make_shared(T... args)
constexpr Zero kZero
Definition Zero.h:30
Keylet ownerDir(AccountID const &id) noexcept
The root page of an account's directory.
Definition Indexes.cpp:373
Keylet ticket(AccountID const &id, SeqProxy const &ticketSeq)
A ticket belonging to an account.
Definition Indexes.cpp:310
Keylet account(AccountID const &id) noexcept
AccountID root.
Definition Indexes.cpp:198
XrpT const XRP
Converts to XRP Issue or STAmount.
Definition amount.cpp:92
json::Value rate(Account const &account, double multiplier)
Set a transfer rate.
Definition rate.cpp:15
TEST(UnitsTest, types)
Definition Units.cpp:16
constexpr XRPAmount
Convert XRP to drops (integral types).
Definition TxTest.h:54
constexpr std::uint32_t asfToLsf(std::uint32_t asf)
Convert AccountSet flag (asf) to LedgerState flag (lsf).
Definition TxTest.h:99
@ telBAD_DOMAIN
Definition TER.h:39
@ telBAD_PUBLIC_KEY
Definition TER.h:41
@ terPRE_TICKET
Definition TER.h:222
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,...
std::pair< PublicKey, SecretKey > randomKeyPair(KeyType type)
Create a key pair using secure random numbers.
bool dirIsEmpty(ReadView const &view, Keylet const &k)
Returns true if the directory is empty.
T get(Section const &section, std::string const &name, T const &defaultValue=T{})
Retrieve a key/value pair from a section.
@ tefNO_TICKET
Definition TER.h:177
BaseUInt< 128 > uint128
Definition base_uint.h:578
Slice makeSlice(std::array< T, N > const &a)
Definition Slice.h:228
Rate transferRate(ReadView const &view, AccountID const &issuer)
Returns IOU issuer transfer fee as Rate.
@ temINVALID_FLAG
Definition TER.h:99
@ temBAD_TRANSFER_RATE
Definition TER.h:95
@ temBAD_SIGNATURE
Definition TER.h:93
TERSubset< CanCvtToTER > TER
Definition TER.h:647
@ tecNEED_MASTER_KEY
Definition TER.h:311
@ tecNO_ALTERNATIVE_KEY
Definition TER.h:299
@ tecOWNERS
Definition TER.h:301
STAmount multiply(STAmount const &amount, Number const &frac, Number::RoundingMode rm)
BaseUInt< 256 > uint256
Definition base_uint.h:580
@ tesSUCCESS
Definition TER.h:245
T length(T... args)
Represents a transfer rate.
Definition Rate.h:21
TER ter
The transaction engine result code.
Definition TxTest.h:160