testing_api_cmd_withdraw.c (22350B)
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 * Public key of the coin. 122 */ 123 struct TALER_CoinSpendPublicKeyP coin_pub; 124 125 /** 126 * Blinding key used during the operation. 127 */ 128 union GNUNET_CRYPTO_BlindingSecretP bks; 129 130 /** 131 * Values contributed from the exchange during the 132 * withdraw protocol. 133 */ 134 struct TALER_ExchangeBlindingValues exchange_vals; 135 136 /** 137 * Interpreter state (during command). 138 */ 139 struct TALER_TESTING_Interpreter *is; 140 141 /** 142 * Set (by the interpreter) to the exchange's signature over the 143 * coin's public key. 144 */ 145 struct TALER_DenominationSignature sig; 146 147 /** 148 * Seed for the key material of the coin, set by the interpreter. 149 */ 150 struct TALER_WithdrawMasterSeedP seed; 151 152 /** 153 * Blinding seed for the blinding preparation for CS. 154 */ 155 struct TALER_BlindingMasterSeedP blinding_seed; 156 157 /** 158 * An age > 0 signifies age restriction is required 159 */ 160 uint8_t age; 161 162 /** 163 * If age > 0, put here the corresponding age commitment with its proof and 164 * its hash, respectively. 165 */ 166 struct TALER_AgeCommitmentProof age_commitment_proof; 167 struct TALER_AgeCommitmentHashP h_age_commitment; 168 169 /** 170 * Reserve history entry that corresponds to this operation. 171 * Will be of type #TALER_EXCHANGE_RTT_WITHDRAWAL. 172 */ 173 struct TALER_EXCHANGE_ReserveHistoryEntry reserve_history; 174 175 /** 176 * Withdraw handle (while operation is running). 177 */ 178 struct TALER_EXCHANGE_PostWithdrawHandle *wsh; 179 180 /** 181 * The commitment for the withdraw operation, later needed for /recoup 182 */ 183 struct TALER_HashBlindedPlanchetsP planchets_h; 184 185 /** 186 * Task scheduled to try later. 187 */ 188 struct GNUNET_SCHEDULER_Task *retry_task; 189 190 /** 191 * How long do we wait until we retry? 192 */ 193 struct GNUNET_TIME_Relative backoff; 194 195 /** 196 * Total withdraw backoff applied. 197 */ 198 struct GNUNET_TIME_Relative total_backoff; 199 200 /** 201 * Set to the KYC requirement payto hash *if* the exchange replied with a 202 * request for KYC. 203 */ 204 struct TALER_NormalizedPaytoHashP h_payto; 205 206 /** 207 * Set to the KYC requirement row *if* the exchange replied with 208 * a request for KYC. 209 */ 210 uint64_t requirement_row; 211 212 /** 213 * Expected HTTP response code to the request. 214 */ 215 unsigned int expected_response_code; 216 217 /** 218 * Was this command modified via 219 * #TALER_TESTING_cmd_withdraw_with_retry to 220 * enable retries? How often should we still retry? 221 */ 222 unsigned int do_retry; 223 }; 224 225 226 /** 227 * Run the command. 228 * 229 * @param cls closure. 230 * @param cmd the commaind being run. 231 * @param is interpreter state. 232 */ 233 static void 234 withdraw_run (void *cls, 235 const struct TALER_TESTING_Command *cmd, 236 struct TALER_TESTING_Interpreter *is); 237 238 239 /** 240 * Task scheduled to re-try #withdraw_run. 241 * 242 * @param cls a `struct WithdrawState` 243 */ 244 static void 245 do_retry (void *cls) 246 { 247 struct WithdrawState *ws = cls; 248 249 ws->retry_task = NULL; 250 TALER_TESTING_touch_cmd (ws->is); 251 withdraw_run (ws, 252 NULL, 253 ws->is); 254 } 255 256 257 /** 258 * "reserve withdraw" operation callback; checks that the 259 * response code is expected and store the exchange signature 260 * in the state. 261 * 262 * @param cls closure. 263 * @param wr withdraw response details 264 */ 265 static void 266 withdraw_cb (void *cls, 267 const struct TALER_EXCHANGE_PostWithdrawResponse *wr) 268 { 269 struct WithdrawState *ws = cls; 270 struct TALER_TESTING_Interpreter *is = ws->is; 271 272 ws->wsh = NULL; 273 if (ws->expected_response_code != wr->hr.http_status) 274 { 275 if (0 != ws->do_retry) 276 { 277 if (TALER_EC_EXCHANGE_GENERIC_RESERVE_UNKNOWN != wr->hr.ec) 278 ws->do_retry--; /* we don't count reserve unknown as failures here */ 279 if ( (0 == wr->hr.http_status) || 280 (TALER_EC_GENERIC_DB_SOFT_FAILURE == wr->hr.ec) || 281 (TALER_EC_EXCHANGE_WITHDRAW_INSUFFICIENT_FUNDS == wr->hr.ec) || 282 (TALER_EC_EXCHANGE_GENERIC_RESERVE_UNKNOWN == wr->hr.ec) || 283 (MHD_HTTP_INTERNAL_SERVER_ERROR == wr->hr.http_status) ) 284 { 285 GNUNET_log (GNUNET_ERROR_TYPE_INFO, 286 "Retrying withdraw failed with %u/%d\n", 287 wr->hr.http_status, 288 (int) wr->hr.ec); 289 /* on DB conflicts, do not use backoff */ 290 if (TALER_EC_GENERIC_DB_SOFT_FAILURE == wr->hr.ec) 291 ws->backoff = GNUNET_TIME_UNIT_ZERO; 292 else if (TALER_EC_EXCHANGE_GENERIC_RESERVE_UNKNOWN != wr->hr.ec) 293 ws->backoff = EXCHANGE_LIB_BACKOFF (ws->backoff); 294 else 295 ws->backoff = GNUNET_TIME_relative_max (UNKNOWN_MIN_BACKOFF, 296 ws->backoff); 297 ws->backoff = GNUNET_TIME_relative_min (ws->backoff, 298 UNKNOWN_MAX_BACKOFF); 299 ws->total_backoff = GNUNET_TIME_relative_add (ws->total_backoff, 300 ws->backoff); 301 TALER_TESTING_inc_tries (ws->is); 302 ws->retry_task = GNUNET_SCHEDULER_add_delayed (ws->backoff, 303 &do_retry, 304 ws); 305 return; 306 } 307 } 308 TALER_TESTING_unexpected_status_with_body (is, 309 wr->hr.http_status, 310 ws->expected_response_code, 311 wr->hr.reply); 312 return; 313 } 314 switch (wr->hr.http_status) 315 { 316 case MHD_HTTP_OK: 317 GNUNET_assert (1 == wr->details.ok.num_sigs); 318 TALER_denom_sig_copy (&ws->sig, 319 &wr->details.ok.coin_details[0].denom_sig); 320 ws->coin_priv = wr->details.ok.coin_details[0].coin_priv; 321 GNUNET_CRYPTO_eddsa_key_get_public (&ws->coin_priv.eddsa_priv, 322 &ws->coin_pub.eddsa_pub); 323 ws->bks = wr->details.ok.coin_details[0].blinding_key; 324 TALER_denom_ewv_copy (&ws->exchange_vals, 325 &wr->details.ok.coin_details[0].blinding_values); 326 ws->planchets_h = wr->details.ok.planchets_h; 327 if (0<ws->age) 328 { 329 /* copy the age-commitment data */ 330 ws->h_age_commitment = wr->details.ok.coin_details[0].h_age_commitment; 331 TALER_age_commitment_proof_deep_copy ( 332 &ws->age_commitment_proof, 333 &wr->details.ok.coin_details[0].age_commitment_proof); 334 } 335 336 if (0 != ws->total_backoff.rel_value_us) 337 { 338 GNUNET_log (GNUNET_ERROR_TYPE_INFO, 339 "Total withdraw backoff for %s was %s\n", 340 ws->cmd->label, 341 GNUNET_STRINGS_relative_time_to_string (ws->total_backoff, 342 true)); 343 } 344 break; 345 case MHD_HTTP_FORBIDDEN: 346 /* nothing to check */ 347 break; 348 case MHD_HTTP_NOT_FOUND: 349 /* nothing to check */ 350 break; 351 case MHD_HTTP_CONFLICT: 352 /* nothing to check */ 353 break; 354 case MHD_HTTP_GONE: 355 /* theoretically could check that the key was actually */ 356 break; 357 case MHD_HTTP_UNAVAILABLE_FOR_LEGAL_REASONS: 358 /* KYC required */ 359 ws->requirement_row = 360 wr->details.unavailable_for_legal_reasons.requirement_row; 361 ws->h_payto 362 = wr->details.unavailable_for_legal_reasons.h_payto; 363 break; 364 default: 365 /* Unsupported status code (by test harness) */ 366 GNUNET_log (GNUNET_ERROR_TYPE_WARNING, 367 "Withdraw test command does not support status code %u\n", 368 wr->hr.http_status); 369 GNUNET_break (0); 370 break; 371 } 372 TALER_TESTING_interpreter_next (is); 373 } 374 375 376 /** 377 * Run the command. 378 */ 379 static void 380 withdraw_run (void *cls, 381 const struct TALER_TESTING_Command *cmd, 382 struct TALER_TESTING_Interpreter *is) 383 { 384 struct WithdrawState *ws = cls; 385 const struct TALER_ReservePrivateKeyP *rp; 386 const struct TALER_TESTING_Command *create_reserve; 387 const struct TALER_EXCHANGE_DenomPublicKey *dpk; 388 389 if (NULL != cmd) 390 ws->cmd = cmd; 391 ws->is = is; 392 create_reserve 393 = TALER_TESTING_interpreter_lookup_command ( 394 is, 395 ws->reserve_reference); 396 if (NULL == create_reserve) 397 { 398 GNUNET_break (0); 399 TALER_TESTING_interpreter_fail (is); 400 return; 401 } 402 if (GNUNET_OK != 403 TALER_TESTING_get_trait_reserve_priv (create_reserve, 404 &rp)) 405 { 406 GNUNET_break (0); 407 TALER_TESTING_interpreter_fail (is); 408 return; 409 } 410 if (NULL == ws->exchange_url) 411 ws->exchange_url 412 = GNUNET_strdup (TALER_TESTING_get_exchange_url (is)); 413 ws->reserve_priv = *rp; 414 GNUNET_CRYPTO_eddsa_key_get_public (&ws->reserve_priv.eddsa_priv, 415 &ws->reserve_pub.eddsa_pub); 416 ws->reserve_payto_uri 417 = TALER_reserve_make_payto (ws->exchange_url, 418 &ws->reserve_pub); 419 420 TALER_withdraw_master_seed_setup_random (&ws->seed); 421 TALER_cs_withdraw_seed_to_blinding_seed (&ws->seed, 422 &ws->blinding_seed); 423 424 /** 425 * In case of coin key material reuse, we _only_ reuse the 426 * master seed, but the blinding seed is still randomly chosen, 427 * see the lines prior to this. 428 */ 429 if (NULL != ws->reuse_coin_key_ref) 430 { 431 const struct TALER_WithdrawMasterSeedP *seed; 432 const struct TALER_TESTING_Command *cref; 433 char *cstr; 434 unsigned int index; 435 436 GNUNET_assert (GNUNET_OK == 437 TALER_TESTING_parse_coin_reference ( 438 ws->reuse_coin_key_ref, 439 &cstr, 440 &index)); 441 cref = TALER_TESTING_interpreter_lookup_command (is, 442 cstr); 443 GNUNET_assert (NULL != cref); 444 GNUNET_free (cstr); 445 GNUNET_assert (GNUNET_OK == 446 TALER_TESTING_get_trait_withdraw_seed (cref, 447 &seed)); 448 ws->seed = *seed; 449 450 if (ws->reuse_blinding_seed) 451 TALER_cs_withdraw_seed_to_blinding_seed (&ws->seed, 452 &ws->blinding_seed); 453 } 454 455 if (NULL == ws->pk) 456 { 457 dpk = TALER_TESTING_find_pk (TALER_TESTING_get_keys (is), 458 &ws->amount, 459 ws->age > 0); 460 if (NULL == dpk) 461 { 462 GNUNET_log (GNUNET_ERROR_TYPE_ERROR, 463 "Failed to determine denomination key at %s\n", 464 (NULL != cmd) ? cmd->label : "<retried command>"); 465 GNUNET_break (0); 466 TALER_TESTING_interpreter_fail (is); 467 return; 468 } 469 /* We copy the denomination key, as re-querying /keys 470 * would free the old one. */ 471 ws->pk = TALER_EXCHANGE_copy_denomination_key (dpk); 472 } 473 else 474 { 475 ws->amount = ws->pk->value; 476 } 477 478 ws->reserve_history.type = TALER_EXCHANGE_RTT_WITHDRAWAL; 479 GNUNET_assert (0 <= 480 TALER_amount_add (&ws->reserve_history.amount, 481 &ws->amount, 482 &ws->pk->fees.withdraw)); 483 ws->reserve_history.details.withdraw.fee = 484 ws->pk->fees.withdraw; 485 486 ws->wsh = TALER_EXCHANGE_post_withdraw_create ( 487 TALER_TESTING_interpreter_get_context (is), 488 TALER_TESTING_get_exchange_url (is), 489 TALER_TESTING_get_keys (is), 490 rp, 491 1, 492 ws->pk, 493 &ws->seed, 494 ws->age); 495 if (NULL == ws->wsh) 496 { 497 GNUNET_break (0); 498 TALER_TESTING_interpreter_fail (is); 499 return; 500 } 501 GNUNET_assert (GNUNET_OK == 502 TALER_EXCHANGE_post_withdraw_set_options ( 503 ws->wsh, 504 TALER_EXCHANGE_post_withdraw_option_blinding_seed ( 505 &ws->blinding_seed))); 506 GNUNET_assert (TALER_EC_NONE == 507 TALER_EXCHANGE_post_withdraw_start (ws->wsh, 508 &withdraw_cb, 509 ws)); 510 } 511 512 513 /** 514 * Free the state of a "withdraw" CMD, and possibly cancel 515 * a pending operation thereof. 516 * 517 * @param cls closure. 518 * @param cmd the command being freed. 519 */ 520 static void 521 withdraw_cleanup (void *cls, 522 const struct TALER_TESTING_Command *cmd) 523 { 524 struct WithdrawState *ws = cls; 525 526 if (NULL != ws->wsh) 527 { 528 TALER_TESTING_command_incomplete (ws->is, 529 cmd->label); 530 TALER_EXCHANGE_post_withdraw_cancel (ws->wsh); 531 ws->wsh = NULL; 532 } 533 if (NULL != ws->retry_task) 534 { 535 GNUNET_SCHEDULER_cancel (ws->retry_task); 536 ws->retry_task = NULL; 537 } 538 TALER_denom_sig_free (&ws->sig); 539 TALER_denom_ewv_free (&ws->exchange_vals); 540 if (NULL != ws->pk) 541 { 542 TALER_EXCHANGE_destroy_denomination_key (ws->pk); 543 ws->pk = NULL; 544 } 545 if (ws->age > 0) 546 TALER_age_commitment_proof_free (&ws->age_commitment_proof); 547 GNUNET_free (ws->exchange_url); 548 GNUNET_free (ws->reserve_payto_uri.normalized_payto); 549 GNUNET_free (ws); 550 } 551 552 553 /** 554 * Offer internal data to a "withdraw" CMD state to other 555 * commands. 556 * 557 * @param cls closure 558 * @param[out] ret result (could be anything) 559 * @param trait name of the trait 560 * @param index index number of the object to offer. 561 * @return #GNUNET_OK on success 562 */ 563 static enum GNUNET_GenericReturnValue 564 withdraw_traits (void *cls, 565 const void **ret, 566 const char *trait, 567 unsigned int index) 568 { 569 struct WithdrawState *ws = cls; 570 struct TALER_TESTING_Trait traits[] = { 571 /* history entry MUST be first due to response code logic below! */ 572 TALER_TESTING_make_trait_reserve_history (0 /* only one coin */, 573 &ws->reserve_history), 574 TALER_TESTING_make_trait_coin_priv (0 /* only one coin */, 575 &ws->coin_priv), 576 TALER_TESTING_make_trait_coin_pub (0 /* only one coin */, 577 &ws->coin_pub), 578 TALER_TESTING_make_trait_withdraw_seed (&ws->seed), 579 TALER_TESTING_make_trait_blinding_seed (&ws->blinding_seed), 580 TALER_TESTING_make_trait_withdraw_commitment (&ws->planchets_h), 581 TALER_TESTING_make_trait_blinding_key (0 /* only one coin */, 582 &ws->bks), 583 TALER_TESTING_make_trait_exchange_blinding_values (0 /* only one coin */, 584 &ws->exchange_vals), 585 TALER_TESTING_make_trait_denom_pub (0 /* only one coin */, 586 ws->pk), 587 TALER_TESTING_make_trait_denom_sig (0 /* only one coin */, 588 &ws->sig), 589 TALER_TESTING_make_trait_reserve_priv (&ws->reserve_priv), 590 TALER_TESTING_make_trait_reserve_pub (&ws->reserve_pub), 591 TALER_TESTING_make_trait_amount (&ws->amount), 592 TALER_TESTING_make_trait_legi_requirement_row (&ws->requirement_row), 593 TALER_TESTING_make_trait_h_normalized_payto (&ws->h_payto), 594 TALER_TESTING_make_trait_normalized_payto_uri (&ws->reserve_payto_uri), 595 TALER_TESTING_make_trait_exchange_url (ws->exchange_url), 596 TALER_TESTING_make_trait_age_commitment_proof (0, 597 0 < ws->age 598 ? &ws->age_commitment_proof 599 : NULL), 600 TALER_TESTING_make_trait_h_age_commitment (0, 601 0 < ws->age 602 ? &ws->h_age_commitment 603 : NULL), 604 TALER_TESTING_trait_end () 605 }; 606 607 return TALER_TESTING_get_trait ((ws->expected_response_code == MHD_HTTP_OK) 608 ? &traits[0] /* we have reserve history */ 609 : &traits[1], /* skip reserve history */ 610 ret, 611 trait, 612 index); 613 } 614 615 616 struct TALER_TESTING_Command 617 TALER_TESTING_cmd_withdraw_amount (const char *label, 618 const char *reserve_reference, 619 const char *amount, 620 uint8_t age, 621 unsigned int expected_response_code) 622 { 623 struct WithdrawState *ws; 624 625 ws = GNUNET_new (struct WithdrawState); 626 ws->age = age; 627 ws->reserve_reference = reserve_reference; 628 if (GNUNET_OK != 629 TALER_string_to_amount (amount, 630 &ws->amount)) 631 { 632 GNUNET_log (GNUNET_ERROR_TYPE_ERROR, 633 "Failed to parse amount `%s' at %s\n", 634 amount, 635 label); 636 GNUNET_assert (0); 637 } 638 ws->expected_response_code = expected_response_code; 639 { 640 struct TALER_TESTING_Command cmd = { 641 .cls = ws, 642 .label = label, 643 .run = &withdraw_run, 644 .cleanup = &withdraw_cleanup, 645 .traits = &withdraw_traits 646 }; 647 648 return cmd; 649 } 650 } 651 652 653 struct TALER_TESTING_Command 654 TALER_TESTING_cmd_withdraw_amount_reuse_key ( 655 const char *label, 656 const char *reserve_reference, 657 const char *amount, 658 uint8_t age, 659 const char *coin_ref, 660 unsigned int expected_response_code) 661 { 662 struct TALER_TESTING_Command cmd; 663 664 cmd = TALER_TESTING_cmd_withdraw_amount (label, 665 reserve_reference, 666 amount, 667 age, 668 expected_response_code); 669 { 670 struct WithdrawState *ws = cmd.cls; 671 672 ws->reuse_coin_key_ref = coin_ref; 673 } 674 return cmd; 675 } 676 677 678 struct TALER_TESTING_Command 679 TALER_TESTING_cmd_withdraw_amount_reuse_all_secrets ( 680 const char *label, 681 const char *reserve_reference, 682 const char *amount, 683 uint8_t age, 684 const char *coin_ref, 685 unsigned int expected_response_code) 686 { 687 struct TALER_TESTING_Command cmd; 688 689 cmd = TALER_TESTING_cmd_withdraw_amount (label, 690 reserve_reference, 691 amount, 692 age, 693 expected_response_code); 694 { 695 struct WithdrawState *ws = cmd.cls; 696 697 ws->reuse_coin_key_ref = coin_ref; 698 ws->reuse_blinding_seed = true; 699 } 700 return cmd; 701 } 702 703 704 struct TALER_TESTING_Command 705 TALER_TESTING_cmd_withdraw_denomination ( 706 const char *label, 707 const char *reserve_reference, 708 const struct TALER_EXCHANGE_DenomPublicKey *dk, 709 unsigned int expected_response_code) 710 { 711 struct WithdrawState *ws; 712 713 if (NULL == dk) 714 { 715 GNUNET_log (GNUNET_ERROR_TYPE_ERROR, 716 "Denomination key not specified at %s\n", 717 label); 718 GNUNET_assert (0); 719 } 720 ws = GNUNET_new (struct WithdrawState); 721 ws->reserve_reference = reserve_reference; 722 ws->pk = TALER_EXCHANGE_copy_denomination_key (dk); 723 ws->expected_response_code = expected_response_code; 724 { 725 struct TALER_TESTING_Command cmd = { 726 .cls = ws, 727 .label = label, 728 .run = &withdraw_run, 729 .cleanup = &withdraw_cleanup, 730 .traits = &withdraw_traits 731 }; 732 733 return cmd; 734 } 735 } 736 737 738 struct TALER_TESTING_Command 739 TALER_TESTING_cmd_withdraw_with_retry (struct TALER_TESTING_Command cmd) 740 { 741 struct WithdrawState *ws; 742 743 GNUNET_assert (&withdraw_run == cmd.run); 744 ws = cmd.cls; 745 ws->do_retry = NUM_RETRIES; 746 return cmd; 747 } 748 749 750 /* end of testing_api_cmd_withdraw.c */