testing_api_cmd_withdraw.c (21604B)
1 /* 2 This file is part of TALER 3 Copyright (C) 2018-2024 Taler Systems SA 4 5 TALER is free software; you can redistribute it and/or modify it 6 under the terms of the GNU General Public License as published by 7 the Free Software Foundation; either version 3, or (at your 8 option) any later version. 9 10 TALER is distributed in the hope that it will be useful, but 11 WITHOUT ANY WARRANTY; without even the implied warranty of 12 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 13 General Public License for more details. 14 15 You should have received a copy of the GNU General Public 16 License along with TALER; see the file COPYING. If not, see 17 <http://www.gnu.org/licenses/> 18 */ 19 /** 20 * @file testing/testing_api_cmd_withdraw.c 21 * @brief main interpreter loop for testcases 22 * @author Christian Grothoff 23 * @author Marcello Stanisci 24 * @author Özgür Kesim 25 */ 26 #include "taler/platform.h" 27 #include "taler/taler_json_lib.h" 28 #include <microhttpd.h> 29 #include <gnunet/gnunet_curl_lib.h> 30 #include "taler/taler_signatures.h" 31 #include "taler/taler_extensions.h" 32 #include "taler/taler_testing_lib.h" 33 #include "taler/backoff.h" 34 35 36 /** 37 * How often do we retry before giving up? 38 */ 39 #define NUM_RETRIES 15 40 41 /** 42 * How long do we wait AT LEAST if the exchange says the reserve is unknown? 43 */ 44 #define UNKNOWN_MIN_BACKOFF GNUNET_TIME_relative_multiply ( \ 45 GNUNET_TIME_UNIT_MILLISECONDS, 10) 46 47 /** 48 * How long do we wait AT MOST if the exchange says the reserve is unknown? 49 */ 50 #define UNKNOWN_MAX_BACKOFF GNUNET_TIME_relative_multiply ( \ 51 GNUNET_TIME_UNIT_MILLISECONDS, 100) 52 53 /** 54 * State for a "withdraw" CMD. 55 */ 56 struct WithdrawState 57 { 58 59 /** 60 * Which reserve should we withdraw from? 61 */ 62 const char *reserve_reference; 63 64 /** 65 * Reference to a withdraw or reveal operation from which we should 66 * reuse the private coin key, or NULL for regular withdrawal. 67 */ 68 const char *reuse_coin_key_ref; 69 70 /** 71 * If true and @e reuse_coin_key_ref is not NULL, also reuses 72 * the blinding_seed. 73 */ 74 bool reuse_blinding_seed; 75 76 /** 77 * Our command. 78 */ 79 const struct TALER_TESTING_Command *cmd; 80 81 /** 82 * String describing the denomination value we should withdraw. 83 * A corresponding denomination key must exist in the exchange's 84 * offerings. Can be NULL if @e pk is set instead. 85 */ 86 struct TALER_Amount amount; 87 88 /** 89 * If @e amount is NULL, this specifies the denomination key to 90 * use. Otherwise, this will be set (by the interpreter) to the 91 * denomination PK matching @e amount. 92 */ 93 struct TALER_EXCHANGE_DenomPublicKey *pk; 94 95 /** 96 * Exchange base URL. Only used as offered trait. 97 */ 98 char *exchange_url; 99 100 /** 101 * URI if the reserve we are withdrawing from. 102 */ 103 struct TALER_NormalizedPayto reserve_payto_uri; 104 105 /** 106 * Private key of the reserve we are withdrawing from. 107 */ 108 struct TALER_ReservePrivateKeyP reserve_priv; 109 110 /** 111 * Public key of the reserve we are withdrawing from. 112 */ 113 struct TALER_ReservePublicKeyP reserve_pub; 114 115 /** 116 * Private key of the coin. 117 */ 118 struct TALER_CoinSpendPrivateKeyP coin_priv; 119 120 /** 121 * Blinding key used during the operation. 122 */ 123 union GNUNET_CRYPTO_BlindingSecretP bks; 124 125 /** 126 * Values contributed from the exchange during the 127 * withdraw protocol. 128 */ 129 struct TALER_ExchangeBlindingValues exchange_vals; 130 131 /** 132 * Interpreter state (during command). 133 */ 134 struct TALER_TESTING_Interpreter *is; 135 136 /** 137 * Set (by the interpreter) to the exchange's signature over the 138 * coin's public key. 139 */ 140 struct TALER_DenominationSignature sig; 141 142 /** 143 * Seed for the key material of the coin, set by the interpreter. 144 */ 145 struct TALER_WithdrawMasterSeedP seed; 146 147 /** 148 * Blinding seed for the blinding preparation for CS. 149 */ 150 struct TALER_BlindingMasterSeedP blinding_seed; 151 152 /** 153 * An age > 0 signifies age restriction is required 154 */ 155 uint8_t age; 156 157 /** 158 * If age > 0, put here the corresponding age commitment with its proof and 159 * its hash, respectively. 160 */ 161 struct TALER_AgeCommitmentProof age_commitment_proof; 162 struct TALER_AgeCommitmentHashP h_age_commitment; 163 164 /** 165 * Reserve history entry that corresponds to this operation. 166 * Will be of type #TALER_EXCHANGE_RTT_WITHDRAWAL. 167 */ 168 struct TALER_EXCHANGE_ReserveHistoryEntry reserve_history; 169 170 /** 171 * Withdraw handle (while operation is running). 172 */ 173 struct TALER_EXCHANGE_WithdrawHandle *wsh; 174 175 /** 176 * The commitment for the withdraw operation, later needed for /recoup 177 */ 178 struct TALER_HashBlindedPlanchetsP planchets_h; 179 180 /** 181 * Task scheduled to try later. 182 */ 183 struct GNUNET_SCHEDULER_Task *retry_task; 184 185 /** 186 * How long do we wait until we retry? 187 */ 188 struct GNUNET_TIME_Relative backoff; 189 190 /** 191 * Total withdraw backoff applied. 192 */ 193 struct GNUNET_TIME_Relative total_backoff; 194 195 /** 196 * Set to the KYC requirement payto hash *if* the exchange replied with a 197 * request for KYC. 198 */ 199 struct TALER_NormalizedPaytoHashP h_payto; 200 201 /** 202 * Set to the KYC requirement row *if* the exchange replied with 203 * a request for KYC. 204 */ 205 uint64_t requirement_row; 206 207 /** 208 * Expected HTTP response code to the request. 209 */ 210 unsigned int expected_response_code; 211 212 /** 213 * Was this command modified via 214 * #TALER_TESTING_cmd_withdraw_with_retry to 215 * enable retries? How often should we still retry? 216 */ 217 unsigned int do_retry; 218 }; 219 220 221 /** 222 * Run the command. 223 * 224 * @param cls closure. 225 * @param cmd the commaind being run. 226 * @param is interpreter state. 227 */ 228 static void 229 withdraw_run (void *cls, 230 const struct TALER_TESTING_Command *cmd, 231 struct TALER_TESTING_Interpreter *is); 232 233 234 /** 235 * Task scheduled to re-try #withdraw_run. 236 * 237 * @param cls a `struct WithdrawState` 238 */ 239 static void 240 do_retry (void *cls) 241 { 242 struct WithdrawState *ws = cls; 243 244 ws->retry_task = NULL; 245 TALER_TESTING_touch_cmd (ws->is); 246 withdraw_run (ws, 247 NULL, 248 ws->is); 249 } 250 251 252 /** 253 * "reserve withdraw" operation callback; checks that the 254 * response code is expected and store the exchange signature 255 * in the state. 256 * 257 * @param cls closure. 258 * @param wr withdraw response details 259 */ 260 static void 261 withdraw_cb (void *cls, 262 const struct TALER_EXCHANGE_WithdrawResponse *wr) 263 { 264 struct WithdrawState *ws = cls; 265 struct TALER_TESTING_Interpreter *is = ws->is; 266 267 ws->wsh = NULL; 268 if (ws->expected_response_code != wr->hr.http_status) 269 { 270 if (0 != ws->do_retry) 271 { 272 if (TALER_EC_EXCHANGE_GENERIC_RESERVE_UNKNOWN != wr->hr.ec) 273 ws->do_retry--; /* we don't count reserve unknown as failures here */ 274 if ( (0 == wr->hr.http_status) || 275 (TALER_EC_GENERIC_DB_SOFT_FAILURE == wr->hr.ec) || 276 (TALER_EC_EXCHANGE_WITHDRAW_INSUFFICIENT_FUNDS == wr->hr.ec) || 277 (TALER_EC_EXCHANGE_GENERIC_RESERVE_UNKNOWN == wr->hr.ec) || 278 (MHD_HTTP_INTERNAL_SERVER_ERROR == wr->hr.http_status) ) 279 { 280 GNUNET_log (GNUNET_ERROR_TYPE_INFO, 281 "Retrying withdraw failed with %u/%d\n", 282 wr->hr.http_status, 283 (int) wr->hr.ec); 284 /* on DB conflicts, do not use backoff */ 285 if (TALER_EC_GENERIC_DB_SOFT_FAILURE == wr->hr.ec) 286 ws->backoff = GNUNET_TIME_UNIT_ZERO; 287 else if (TALER_EC_EXCHANGE_GENERIC_RESERVE_UNKNOWN != wr->hr.ec) 288 ws->backoff = EXCHANGE_LIB_BACKOFF (ws->backoff); 289 else 290 ws->backoff = GNUNET_TIME_relative_max (UNKNOWN_MIN_BACKOFF, 291 ws->backoff); 292 ws->backoff = GNUNET_TIME_relative_min (ws->backoff, 293 UNKNOWN_MAX_BACKOFF); 294 ws->total_backoff = GNUNET_TIME_relative_add (ws->total_backoff, 295 ws->backoff); 296 TALER_TESTING_inc_tries (ws->is); 297 ws->retry_task = GNUNET_SCHEDULER_add_delayed (ws->backoff, 298 &do_retry, 299 ws); 300 return; 301 } 302 } 303 TALER_TESTING_unexpected_status_with_body (is, 304 wr->hr.http_status, 305 ws->expected_response_code, 306 wr->hr.reply); 307 return; 308 } 309 switch (wr->hr.http_status) 310 { 311 case MHD_HTTP_OK: 312 GNUNET_assert (1 == wr->details.ok.num_sigs); 313 TALER_denom_sig_copy (&ws->sig, 314 &wr->details.ok.coin_details[0].denom_sig); 315 ws->coin_priv = wr->details.ok.coin_details[0].coin_priv; 316 ws->bks = wr->details.ok.coin_details[0].blinding_key; 317 TALER_denom_ewv_copy (&ws->exchange_vals, 318 &wr->details.ok.coin_details[0].blinding_values); 319 ws->planchets_h = wr->details.ok.planchets_h; 320 if (0<ws->age) 321 { 322 /* copy the age-commitment data */ 323 ws->h_age_commitment = wr->details.ok.coin_details[0].h_age_commitment; 324 TALER_age_commitment_proof_deep_copy ( 325 &ws->age_commitment_proof, 326 &wr->details.ok.coin_details[0].age_commitment_proof); 327 } 328 329 if (0 != ws->total_backoff.rel_value_us) 330 { 331 GNUNET_log (GNUNET_ERROR_TYPE_INFO, 332 "Total withdraw backoff for %s was %s\n", 333 ws->cmd->label, 334 GNUNET_STRINGS_relative_time_to_string (ws->total_backoff, 335 true)); 336 } 337 break; 338 case MHD_HTTP_FORBIDDEN: 339 /* nothing to check */ 340 break; 341 case MHD_HTTP_NOT_FOUND: 342 /* nothing to check */ 343 break; 344 case MHD_HTTP_CONFLICT: 345 /* nothing to check */ 346 break; 347 case MHD_HTTP_GONE: 348 /* theoretically could check that the key was actually */ 349 break; 350 case MHD_HTTP_UNAVAILABLE_FOR_LEGAL_REASONS: 351 /* KYC required */ 352 ws->requirement_row = 353 wr->details.unavailable_for_legal_reasons.requirement_row; 354 ws->h_payto 355 = wr->details.unavailable_for_legal_reasons.h_payto; 356 break; 357 default: 358 /* Unsupported status code (by test harness) */ 359 GNUNET_log (GNUNET_ERROR_TYPE_WARNING, 360 "Withdraw test command does not support status code %u\n", 361 wr->hr.http_status); 362 GNUNET_break (0); 363 break; 364 } 365 TALER_TESTING_interpreter_next (is); 366 } 367 368 369 /** 370 * Run the command. 371 */ 372 static void 373 withdraw_run (void *cls, 374 const struct TALER_TESTING_Command *cmd, 375 struct TALER_TESTING_Interpreter *is) 376 { 377 struct WithdrawState *ws = cls; 378 const struct TALER_ReservePrivateKeyP *rp; 379 const struct TALER_TESTING_Command *create_reserve; 380 const struct TALER_EXCHANGE_DenomPublicKey *dpk; 381 382 if (NULL != cmd) 383 ws->cmd = cmd; 384 ws->is = is; 385 create_reserve 386 = TALER_TESTING_interpreter_lookup_command ( 387 is, 388 ws->reserve_reference); 389 if (NULL == create_reserve) 390 { 391 GNUNET_break (0); 392 TALER_TESTING_interpreter_fail (is); 393 return; 394 } 395 if (GNUNET_OK != 396 TALER_TESTING_get_trait_reserve_priv (create_reserve, 397 &rp)) 398 { 399 GNUNET_break (0); 400 TALER_TESTING_interpreter_fail (is); 401 return; 402 } 403 if (NULL == ws->exchange_url) 404 ws->exchange_url 405 = GNUNET_strdup (TALER_TESTING_get_exchange_url (is)); 406 ws->reserve_priv = *rp; 407 GNUNET_CRYPTO_eddsa_key_get_public (&ws->reserve_priv.eddsa_priv, 408 &ws->reserve_pub.eddsa_pub); 409 ws->reserve_payto_uri 410 = TALER_reserve_make_payto (ws->exchange_url, 411 &ws->reserve_pub); 412 413 TALER_withdraw_master_seed_setup_random (&ws->seed); 414 TALER_cs_withdraw_seed_to_blinding_seed (&ws->seed, 415 &ws->blinding_seed); 416 417 /** 418 * In case of coin key material reuse, we _only_ reuse the 419 * master seed, but the blinding seed is still randomly chosen, 420 * see the lines prior to this. 421 */ 422 if (NULL != ws->reuse_coin_key_ref) 423 { 424 const struct TALER_WithdrawMasterSeedP *seed; 425 const struct TALER_TESTING_Command *cref; 426 char *cstr; 427 unsigned int index; 428 429 GNUNET_assert (GNUNET_OK == 430 TALER_TESTING_parse_coin_reference ( 431 ws->reuse_coin_key_ref, 432 &cstr, 433 &index)); 434 cref = TALER_TESTING_interpreter_lookup_command (is, 435 cstr); 436 GNUNET_assert (NULL != cref); 437 GNUNET_free (cstr); 438 GNUNET_assert (GNUNET_OK == 439 TALER_TESTING_get_trait_withdraw_seed (cref, 440 &seed)); 441 ws->seed = *seed; 442 443 if (ws->reuse_blinding_seed) 444 TALER_cs_withdraw_seed_to_blinding_seed (&ws->seed, 445 &ws->blinding_seed); 446 } 447 448 if (NULL == ws->pk) 449 { 450 dpk = TALER_TESTING_find_pk (TALER_TESTING_get_keys (is), 451 &ws->amount, 452 ws->age > 0); 453 if (NULL == dpk) 454 { 455 GNUNET_log (GNUNET_ERROR_TYPE_ERROR, 456 "Failed to determine denomination key at %s\n", 457 (NULL != cmd) ? cmd->label : "<retried command>"); 458 GNUNET_break (0); 459 TALER_TESTING_interpreter_fail (is); 460 return; 461 } 462 /* We copy the denomination key, as re-querying /keys 463 * would free the old one. */ 464 ws->pk = TALER_EXCHANGE_copy_denomination_key (dpk); 465 } 466 else 467 { 468 ws->amount = ws->pk->value; 469 } 470 471 ws->reserve_history.type = TALER_EXCHANGE_RTT_WITHDRAWAL; 472 GNUNET_assert (0 <= 473 TALER_amount_add (&ws->reserve_history.amount, 474 &ws->amount, 475 &ws->pk->fees.withdraw)); 476 ws->reserve_history.details.withdraw.fee = 477 ws->pk->fees.withdraw; 478 479 ws->wsh = TALER_EXCHANGE_withdraw_extra_blinding_seed ( 480 TALER_TESTING_interpreter_get_context (is), 481 TALER_TESTING_get_keys (is), 482 TALER_TESTING_get_exchange_url (is), 483 rp, 484 1, 485 ws->pk, 486 &ws->seed, 487 &ws->blinding_seed, 488 ws->age, 489 &withdraw_cb, 490 ws); 491 492 if (NULL == ws->wsh) 493 { 494 GNUNET_break (0); 495 TALER_TESTING_interpreter_fail (is); 496 return; 497 } 498 } 499 500 501 /** 502 * Free the state of a "withdraw" CMD, and possibly cancel 503 * a pending operation thereof. 504 * 505 * @param cls closure. 506 * @param cmd the command being freed. 507 */ 508 static void 509 withdraw_cleanup (void *cls, 510 const struct TALER_TESTING_Command *cmd) 511 { 512 struct WithdrawState *ws = cls; 513 514 if (NULL != ws->wsh) 515 { 516 TALER_TESTING_command_incomplete (ws->is, 517 cmd->label); 518 TALER_EXCHANGE_withdraw_cancel (ws->wsh); 519 ws->wsh = NULL; 520 } 521 if (NULL != ws->retry_task) 522 { 523 GNUNET_SCHEDULER_cancel (ws->retry_task); 524 ws->retry_task = NULL; 525 } 526 TALER_denom_sig_free (&ws->sig); 527 TALER_denom_ewv_free (&ws->exchange_vals); 528 if (NULL != ws->pk) 529 { 530 TALER_EXCHANGE_destroy_denomination_key (ws->pk); 531 ws->pk = NULL; 532 } 533 if (ws->age > 0) 534 TALER_age_commitment_proof_free (&ws->age_commitment_proof); 535 GNUNET_free (ws->exchange_url); 536 GNUNET_free (ws->reserve_payto_uri.normalized_payto); 537 GNUNET_free (ws); 538 } 539 540 541 /** 542 * Offer internal data to a "withdraw" CMD state to other 543 * commands. 544 * 545 * @param cls closure 546 * @param[out] ret result (could be anything) 547 * @param trait name of the trait 548 * @param index index number of the object to offer. 549 * @return #GNUNET_OK on success 550 */ 551 static enum GNUNET_GenericReturnValue 552 withdraw_traits (void *cls, 553 const void **ret, 554 const char *trait, 555 unsigned int index) 556 { 557 struct WithdrawState *ws = cls; 558 struct TALER_TESTING_Trait traits[] = { 559 /* history entry MUST be first due to response code logic below! */ 560 TALER_TESTING_make_trait_reserve_history (0 /* only one coin */, 561 &ws->reserve_history), 562 TALER_TESTING_make_trait_coin_priv (0 /* only one coin */, 563 &ws->coin_priv), 564 TALER_TESTING_make_trait_withdraw_seed (&ws->seed), 565 TALER_TESTING_make_trait_blinding_seed (&ws->blinding_seed), 566 TALER_TESTING_make_trait_withdraw_commitment (&ws->planchets_h), 567 TALER_TESTING_make_trait_blinding_key (0 /* only one coin */, 568 &ws->bks), 569 TALER_TESTING_make_trait_exchange_blinding_values (0 /* only one coin */, 570 &ws->exchange_vals), 571 TALER_TESTING_make_trait_denom_pub (0 /* only one coin */, 572 ws->pk), 573 TALER_TESTING_make_trait_denom_sig (0 /* only one coin */, 574 &ws->sig), 575 TALER_TESTING_make_trait_reserve_priv (&ws->reserve_priv), 576 TALER_TESTING_make_trait_reserve_pub (&ws->reserve_pub), 577 TALER_TESTING_make_trait_amount (&ws->amount), 578 TALER_TESTING_make_trait_legi_requirement_row (&ws->requirement_row), 579 TALER_TESTING_make_trait_h_normalized_payto (&ws->h_payto), 580 TALER_TESTING_make_trait_normalized_payto_uri (&ws->reserve_payto_uri), 581 TALER_TESTING_make_trait_exchange_url (ws->exchange_url), 582 TALER_TESTING_make_trait_age_commitment_proof (0, 583 0 < ws->age 584 ? &ws->age_commitment_proof 585 : NULL), 586 TALER_TESTING_make_trait_h_age_commitment (0, 587 0 < ws->age 588 ? &ws->h_age_commitment 589 : NULL), 590 TALER_TESTING_trait_end () 591 }; 592 593 return TALER_TESTING_get_trait ((ws->expected_response_code == MHD_HTTP_OK) 594 ? &traits[0] /* we have reserve history */ 595 : &traits[1], /* skip reserve history */ 596 ret, 597 trait, 598 index); 599 } 600 601 602 struct TALER_TESTING_Command 603 TALER_TESTING_cmd_withdraw_amount (const char *label, 604 const char *reserve_reference, 605 const char *amount, 606 uint8_t age, 607 unsigned int expected_response_code) 608 { 609 struct WithdrawState *ws; 610 611 ws = GNUNET_new (struct WithdrawState); 612 ws->age = age; 613 ws->reserve_reference = reserve_reference; 614 if (GNUNET_OK != 615 TALER_string_to_amount (amount, 616 &ws->amount)) 617 { 618 GNUNET_log (GNUNET_ERROR_TYPE_ERROR, 619 "Failed to parse amount `%s' at %s\n", 620 amount, 621 label); 622 GNUNET_assert (0); 623 } 624 ws->expected_response_code = expected_response_code; 625 { 626 struct TALER_TESTING_Command cmd = { 627 .cls = ws, 628 .label = label, 629 .run = &withdraw_run, 630 .cleanup = &withdraw_cleanup, 631 .traits = &withdraw_traits 632 }; 633 634 return cmd; 635 } 636 } 637 638 639 struct TALER_TESTING_Command 640 TALER_TESTING_cmd_withdraw_amount_reuse_key ( 641 const char *label, 642 const char *reserve_reference, 643 const char *amount, 644 uint8_t age, 645 const char *coin_ref, 646 unsigned int expected_response_code) 647 { 648 struct TALER_TESTING_Command cmd; 649 650 cmd = TALER_TESTING_cmd_withdraw_amount (label, 651 reserve_reference, 652 amount, 653 age, 654 expected_response_code); 655 { 656 struct WithdrawState *ws = cmd.cls; 657 658 ws->reuse_coin_key_ref = coin_ref; 659 } 660 return cmd; 661 } 662 663 664 struct TALER_TESTING_Command 665 TALER_TESTING_cmd_withdraw_amount_reuse_all_secrets ( 666 const char *label, 667 const char *reserve_reference, 668 const char *amount, 669 uint8_t age, 670 const char *coin_ref, 671 unsigned int expected_response_code) 672 { 673 struct TALER_TESTING_Command cmd; 674 675 cmd = TALER_TESTING_cmd_withdraw_amount (label, 676 reserve_reference, 677 amount, 678 age, 679 expected_response_code); 680 { 681 struct WithdrawState *ws = cmd.cls; 682 683 ws->reuse_coin_key_ref = coin_ref; 684 ws->reuse_blinding_seed = true; 685 } 686 return cmd; 687 } 688 689 690 struct TALER_TESTING_Command 691 TALER_TESTING_cmd_withdraw_denomination ( 692 const char *label, 693 const char *reserve_reference, 694 const struct TALER_EXCHANGE_DenomPublicKey *dk, 695 unsigned int expected_response_code) 696 { 697 struct WithdrawState *ws; 698 699 if (NULL == dk) 700 { 701 GNUNET_log (GNUNET_ERROR_TYPE_ERROR, 702 "Denomination key not specified at %s\n", 703 label); 704 GNUNET_assert (0); 705 } 706 ws = GNUNET_new (struct WithdrawState); 707 ws->reserve_reference = reserve_reference; 708 ws->pk = TALER_EXCHANGE_copy_denomination_key (dk); 709 ws->expected_response_code = expected_response_code; 710 { 711 struct TALER_TESTING_Command cmd = { 712 .cls = ws, 713 .label = label, 714 .run = &withdraw_run, 715 .cleanup = &withdraw_cleanup, 716 .traits = &withdraw_traits 717 }; 718 719 return cmd; 720 } 721 } 722 723 724 struct TALER_TESTING_Command 725 TALER_TESTING_cmd_withdraw_with_retry (struct TALER_TESTING_Command cmd) 726 { 727 struct WithdrawState *ws; 728 729 GNUNET_assert (&withdraw_run == cmd.run); 730 ws = cmd.cls; 731 ws->do_retry = NUM_RETRIES; 732 return cmd; 733 } 734 735 736 /* end of testing_api_cmd_withdraw.c */