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