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