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