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