xrpld
Loading...
Searching...
No Matches
RobustTransaction_test.cpp
1#include <test/jtx/Env.h>
2#include <test/jtx/WSClient.h>
3#include <test/jtx/amount.h>
4#include <test/jtx/balance.h> // IWYU pragma: keep
5#include <test/jtx/flags.h>
6#include <test/jtx/pay.h>
7
8#include <xrpl/beast/unit_test/suite.h>
9#include <xrpl/core/JobQueue.h>
10#include <xrpl/json/json_value.h>
11#include <xrpl/protocol/SField.h>
12#include <xrpl/protocol/Seed.h>
13#include <xrpl/protocol/jss.h>
14
15#include <chrono>
16
17namespace xrpl::test {
18
20{
21public:
22 void
24 {
25 using namespace std::chrono_literals;
26 using namespace jtx;
27 Env env(*this);
28 env.fund(XRP(10000), "alice", "bob");
29 env.close();
30 auto wsc = makeWSClient(env.app().config());
31
32 {
33 // RPC subscribe to transactions stream
34 json::Value jv;
35 jv[jss::streams] = json::ValueType::Array;
36 jv[jss::streams].append("transactions");
37 jv = wsc->invoke("subscribe", jv);
38 BEAST_EXPECT(jv[jss::status] == "success");
39 if (wsc->version() == 2)
40 {
41 BEAST_EXPECT(jv.isMember(jss::jsonrpc) && jv[jss::jsonrpc] == "2.0");
42 BEAST_EXPECT(jv.isMember(jss::ripplerpc) && jv[jss::ripplerpc] == "2.0");
43 BEAST_EXPECT(jv.isMember(jss::id) && jv[jss::id] == 5);
44 }
45 }
46
47 {
48 // Submit past ledger sequence transaction
49 json::Value payment;
50 payment[jss::secret] = toBase58(generateSeed("alice"));
51 payment[jss::tx_json] = pay("alice", "bob", XRP(1));
52 payment[jss::tx_json][sfLastLedgerSequence.fieldName] = 1;
53 auto jv = wsc->invoke("submit", payment);
54 if (wsc->version() == 2)
55 {
56 BEAST_EXPECT(jv.isMember(jss::jsonrpc) && jv[jss::jsonrpc] == "2.0");
57 BEAST_EXPECT(jv.isMember(jss::ripplerpc) && jv[jss::ripplerpc] == "2.0");
58 BEAST_EXPECT(jv.isMember(jss::id) && jv[jss::id] == 5);
59 }
60 BEAST_EXPECT(jv[jss::result][jss::engine_result] == "tefMAX_LEDGER");
61
62 // Submit past sequence transaction
63 payment[jss::tx_json] = pay("alice", "bob", XRP(1));
64 payment[jss::tx_json][sfSequence.fieldName] = env.seq("alice") - 1;
65 jv = wsc->invoke("submit", payment);
66 if (wsc->version() == 2)
67 {
68 BEAST_EXPECT(jv.isMember(jss::jsonrpc) && jv[jss::jsonrpc] == "2.0");
69 BEAST_EXPECT(jv.isMember(jss::ripplerpc) && jv[jss::ripplerpc] == "2.0");
70 BEAST_EXPECT(jv.isMember(jss::id) && jv[jss::id] == 5);
71 }
72 BEAST_EXPECT(jv[jss::result][jss::engine_result] == "tefPAST_SEQ");
73
74 // Submit future sequence transaction
75 payment[jss::tx_json][sfSequence.fieldName] = env.seq("alice") + 1;
76 jv = wsc->invoke("submit", payment);
77 if (wsc->version() == 2)
78 {
79 BEAST_EXPECT(jv.isMember(jss::jsonrpc) && jv[jss::jsonrpc] == "2.0");
80 BEAST_EXPECT(jv.isMember(jss::ripplerpc) && jv[jss::ripplerpc] == "2.0");
81 BEAST_EXPECT(jv.isMember(jss::id) && jv[jss::id] == 5);
82 }
83 BEAST_EXPECT(jv[jss::result][jss::engine_result] == "terPRE_SEQ");
84
85 // Submit transaction to bridge the sequence gap
86 payment[jss::tx_json][sfSequence.fieldName] = env.seq("alice");
87 jv = wsc->invoke("submit", payment);
88 if (wsc->version() == 2)
89 {
90 BEAST_EXPECT(jv.isMember(jss::jsonrpc) && jv[jss::jsonrpc] == "2.0");
91 BEAST_EXPECT(jv.isMember(jss::ripplerpc) && jv[jss::ripplerpc] == "2.0");
92 BEAST_EXPECT(jv.isMember(jss::id) && jv[jss::id] == 5);
93 }
94 BEAST_EXPECT(jv[jss::result][jss::engine_result] == "tesSUCCESS");
95
96 // Wait for the jobqueue to process everything
97 env.app().getJobQueue().rendezvous();
98
99 // Finalize transactions
100 jv = wsc->invoke("ledger_accept");
101 if (wsc->version() == 2)
102 {
103 BEAST_EXPECT(jv.isMember(jss::jsonrpc) && jv[jss::jsonrpc] == "2.0");
104 BEAST_EXPECT(jv.isMember(jss::ripplerpc) && jv[jss::ripplerpc] == "2.0");
105 BEAST_EXPECT(jv.isMember(jss::id) && jv[jss::id] == 5);
106 }
107 BEAST_EXPECT(jv[jss::result].isMember(jss::ledger_current_index));
108 }
109
110 {
111 // Check balances
112 BEAST_EXPECT(wsc->findMsg(5s, [&](auto const& jv) {
113 auto const& ff = jv[jss::meta]["AffectedNodes"][1u]["ModifiedNode"]["FinalFields"];
114 return ff[jss::Account] == Account("bob").human() && ff["Balance"] == "10001000000";
115 }));
116
117 BEAST_EXPECT(wsc->findMsg(5s, [&](auto const& jv) {
118 auto const& ff = jv[jss::meta]["AffectedNodes"][1u]["ModifiedNode"]["FinalFields"];
119 return ff[jss::Account] == Account("bob").human() && ff["Balance"] == "10002000000";
120 }));
121 }
122
123 {
124 // RPC unsubscribe to transactions stream
125 json::Value jv;
126 jv[jss::streams] = json::ValueType::Array;
127 jv[jss::streams].append("transactions");
128 jv = wsc->invoke("unsubscribe", jv);
129 if (wsc->version() == 2)
130 {
131 BEAST_EXPECT(jv.isMember(jss::jsonrpc) && jv[jss::jsonrpc] == "2.0");
132 BEAST_EXPECT(jv.isMember(jss::ripplerpc) && jv[jss::ripplerpc] == "2.0");
133 BEAST_EXPECT(jv.isMember(jss::id) && jv[jss::id] == 5);
134 }
135 BEAST_EXPECT(jv[jss::status] == "success");
136 }
137 }
138
139 /*
140 Submit a normal payment. Client disconnects after the proposed
141 transaction result is received.
142
143 Client reconnects in the future. During this time it is presumed that the
144 transaction should have succeeded.
145
146 Upon reconnection, recent account transaction history is loaded.
147 The submitted transaction should be detected, and the transaction should
148 ultimately succeed.
149 */
150 void
152 {
153 using namespace jtx;
154 Env env(*this);
155 env.fund(XRP(10000), "alice", "bob");
156 env.close();
157 auto wsc = makeWSClient(env.app().config());
158
159 {
160 // Submit normal payment
161 json::Value jv;
162 jv[jss::secret] = toBase58(generateSeed("alice"));
163 jv[jss::tx_json] = pay("alice", "bob", XRP(1));
164 jv = wsc->invoke("submit", jv);
165 if (wsc->version() == 2)
166 {
167 BEAST_EXPECT(jv.isMember(jss::jsonrpc) && jv[jss::jsonrpc] == "2.0");
168 BEAST_EXPECT(jv.isMember(jss::ripplerpc) && jv[jss::ripplerpc] == "2.0");
169 BEAST_EXPECT(jv.isMember(jss::id) && jv[jss::id] == 5);
170 }
171 BEAST_EXPECT(jv[jss::result][jss::engine_result] == "tesSUCCESS");
172
173 // Disconnect
174 wsc.reset();
175
176 // Server finalizes transaction
177 env.close();
178 }
179
180 {
181 // RPC account_tx
182 json::Value jv;
183 jv[jss::account] = Account("bob").human();
184 jv[jss::ledger_index_min] = -1;
185 jv[jss::ledger_index_max] = -1;
186 wsc = makeWSClient(env.app().config());
187 jv = wsc->invoke("account_tx", jv);
188 if (wsc->version() == 2)
189 {
190 BEAST_EXPECT(jv.isMember(jss::jsonrpc) && jv[jss::jsonrpc] == "2.0");
191 BEAST_EXPECT(jv.isMember(jss::ripplerpc) && jv[jss::ripplerpc] == "2.0");
192 BEAST_EXPECT(jv.isMember(jss::id) && jv[jss::id] == 5);
193 }
194
195 // Check balance
196 auto ff = jv[jss::result][jss::transactions][0u][jss::meta]["AffectedNodes"][1u]
197 ["ModifiedNode"]["FinalFields"];
198 BEAST_EXPECT(ff[jss::Account] == Account("bob").human());
199 BEAST_EXPECT(ff["Balance"] == "10001000000");
200 }
201 }
202
203 void
205 {
206 using namespace std::chrono_literals;
207 using namespace jtx;
208 Env env(*this);
209 env.fund(XRP(10000), "alice", "bob");
210 env.close();
211 auto wsc = makeWSClient(env.app().config());
212
213 {
214 // Submit normal payment
215 json::Value jv;
216 jv[jss::secret] = toBase58(generateSeed("alice"));
217 jv[jss::tx_json] = pay("alice", "bob", XRP(1));
218 jv = wsc->invoke("submit", jv);
219 if (wsc->version() == 2)
220 {
221 BEAST_EXPECT(jv.isMember(jss::jsonrpc) && jv[jss::jsonrpc] == "2.0");
222 BEAST_EXPECT(jv.isMember(jss::ripplerpc) && jv[jss::ripplerpc] == "2.0");
223 BEAST_EXPECT(jv.isMember(jss::id) && jv[jss::id] == 5);
224 }
225 BEAST_EXPECT(jv[jss::result][jss::engine_result] == "tesSUCCESS");
226
227 // Finalize transaction
228 jv = wsc->invoke("ledger_accept");
229 if (wsc->version() == 2)
230 {
231 BEAST_EXPECT(jv.isMember(jss::jsonrpc) && jv[jss::jsonrpc] == "2.0");
232 BEAST_EXPECT(jv.isMember(jss::ripplerpc) && jv[jss::ripplerpc] == "2.0");
233 BEAST_EXPECT(jv.isMember(jss::id) && jv[jss::id] == 5);
234 }
235 BEAST_EXPECT(jv[jss::result].isMember(jss::ledger_current_index));
236
237 // Wait for the jobqueue to process everything
238 env.app().getJobQueue().rendezvous();
239 }
240
241 {
242 {
243 // RPC subscribe to ledger stream
244 json::Value jv;
245 jv[jss::streams] = json::ValueType::Array;
246 jv[jss::streams].append("ledger");
247 jv = wsc->invoke("subscribe", jv);
248 if (wsc->version() == 2)
249 {
250 BEAST_EXPECT(jv.isMember(jss::jsonrpc) && jv[jss::jsonrpc] == "2.0");
251 BEAST_EXPECT(jv.isMember(jss::ripplerpc) && jv[jss::ripplerpc] == "2.0");
252 BEAST_EXPECT(jv.isMember(jss::id) && jv[jss::id] == 5);
253 }
254 BEAST_EXPECT(jv[jss::status] == "success");
255 }
256
257 // Close ledgers
258 for (auto i = 0; i < 8; ++i)
259 {
260 auto jv = wsc->invoke("ledger_accept");
261 if (wsc->version() == 2)
262 {
263 BEAST_EXPECT(jv.isMember(jss::jsonrpc) && jv[jss::jsonrpc] == "2.0");
264 BEAST_EXPECT(jv.isMember(jss::ripplerpc) && jv[jss::ripplerpc] == "2.0");
265 BEAST_EXPECT(jv.isMember(jss::id) && jv[jss::id] == 5);
266 }
267 BEAST_EXPECT(jv[jss::result].isMember(jss::ledger_current_index));
268
269 // Wait for the jobqueue to process everything
270 env.app().getJobQueue().rendezvous();
271
272 BEAST_EXPECT(wsc->findMsg(
273 5s, [&](auto const& jval) { return jval[jss::type] == "ledgerClosed"; }));
274 }
275
276 {
277 // RPC unsubscribe to ledger stream
278 json::Value jv;
279 jv[jss::streams] = json::ValueType::Array;
280 jv[jss::streams].append("ledger");
281 jv = wsc->invoke("unsubscribe", jv);
282 if (wsc->version() == 2)
283 {
284 BEAST_EXPECT(jv.isMember(jss::jsonrpc) && jv[jss::jsonrpc] == "2.0");
285 BEAST_EXPECT(jv.isMember(jss::ripplerpc) && jv[jss::ripplerpc] == "2.0");
286 BEAST_EXPECT(jv.isMember(jss::id) && jv[jss::id] == 5);
287 }
288 BEAST_EXPECT(jv[jss::status] == "success");
289 }
290 }
291
292 {
293 // Disconnect, reconnect
294 wsc = makeWSClient(env.app().config());
295 {
296 // RPC subscribe to ledger stream
297 json::Value jv;
298 jv[jss::streams] = json::ValueType::Array;
299 jv[jss::streams].append("ledger");
300 jv = wsc->invoke("subscribe", jv);
301 if (wsc->version() == 2)
302 {
303 BEAST_EXPECT(jv.isMember(jss::jsonrpc) && jv[jss::jsonrpc] == "2.0");
304 BEAST_EXPECT(jv.isMember(jss::ripplerpc) && jv[jss::ripplerpc] == "2.0");
305 BEAST_EXPECT(jv.isMember(jss::id) && jv[jss::id] == 5);
306 }
307 BEAST_EXPECT(jv[jss::status] == "success");
308 }
309
310 // Close ledgers
311 for (auto i = 0; i < 2; ++i)
312 {
313 auto jv = wsc->invoke("ledger_accept");
314 if (wsc->version() == 2)
315 {
316 BEAST_EXPECT(jv.isMember(jss::jsonrpc) && jv[jss::jsonrpc] == "2.0");
317 BEAST_EXPECT(jv.isMember(jss::ripplerpc) && jv[jss::ripplerpc] == "2.0");
318 BEAST_EXPECT(jv.isMember(jss::id) && jv[jss::id] == 5);
319 }
320 BEAST_EXPECT(jv[jss::result].isMember(jss::ledger_current_index));
321
322 // Wait for the jobqueue to process everything
323 env.app().getJobQueue().rendezvous();
324
325 BEAST_EXPECT(wsc->findMsg(
326 5s, [&](auto const& jval) { return jval[jss::type] == "ledgerClosed"; }));
327 }
328
329 {
330 // RPC unsubscribe to ledger stream
331 json::Value jv;
332 jv[jss::streams] = json::ValueType::Array;
333 jv[jss::streams].append("ledger");
334 jv = wsc->invoke("unsubscribe", jv);
335 if (wsc->version() == 2)
336 {
337 BEAST_EXPECT(jv.isMember(jss::jsonrpc) && jv[jss::jsonrpc] == "2.0");
338 BEAST_EXPECT(jv.isMember(jss::ripplerpc) && jv[jss::ripplerpc] == "2.0");
339 BEAST_EXPECT(jv.isMember(jss::id) && jv[jss::id] == 5);
340 }
341 BEAST_EXPECT(jv[jss::status] == "success");
342 }
343 }
344
345 {
346 // RPC account_tx
347 json::Value jv;
348 jv[jss::account] = Account("bob").human();
349 jv[jss::ledger_index_min] = -1;
350 jv[jss::ledger_index_max] = -1;
351 wsc = makeWSClient(env.app().config());
352 jv = wsc->invoke("account_tx", jv);
353 if (wsc->version() == 2)
354 {
355 BEAST_EXPECT(jv.isMember(jss::jsonrpc) && jv[jss::jsonrpc] == "2.0");
356 BEAST_EXPECT(jv.isMember(jss::ripplerpc) && jv[jss::ripplerpc] == "2.0");
357 BEAST_EXPECT(jv.isMember(jss::id) && jv[jss::id] == 5);
358 }
359
360 // Check balance
361 auto ff = jv[jss::result][jss::transactions][0u][jss::meta]["AffectedNodes"][1u]
362 ["ModifiedNode"]["FinalFields"];
363 BEAST_EXPECT(ff[jss::Account] == Account("bob").human());
364 BEAST_EXPECT(ff["Balance"] == "10001000000");
365 }
366 }
367
368 void
370 {
371 using namespace std::chrono_literals;
372 using namespace jtx;
373 Env env(*this);
374 env.fund(XRP(10000), "alice");
375 env.close();
376 auto wsc = makeWSClient(env.app().config());
377
378 {
379 // RPC subscribe to accounts_proposed stream
380 json::Value jv;
381 jv[jss::accounts_proposed] = json::ValueType::Array;
382 jv[jss::accounts_proposed].append(Account("alice").human());
383 jv = wsc->invoke("subscribe", jv);
384 if (wsc->version() == 2)
385 {
386 BEAST_EXPECT(jv.isMember(jss::jsonrpc) && jv[jss::jsonrpc] == "2.0");
387 BEAST_EXPECT(jv.isMember(jss::ripplerpc) && jv[jss::ripplerpc] == "2.0");
388 BEAST_EXPECT(jv.isMember(jss::id) && jv[jss::id] == 5);
389 }
390 BEAST_EXPECT(jv[jss::status] == "success");
391 }
392
393 {
394 // Submit account_set transaction
395 json::Value jv;
396 jv[jss::secret] = toBase58(generateSeed("alice"));
397 jv[jss::tx_json] = fset("alice", 0);
398 jv[jss::tx_json][jss::Fee] = static_cast<int>(env.current()->fees().base.drops());
399 jv = wsc->invoke("submit", jv);
400 if (wsc->version() == 2)
401 {
402 BEAST_EXPECT(jv.isMember(jss::jsonrpc) && jv[jss::jsonrpc] == "2.0");
403 BEAST_EXPECT(jv.isMember(jss::ripplerpc) && jv[jss::ripplerpc] == "2.0");
404 BEAST_EXPECT(jv.isMember(jss::id) && jv[jss::id] == 5);
405 }
406 BEAST_EXPECT(jv[jss::result][jss::engine_result] == "tesSUCCESS");
407 }
408
409 {
410 // Check stream update
411 BEAST_EXPECT(wsc->findMsg(5s, [&](auto const& jv) {
412 return jv[jss::transaction][jss::TransactionType] == jss::AccountSet;
413 }));
414 }
415
416 {
417 // RPC unsubscribe to accounts_proposed stream
418 json::Value jv;
419 jv[jss::accounts_proposed] = json::ValueType::Array;
420 jv[jss::accounts_proposed].append(Account("alice").human());
421 jv = wsc->invoke("unsubscribe", jv);
422 if (wsc->version() == 2)
423 {
424 BEAST_EXPECT(jv.isMember(jss::jsonrpc) && jv[jss::jsonrpc] == "2.0");
425 BEAST_EXPECT(jv.isMember(jss::ripplerpc) && jv[jss::ripplerpc] == "2.0");
426 BEAST_EXPECT(jv.isMember(jss::id) && jv[jss::id] == 5);
427 }
428 BEAST_EXPECT(jv[jss::status] == "success");
429 }
430 }
431
432 void
433 run() override
434 {
439 }
440};
441
442BEAST_DEFINE_TESTSUITE(RobustTransaction, rpc, xrpl);
443
444} // namespace xrpl::test
A testsuite class.
Definition suite.h:50
Represents a JSON value.
Definition json_value.h:130
Value & append(Value const &value)
Append value to array at the end.
bool isMember(char const *key) const
Return true if the object has a member named key.
virtual Config & config()=0
void rendezvous()
Block until no jobs running.
Definition JobQueue.cpp:243
virtual JobQueue & getJobQueue()=0
A transaction testing environment.
Definition Env.h:143
Application & app()
Definition Env.h:280
bool close(NetClock::time_point closeTime, std::optional< std::chrono::milliseconds > consensusDelay=std::nullopt)
Close and advance the ledger.
Definition Env.cpp:133
void fund(bool setDefaultRipple, STAmount const &amount, Account const &account)
Definition Env.cpp:296
std::uint32_t seq(Account const &account) const
Returns the next sequence number on account.
Definition Env.cpp:275
std::shared_ptr< OpenView const > current() const
Returns the current ledger.
Definition Env.h:353
@ Array
array value (ordered list)
Definition json_value.h:25
json::Value pay(AccountID const &account, AccountID const &to, AnyAmount amount)
Create a payment.
Definition pay.cpp:14
XrpT const XRP
Converts to XRP Issue or STAmount.
Definition amount.cpp:92
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)
std::unique_ptr< WSClient > makeWSClient(Config const &cfg, bool v2, unsigned rpcVersion, std::unordered_map< std::string, std::string > const &headers)
Returns a client operating through WebSockets/S.
Definition WSClient.cpp:329
Use hash_* containers for keys that do not need a cryptographically secure hashing algorithm.
Definition algorithm.h:5
std::string toBase58(AccountID const &v)
Convert AccountID to base58 checked string.
Definition AccountID.cpp:93
Seed generateSeed(std::string const &passPhrase)
Generate a seed deterministically.
Definition Seed.cpp:58