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