testing_api_cmd_batch_deposit.c (22686B)
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_batch_deposit.c 21 * @brief command for testing /batch-deposit. 22 * @author Marcello Stanisci 23 * @author Christian Grothoff 24 */ 25 #include "taler/taler_json_lib.h" 26 #include <gnunet/gnunet_curl_lib.h> 27 struct BatchDepositState; 28 #define TALER_EXCHANGE_POST_BATCH_DEPOSIT_RESULT_CLOSURE \ 29 struct BatchDepositState 30 #include "taler/exchange/post-batch-deposit.h" 31 #include "taler/taler_testing_lib.h" 32 #include "taler/taler_signatures.h" 33 #include "backoff.h" 34 35 36 /** 37 * How often do we retry before giving up? 38 */ 39 #define NUM_RETRIES 5 40 41 /** 42 * How long do we wait AT MOST when retrying? 43 */ 44 #define MAX_BACKOFF GNUNET_TIME_relative_multiply ( \ 45 GNUNET_TIME_UNIT_MILLISECONDS, 100) 46 47 48 /** 49 * Information per coin in the batch. 50 */ 51 struct Coin 52 { 53 54 /** 55 * Amount to deposit. 56 */ 57 struct TALER_Amount amount; 58 59 /** 60 * Deposit fee. 61 */ 62 struct TALER_Amount deposit_fee; 63 64 /** 65 * Our coin signature. 66 */ 67 struct TALER_CoinSpendSignatureP coin_sig; 68 69 /** 70 * Reference to any command that is able to provide a coin, 71 * possibly using $LABEL#$INDEX notation. 72 */ 73 char *coin_reference; 74 75 /** 76 * Denomination public key of the coin. 77 */ 78 const struct TALER_EXCHANGE_DenomPublicKey *denom_pub; 79 80 /** 81 * The command being referenced. 82 */ 83 const struct TALER_TESTING_Command *coin_cmd; 84 85 /** 86 * Expected entry in the coin history created by this 87 * coin. 88 */ 89 struct TALER_EXCHANGE_CoinHistoryEntry che; 90 91 /** 92 * Index of the coin at @e coin_cmd. 93 */ 94 unsigned int coin_idx; 95 }; 96 97 98 /** 99 * State for a "batch deposit" CMD. 100 */ 101 struct BatchDepositState 102 { 103 104 /** 105 * Refund deadline. Zero for no refunds. 106 */ 107 struct GNUNET_TIME_Timestamp refund_deadline; 108 109 /** 110 * Wire deadline. 111 */ 112 struct GNUNET_TIME_Timestamp wire_deadline; 113 114 /** 115 * Timestamp of the /deposit operation in the wallet (contract signing time). 116 */ 117 struct GNUNET_TIME_Timestamp wallet_timestamp; 118 119 /** 120 * How long do we wait until we retry? 121 */ 122 struct GNUNET_TIME_Relative backoff; 123 124 /** 125 * When did the exchange receive the deposit? 126 */ 127 struct GNUNET_TIME_Timestamp exchange_timestamp; 128 129 /** 130 * Signing key used by the exchange to sign the 131 * deposit confirmation. 132 */ 133 struct TALER_ExchangePublicKeyP exchange_pub; 134 135 /** 136 * Set (by the interpreter) to a fresh private key. This 137 * key will be used to sign the deposit request. 138 */ 139 union TALER_AccountPrivateKeyP account_priv; 140 141 /** 142 * Set (by the interpreter) to the public key 143 * corresponding to @e account_priv. 144 */ 145 union TALER_AccountPublicKeyP account_pub; 146 147 /** 148 * Deposit handle while operation is running. 149 */ 150 struct TALER_EXCHANGE_PostBatchDepositHandle *dh; 151 152 /** 153 * Array of coins to batch-deposit. 154 */ 155 struct Coin *coins; 156 157 /** 158 * Wire details of who is depositing -- this would be merchant 159 * wire details in a normal scenario. 160 */ 161 json_t *wire_details; 162 163 /** 164 * JSON string describing what a proposal is about. 165 */ 166 json_t *contract_terms; 167 168 /** 169 * Interpreter state. 170 */ 171 struct TALER_TESTING_Interpreter *is; 172 173 /** 174 * Task scheduled to try later. 175 */ 176 struct GNUNET_SCHEDULER_Task *retry_task; 177 178 /** 179 * Deposit confirmation signature from the exchange. 180 */ 181 struct TALER_ExchangeSignatureP exchange_sig; 182 183 /** 184 * Set to the KYC requirement payto hash *if* the exchange replied with a 185 * request for KYC. 186 */ 187 struct TALER_NormalizedPaytoHashP h_payto; 188 189 /** 190 * Set to the KYC requirement row *if* the exchange replied with 191 * a request for KYC. 192 */ 193 uint64_t requirement_row; 194 195 /** 196 * Reference to previous deposit operation. 197 * Only present if we're supposed to replay the previous deposit. 198 */ 199 const char *deposit_reference; 200 201 /** 202 * If @e coin_reference refers to an operation that generated 203 * an array of coins, this value determines which coin to pick. 204 */ 205 unsigned int num_coins; 206 207 /** 208 * Expected HTTP response code. 209 */ 210 unsigned int expected_response_code; 211 212 /** 213 * Set to true if the /deposit succeeded 214 * and we now can provide the resulting traits. 215 */ 216 bool deposit_succeeded; 217 218 }; 219 220 221 /** 222 * Callback to analyze the /batch-deposit response, just used to check if the 223 * response code is acceptable. 224 * 225 * @param ds closure. 226 * @param dr deposit response details 227 */ 228 static void 229 batch_deposit_cb (struct BatchDepositState *ds, 230 const struct TALER_EXCHANGE_PostBatchDepositResponse *dr) 231 { 232 ds->dh = NULL; 233 if (ds->expected_response_code != dr->hr.http_status) 234 { 235 TALER_TESTING_unexpected_status (ds->is, 236 dr->hr.http_status, 237 ds->expected_response_code); 238 return; 239 } 240 switch (dr->hr.http_status) 241 { 242 case MHD_HTTP_OK: 243 ds->deposit_succeeded = GNUNET_YES; 244 ds->exchange_timestamp = dr->details.ok.deposit_timestamp; 245 ds->exchange_pub = *dr->details.ok.exchange_pub; 246 ds->exchange_sig = *dr->details.ok.exchange_sig; 247 break; 248 case MHD_HTTP_UNAVAILABLE_FOR_LEGAL_REASONS: 249 /* nothing to check */ 250 ds->requirement_row 251 = dr->details.unavailable_for_legal_reasons.requirement_row; 252 ds->h_payto 253 = dr->details.unavailable_for_legal_reasons.h_payto; 254 break; 255 } 256 TALER_TESTING_interpreter_next (ds->is); 257 } 258 259 260 /** 261 * Run the command. 262 * 263 * @param cls closure. 264 * @param cmd the command to execute. 265 * @param is the interpreter state. 266 */ 267 static void 268 batch_deposit_run (void *cls, 269 const struct TALER_TESTING_Command *cmd, 270 struct TALER_TESTING_Interpreter *is) 271 { 272 struct BatchDepositState *ds = cls; 273 const struct TALER_DenominationSignature *denom_pub_sig; 274 struct TALER_PrivateContractHashP h_contract_terms; 275 enum TALER_ErrorCode ec; 276 struct TALER_WireSaltP wire_salt; 277 struct TALER_MerchantWireHashP h_wire; 278 struct TALER_FullPayto payto_uri; 279 struct TALER_EXCHANGE_CoinDepositDetail cdds[ds->num_coins]; 280 struct GNUNET_JSON_Specification spec[] = { 281 TALER_JSON_spec_full_payto_uri ("payto_uri", 282 &payto_uri), 283 GNUNET_JSON_spec_fixed_auto ("salt", 284 &wire_salt), 285 GNUNET_JSON_spec_end () 286 }; 287 const char *exchange_url 288 = TALER_TESTING_get_exchange_url (is); 289 290 (void) cmd; 291 if (NULL == exchange_url) 292 { 293 GNUNET_break (0); 294 return; 295 } 296 memset (cdds, 297 0, 298 sizeof (cdds)); 299 ds->is = is; 300 GNUNET_assert (NULL != ds->wire_details); 301 if (GNUNET_OK != 302 GNUNET_JSON_parse (ds->wire_details, 303 spec, 304 NULL, NULL)) 305 { 306 json_dumpf (ds->wire_details, 307 stderr, 308 JSON_INDENT (2)); 309 GNUNET_break (0); 310 TALER_TESTING_interpreter_fail (is); 311 return; 312 } 313 #if DUMP_CONTRACT 314 fprintf (stderr, 315 "Using contract:\n"); 316 json_dumpf (ds->contract_terms, 317 stderr, 318 JSON_INDENT (2)); 319 #endif 320 if (GNUNET_OK != 321 TALER_JSON_contract_hash (ds->contract_terms, 322 &h_contract_terms)) 323 { 324 GNUNET_break (0); 325 TALER_TESTING_interpreter_fail (is); 326 return; 327 } 328 GNUNET_assert (GNUNET_OK == 329 TALER_JSON_merchant_wire_signature_hash (ds->wire_details, 330 &h_wire)); 331 if (! GNUNET_TIME_absolute_is_zero (ds->refund_deadline.abs_time)) 332 { 333 struct GNUNET_TIME_Relative refund_deadline; 334 335 refund_deadline 336 = GNUNET_TIME_absolute_get_remaining (ds->refund_deadline.abs_time); 337 ds->wire_deadline 338 = 339 GNUNET_TIME_relative_to_timestamp ( 340 GNUNET_TIME_relative_multiply (refund_deadline, 341 2)); 342 } 343 else 344 { 345 ds->refund_deadline = ds->wallet_timestamp; 346 ds->wire_deadline = GNUNET_TIME_timestamp_get (); 347 } 348 349 { 350 const struct TALER_TESTING_Command *acc_var; 351 if (NULL != (acc_var 352 = TALER_TESTING_interpreter_get_command ( 353 is, 354 "account-priv"))) 355 { 356 const union TALER_AccountPrivateKeyP *account_priv; 357 358 if ( (GNUNET_OK != 359 TALER_TESTING_get_trait_account_priv (acc_var, 360 &account_priv)) ) 361 { 362 GNUNET_break (0); 363 TALER_TESTING_interpreter_fail (is); 364 return; 365 } 366 ds->account_priv = *account_priv; 367 GNUNET_CRYPTO_eddsa_key_get_public ( 368 &ds->account_priv.merchant_priv.eddsa_priv, 369 &ds->account_pub.merchant_pub.eddsa_pub); 370 } 371 else 372 { 373 GNUNET_CRYPTO_eddsa_key_create ( 374 &ds->account_priv.merchant_priv.eddsa_priv); 375 GNUNET_CRYPTO_eddsa_key_get_public ( 376 &ds->account_priv.merchant_priv.eddsa_priv, 377 &ds->account_pub.merchant_pub.eddsa_pub); 378 } 379 } 380 for (unsigned int i = 0; i<ds->num_coins; i++) 381 { 382 struct Coin *coin = &ds->coins[i]; 383 struct TALER_EXCHANGE_CoinDepositDetail *cdd = &cdds[i]; 384 const struct TALER_CoinSpendPrivateKeyP *coin_priv; 385 const struct TALER_AgeCommitmentProof *age_commitment_proof = NULL; 386 387 GNUNET_assert (NULL != coin->coin_reference); 388 cdd->amount = coin->amount; 389 coin->coin_cmd = TALER_TESTING_interpreter_lookup_command ( 390 is, 391 coin->coin_reference); 392 if (NULL == coin->coin_cmd) 393 { 394 GNUNET_break (0); 395 TALER_TESTING_interpreter_fail (is); 396 return; 397 } 398 399 if ( (GNUNET_OK != 400 TALER_TESTING_get_trait_coin_priv (coin->coin_cmd, 401 coin->coin_idx, 402 &coin_priv)) || 403 (GNUNET_OK != 404 TALER_TESTING_get_trait_age_commitment_proof (coin->coin_cmd, 405 coin->coin_idx, 406 &age_commitment_proof)) 407 || 408 (GNUNET_OK != 409 TALER_TESTING_get_trait_denom_pub (coin->coin_cmd, 410 coin->coin_idx, 411 &coin->denom_pub)) || 412 (GNUNET_OK != 413 TALER_TESTING_get_trait_denom_sig (coin->coin_cmd, 414 coin->coin_idx, 415 &denom_pub_sig)) ) 416 { 417 GNUNET_break (0); 418 TALER_TESTING_interpreter_fail (is); 419 return; 420 } 421 if (NULL != age_commitment_proof) 422 { 423 TALER_age_commitment_hash (&age_commitment_proof->commitment, 424 &cdd->h_age_commitment); 425 } 426 coin->deposit_fee = coin->denom_pub->fees.deposit; 427 GNUNET_CRYPTO_eddsa_key_get_public (&coin_priv->eddsa_priv, 428 &cdd->coin_pub.eddsa_pub); 429 cdd->denom_sig = *denom_pub_sig; 430 cdd->h_denom_pub = coin->denom_pub->h_key; 431 TALER_wallet_deposit_sign (&coin->amount, 432 &coin->denom_pub->fees.deposit, 433 &h_wire, 434 &h_contract_terms, 435 NULL, /* wallet_data_hash */ 436 &cdd->h_age_commitment, 437 NULL, /* hash of extensions */ 438 &coin->denom_pub->h_key, 439 ds->wallet_timestamp, 440 &ds->account_pub.merchant_pub, 441 ds->refund_deadline, 442 coin_priv, 443 &cdd->coin_sig); 444 coin->coin_sig = cdd->coin_sig; 445 coin->che.type = TALER_EXCHANGE_CTT_DEPOSIT; 446 coin->che.amount = coin->amount; 447 coin->che.details.deposit.h_wire = h_wire; 448 coin->che.details.deposit.h_contract_terms = h_contract_terms; 449 coin->che.details.deposit.no_h_policy = true; 450 coin->che.details.deposit.no_wallet_data_hash = true; 451 coin->che.details.deposit.wallet_timestamp = ds->wallet_timestamp; 452 coin->che.details.deposit.merchant_pub = ds->account_pub.merchant_pub; 453 coin->che.details.deposit.refund_deadline = ds->refund_deadline; 454 coin->che.details.deposit.sig = cdd->coin_sig; 455 coin->che.details.deposit.no_hac = GNUNET_is_zero (&cdd->h_age_commitment); 456 coin->che.details.deposit.hac = cdd->h_age_commitment; 457 coin->che.details.deposit.deposit_fee = coin->denom_pub->fees.deposit; 458 } 459 460 GNUNET_assert (NULL == ds->dh); 461 { 462 struct TALER_EXCHANGE_DepositContractDetail dcd = { 463 .wire_deadline = ds->wire_deadline, 464 .merchant_payto_uri = payto_uri, 465 .wire_salt = wire_salt, 466 .h_contract_terms = h_contract_terms, 467 .wallet_timestamp = ds->wallet_timestamp, 468 .merchant_pub = ds->account_pub.merchant_pub, 469 .refund_deadline = ds->refund_deadline 470 }; 471 472 TALER_merchant_contract_sign (&h_contract_terms, 473 &ds->account_priv.merchant_priv, 474 &dcd.merchant_sig); 475 ds->dh = TALER_EXCHANGE_post_batch_deposit_create ( 476 TALER_TESTING_interpreter_get_context (is), 477 exchange_url, 478 TALER_TESTING_get_keys (is), 479 &dcd, 480 ds->num_coins, 481 cdds, 482 &ec); 483 } 484 if (NULL == ds->dh) 485 { 486 GNUNET_break (0); 487 GNUNET_log (GNUNET_ERROR_TYPE_ERROR, 488 "Could not create deposit with EC %d\n", 489 (int) ec); 490 TALER_TESTING_interpreter_fail (is); 491 return; 492 } 493 TALER_EXCHANGE_post_batch_deposit_start (ds->dh, 494 &batch_deposit_cb, 495 ds); 496 } 497 498 499 /** 500 * Free the state of a "batch-deposit" CMD, and possibly cancel a 501 * pending operation thereof. 502 * 503 * @param cls closure, must be a `struct BatchDepositState`. 504 * @param cmd the command which is being cleaned up. 505 */ 506 static void 507 batch_deposit_cleanup (void *cls, 508 const struct TALER_TESTING_Command *cmd) 509 { 510 struct BatchDepositState *ds = cls; 511 512 if (NULL != ds->dh) 513 { 514 TALER_TESTING_command_incomplete (ds->is, 515 cmd->label); 516 TALER_EXCHANGE_post_batch_deposit_cancel (ds->dh); 517 ds->dh = NULL; 518 } 519 if (NULL != ds->retry_task) 520 { 521 GNUNET_SCHEDULER_cancel (ds->retry_task); 522 ds->retry_task = NULL; 523 } 524 for (unsigned int i = 0; i<ds->num_coins; i++) 525 GNUNET_free (ds->coins[i].coin_reference); 526 GNUNET_free (ds->coins); 527 json_decref (ds->wire_details); 528 json_decref (ds->contract_terms); 529 GNUNET_free (ds); 530 } 531 532 533 /** 534 * Offer internal data from a "batch-deposit" CMD, to other commands. 535 * 536 * @param cls closure. 537 * @param[out] ret result. 538 * @param trait name of the trait. 539 * @param index index number of the object to offer. 540 * @return #GNUNET_OK on success. 541 */ 542 static enum GNUNET_GenericReturnValue 543 batch_deposit_traits (void *cls, 544 const void **ret, 545 const char *trait, 546 unsigned int index) 547 { 548 struct BatchDepositState *ds = cls; 549 const struct Coin *coin = &ds->coins[index]; 550 /* Will point to coin cmd internals. */ 551 const struct TALER_CoinSpendPrivateKeyP *coin_spent_priv; 552 const struct TALER_CoinSpendPublicKeyP *coin_spent_pub; 553 const struct TALER_AgeCommitmentProof *age_commitment_proof; 554 555 if (index >= ds->num_coins) 556 { 557 GNUNET_log (GNUNET_ERROR_TYPE_ERROR, 558 "[batch_deposit_traits] asked for index #%u while num_coins is #%u\n", 559 index, 560 ds->num_coins); 561 return GNUNET_NO; 562 } 563 if (NULL == coin->coin_cmd) 564 { 565 GNUNET_break (0); 566 TALER_TESTING_interpreter_fail (ds->is); 567 return GNUNET_NO; 568 } 569 if ( (GNUNET_OK != 570 TALER_TESTING_get_trait_coin_priv (coin->coin_cmd, 571 coin->coin_idx, 572 &coin_spent_priv)) || 573 (GNUNET_OK != 574 TALER_TESTING_get_trait_coin_pub (coin->coin_cmd, 575 coin->coin_idx, 576 &coin_spent_pub)) || 577 (GNUNET_OK != 578 TALER_TESTING_get_trait_age_commitment_proof (coin->coin_cmd, 579 coin->coin_idx, 580 &age_commitment_proof)) ) 581 { 582 GNUNET_break (0); 583 TALER_TESTING_interpreter_fail (ds->is); 584 return GNUNET_NO; 585 } 586 587 { 588 struct TALER_TESTING_Trait traits[] = { 589 /* First two traits are only available if 590 ds->traits is #GNUNET_YES */ 591 TALER_TESTING_make_trait_exchange_pub (0, 592 &ds->exchange_pub), 593 TALER_TESTING_make_trait_exchange_sig (0, 594 &ds->exchange_sig), 595 /* These traits are always available */ 596 TALER_TESTING_make_trait_wire_details (ds->wire_details), 597 TALER_TESTING_make_trait_contract_terms (ds->contract_terms), 598 TALER_TESTING_make_trait_merchant_priv (&ds->account_priv.merchant_priv), 599 TALER_TESTING_make_trait_merchant_pub (&ds->account_pub.merchant_pub), 600 TALER_TESTING_make_trait_account_priv (&ds->account_priv), 601 TALER_TESTING_make_trait_account_pub (&ds->account_pub), 602 TALER_TESTING_make_trait_age_commitment_proof (index, 603 age_commitment_proof), 604 TALER_TESTING_make_trait_coin_history (index, 605 &coin->che), 606 TALER_TESTING_make_trait_coin_pub (index, 607 coin_spent_pub), 608 TALER_TESTING_make_trait_denom_pub (index, 609 coin->denom_pub), 610 TALER_TESTING_make_trait_coin_priv (index, 611 coin_spent_priv), 612 TALER_TESTING_make_trait_coin_sig (index, 613 &coin->coin_sig), 614 TALER_TESTING_make_trait_deposit_amount (index, 615 &coin->amount), 616 TALER_TESTING_make_trait_deposit_fee_amount (index, 617 &coin->deposit_fee), 618 TALER_TESTING_make_trait_timestamp (index, 619 &ds->exchange_timestamp), 620 TALER_TESTING_make_trait_wire_deadline (index, 621 &ds->wire_deadline), 622 TALER_TESTING_make_trait_refund_deadline (index, 623 &ds->refund_deadline), 624 TALER_TESTING_make_trait_legi_requirement_row (&ds->requirement_row), 625 TALER_TESTING_make_trait_h_normalized_payto (&ds->h_payto), 626 TALER_TESTING_trait_end () 627 }; 628 629 return TALER_TESTING_get_trait ((ds->deposit_succeeded) 630 ? traits 631 : &traits[2], 632 ret, 633 trait, 634 index); 635 } 636 } 637 638 639 struct TALER_TESTING_Command 640 TALER_TESTING_cmd_batch_deposit ( 641 const char *label, 642 const struct TALER_FullPayto target_account_payto, 643 const char *contract_terms, 644 struct GNUNET_TIME_Relative refund_deadline, 645 unsigned int expected_response_code, 646 ...) 647 { 648 struct BatchDepositState *ds; 649 va_list ap; 650 unsigned int num_coins = 0; 651 const char *ref; 652 653 va_start (ap, 654 expected_response_code); 655 while (NULL != (ref = va_arg (ap, 656 const char *))) 657 { 658 GNUNET_assert (NULL != va_arg (ap, 659 const char *)); 660 num_coins++; 661 } 662 va_end (ap); 663 664 ds = GNUNET_new (struct BatchDepositState); 665 ds->num_coins = num_coins; 666 ds->coins = GNUNET_new_array (num_coins, 667 struct Coin); 668 num_coins = 0; 669 va_start (ap, 670 expected_response_code); 671 while (NULL != (ref = va_arg (ap, 672 const char *))) 673 { 674 struct Coin *coin = &ds->coins[num_coins++]; 675 const char *amount = va_arg (ap, 676 const char *); 677 678 GNUNET_assert (GNUNET_OK == 679 TALER_TESTING_parse_coin_reference (ref, 680 &coin->coin_reference, 681 &coin->coin_idx)); 682 GNUNET_assert (GNUNET_OK == 683 TALER_string_to_amount (amount, 684 &coin->amount)); 685 } 686 va_end (ap); 687 688 ds->wire_details = TALER_TESTING_make_wire_details (target_account_payto); 689 GNUNET_assert (NULL != ds->wire_details); 690 ds->contract_terms = json_loads (contract_terms, 691 JSON_REJECT_DUPLICATES, 692 NULL); 693 if (NULL == ds->contract_terms) 694 { 695 GNUNET_log (GNUNET_ERROR_TYPE_ERROR, 696 "Failed to parse contract terms `%s' for CMD `%s'\n", 697 contract_terms, 698 label); 699 GNUNET_assert (0); 700 } 701 ds->wallet_timestamp = GNUNET_TIME_timestamp_get (); 702 GNUNET_assert (0 == 703 json_object_set_new (ds->contract_terms, 704 "timestamp", 705 GNUNET_JSON_from_timestamp ( 706 ds->wallet_timestamp))); 707 if (! GNUNET_TIME_relative_is_zero (refund_deadline)) 708 { 709 ds->refund_deadline = GNUNET_TIME_relative_to_timestamp (refund_deadline); 710 GNUNET_assert (0 == 711 json_object_set_new (ds->contract_terms, 712 "refund_deadline", 713 GNUNET_JSON_from_timestamp ( 714 ds->refund_deadline))); 715 } 716 ds->expected_response_code = expected_response_code; 717 { 718 struct TALER_TESTING_Command cmd = { 719 .cls = ds, 720 .label = label, 721 .run = &batch_deposit_run, 722 .cleanup = &batch_deposit_cleanup, 723 .traits = &batch_deposit_traits 724 }; 725 726 return cmd; 727 } 728 } 729 730 731 /* end of testing_api_cmd_batch_deposit.c */