routine.rs (60541B)
1 /* 2 This file is part of TALER 3 Copyright (C) 2024-2026 Taler Systems SA 4 5 TALER is free software; you can redistribute it and/or modify it under the 6 terms of the GNU Affero General Public License as published by the Free Software 7 Foundation; either version 3, or (at your option) any later version. 8 9 TALER is distributed in the hope that it will be useful, but WITHOUT ANY 10 WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR 11 A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. 12 13 You should have received a copy of the GNU Affero General Public License along with 14 TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/> 15 */ 16 17 use std::{ 18 fmt::Debug, 19 future::Future, 20 str::FromStr, 21 sync::LazyLock, 22 time::{Duration, Instant}, 23 }; 24 25 use aws_lc_rs::signature::{Ed25519KeyPair, KeyPair as _}; 26 use axum::{Router, http::StatusCode}; 27 use jiff::{SignedDuration, Timestamp}; 28 use serde::de::DeserializeOwned; 29 use taler_api::subject::fmt_in_subject; 30 use taler_common::{ 31 api::{ 32 EddsaPublicKey, EddsaSignature, HashCode, ShortHashCode, 33 params::PageParams, 34 prepared::{ 35 PublicKeyAlg, RegistrationRequest, RegistrationResponse, TransferSubject, TransferType, 36 Unregistration, 37 }, 38 revenue::RevenueIncomingHistory, 39 wire::{ 40 IncomingBankTransaction, IncomingHistory, OutgoingHistory, TransferList, 41 TransferRequest, TransferResponse, TransferState, TransferStatus, 42 }, 43 }, 44 db::IncomingType, 45 error_code::ErrorCode, 46 signature::Signature as _, 47 types::{ 48 amount::{Amount, Currency, amount}, 49 base32::Base32, 50 payto::PaytoURI, 51 time::TalerTimestamp, 52 url, 53 }, 54 }; 55 use tokio::time::sleep; 56 57 use crate::{ 58 json, 59 server::{TestResponse, TestServer as _}, 60 }; 61 62 static UNKNOWN: LazyLock<PaytoURI> = LazyLock::new(|| { 63 PaytoURI::from_str("payto://malformed/unused?receiver-name=Malformed").unwrap() 64 }); 65 66 pub trait Page: DeserializeOwned + Debug { 67 fn ids(&self) -> Vec<i64>; 68 } 69 70 impl Page for IncomingHistory { 71 fn ids(&self) -> Vec<i64> { 72 self.incoming_transactions 73 .iter() 74 .map(|it| match it { 75 IncomingBankTransaction::Reserve { row_id, .. } 76 | IncomingBankTransaction::Wad { row_id, .. } 77 | IncomingBankTransaction::Kyc { row_id, .. } => *row_id as i64, 78 }) 79 .collect() 80 } 81 } 82 83 impl Page for OutgoingHistory { 84 fn ids(&self) -> Vec<i64> { 85 self.outgoing_transactions 86 .iter() 87 .map(|it| it.row_id as i64) 88 .collect() 89 } 90 } 91 92 impl Page for RevenueIncomingHistory { 93 fn ids(&self) -> Vec<i64> { 94 self.incoming_transactions 95 .iter() 96 .map(|it| it.row_id as i64) 97 .collect() 98 } 99 } 100 101 impl Page for TransferList { 102 fn ids(&self) -> Vec<i64> { 103 self.transfers.iter().map(|it| it.row_id as i64).collect() 104 } 105 } 106 107 pub async fn latest_id<T: Page>(router: &Router) -> i64 { 108 let res = router.get("?limit=-1").await; 109 if res.status == StatusCode::NO_CONTENT { 110 0 111 } else { 112 res.assert_ids::<T>(1)[0] 113 } 114 } 115 116 pub async fn routine_pagination<T: Page>( 117 server: &Router, 118 mut register: Tasks<impl AsyncFnMut(usize)>, 119 ) { 120 // Check supported 121 if !server.get("").await.is_implemented() { 122 return; 123 } 124 125 let assert_history = 126 async |args: &str, size: usize| server.get(format!("?{args}")).await.assert_ids::<T>(size); 127 let before = latest_id::<T>(server).await; 128 129 for i in 0..20 { 130 (register.lambda)(i).await; 131 } 132 133 // Use actual row IDs: earlier tests and ignored transactions can leave gaps 134 let ids = assert_history(&format!("limit=20&offset={before}"), 20).await; 135 let reverse: Vec<_> = ids.iter().copied().rev().collect(); 136 assert_eq!(assert_history("", 20).await, reverse); 137 assert_eq!(assert_history("limit=-10", 10).await, reverse[..10]); 138 139 // Cursors are exclusive in both directions, including single-row pages. 140 for limit in [1, 10] { 141 assert_eq!( 142 assert_history(&format!("limit={limit}&offset={}", ids[4]), limit).await, 143 ids[5..5 + limit] 144 ); 145 assert_eq!( 146 assert_history(&format!("limit=-{limit}&offset={}", ids[15]), limit).await, 147 reverse[5..5 + limit] 148 ); 149 } 150 151 // Partial pages and exhausted cursors. 152 assert_eq!( 153 assert_history(&format!("limit=10&offset={}", ids[18]), 1).await, 154 ids[19..] 155 ); 156 assert_history(&format!("limit=1&offset={}", ids[19]), 0).await; 157 let first = assert_history("limit=1", 1).await[0]; 158 assert_history(&format!("limit=-1&offset={first}"), 0).await; 159 assert_history("limit=-1&offset=0", 0).await; 160 assert_history(&format!("limit=1&offset={}", i64::MAX), 0).await; 161 162 for args in [ 163 "limit=0", 164 "limit=1025", 165 "limit=-1025", 166 "limit=-9223372036854775808", 167 "limit=invalid", 168 "offset=-1", 169 "offset=invalid", 170 ] { 171 server 172 .get(format!("?{args}")) 173 .await 174 .assert_error(ErrorCode::GENERIC_PARAMETER_MALFORMED); 175 } 176 } 177 178 pub async fn assert_time<R>(range: std::ops::Range<u64>, task: impl Future<Output = R>) -> R { 179 let start = Instant::now(); 180 let limit = Duration::from_millis(range.end); 181 let Ok(res) = tokio::time::timeout(limit, task).await else { 182 panic!( 183 "Expected to last {range:?} ms, timed out after {} ms", 184 start.elapsed().as_millis() 185 ); 186 }; 187 let elapsed = start 188 .elapsed() 189 .as_millis() 190 .try_into() 191 .expect("timing bound exceeds u64"); 192 if !range.contains(&elapsed) { 193 panic!("Expected to last {range:?} got {elapsed:?}") 194 } 195 res 196 } 197 198 pub async fn routine_history<T: Page>( 199 server: &Router, 200 mut register: Tasks<impl AsyncFnMut(usize)>, 201 mut ignore: Tasks<impl AsyncFnMut(usize)>, 202 ) { 203 let assert_history = async |args: String, size: usize| { 204 server.get(format!("?{args}")).await.assert_ids::<T>(size) 205 }; 206 207 // Check error when no transactions 208 assert_history("limit=7".to_owned(), 0).await; 209 210 // Interleave visible and ignored transactions to exercise gaps in row IDs. 211 for idx in 0..register.len.max(ignore.len) { 212 if idx < register.len { 213 (register.lambda)(idx).await; 214 } 215 if idx < ignore.len { 216 (ignore.lambda)(idx).await; 217 } 218 } 219 let nb_register = register.len; 220 let nb_ignore = ignore.len; 221 let nb_total = nb_register + nb_ignore; 222 223 // Check ignored 224 assert_history(format!("limit={nb_total}"), nb_register).await; 225 // Check skip ignored 226 assert_history(format!("limit={nb_register}"), nb_register).await; 227 228 // Allow CI scheduling and database delays, but require completion before 229 // the long-poll timeout so broken notification delivery still fails. 230 // Check no polling when we cannot have more transactions 231 assert_time( 232 0..5000, 233 assert_history( 234 format!("limit=-{}&timeout_ms=10000", nb_register + 1), 235 nb_register, 236 ), 237 ) 238 .await; 239 // Check no polling when already find transactions even if less than delta 240 assert_time( 241 0..5000, 242 assert_history( 243 format!("limit={}&timeout_ms=10000", nb_register + 1), 244 nb_register, 245 ), 246 ) 247 .await; 248 249 // Check polling 250 let id = latest_id::<T>(server).await; 251 tokio::join!( 252 // Check polling succeed 253 assert_time( 254 100..5000, 255 assert_history(format!("limit=2&offset={id}&timeout_ms=10000"), 1) 256 ), 257 assert_time( 258 200..5000, 259 assert_history( 260 format!( 261 "limit=1&offset={}&timeout_ms=200", 262 id as usize + nb_total * 3 263 ), 264 0 265 ) 266 ), 267 async { 268 sleep(Duration::from_millis(100)).await; 269 (register.lambda)(0).await 270 } 271 ); 272 273 // Test triggers 274 for i in 0..register.len { 275 let id = latest_id::<T>(server).await; 276 tokio::join!( 277 // Check polling succeed 278 assert_time( 279 100..5000, 280 assert_history(format!("limit=7&offset={id}&timeout_ms=10000"), 1) 281 ), 282 async { 283 sleep(Duration::from_millis(100)).await; 284 (register.lambda)(i).await 285 } 286 ); 287 } 288 289 // Test doesn't trigger 290 let id = latest_id::<T>(server).await; 291 tokio::join!( 292 // Check polling succeed 293 assert_time( 294 200..5000, 295 assert_history(format!("limit=7&offset={id}&timeout_ms=200"), 0) 296 ), 297 async { 298 sleep(Duration::from_millis(100)).await; 299 for i in 0..ignore.len { 300 (ignore.lambda)(i).await 301 } 302 } 303 ); 304 305 routine_pagination::<T>(server, register).await; 306 } 307 308 impl TestResponse { 309 #[track_caller] 310 fn assert_ids<T: Page>(&self, size: usize) -> Vec<i64> { 311 if size == 0 { 312 self.assert_no_content(); 313 return vec![]; 314 } 315 let body = self.assert_ok_json::<T>(); 316 let page = body.ids(); 317 let params = self.query::<PageParams>().check().unwrap(); 318 319 // testing the size is like expected 320 assert_eq!(size, page.len(), "bad page length: {page:?}\n{body:?}"); 321 if params.limit < 0 { 322 // The cursor row must not be repeated on the next page 323 assert!( 324 params.offset.map(|offset| page[0] < offset).unwrap_or(true), 325 "bad page offset: {params:?} {page:?}" 326 ); 327 // testing that the id decreases. 328 assert!( 329 page.as_slice().is_sorted_by(|a, b| a > b), 330 "bad page order: {page:?}" 331 ) 332 } else { 333 // The cursor row must not be repeated on the next page. 334 assert!( 335 params.offset.map(|offset| page[0] > offset).unwrap_or(true), 336 "bad page offset: {params:?} {page:?}" 337 ); 338 // testing that the id increases. 339 assert!( 340 page.as_slice().is_sorted_by(|a, b| a < b), 341 "bad page order: {page:?}" 342 ) 343 } 344 page 345 } 346 } 347 348 // Get currency from config 349 async fn get_currency(server: &Router) -> Currency { 350 let config = server 351 .get("/config") 352 .await 353 .assert_ok_json::<serde_json::Value>(); 354 let currency = config["currency"].as_str().unwrap(); 355 Currency::from_str(currency).unwrap() 356 } 357 358 /// Test standard behavior of the transfer endpoints 359 pub async fn transfer_routine( 360 wire_gateway: &Router, 361 default_status: TransferState, 362 credit_account: &PaytoURI, 363 ) { 364 let currency = &get_currency(wire_gateway).await; 365 let default_amount = amount(format!("{currency}:42")); 366 let request_uid = HashCode::rand(); 367 let wtid = ShortHashCode::rand(); 368 let valid_req = json!({ 369 "request_uid": request_uid, 370 "amount": default_amount, 371 "exchange_base_url": "http://exchange.taler/", 372 "wtid": wtid, 373 "credit_account": credit_account, 374 }); 375 376 // Check empty db 377 { 378 wire_gateway.get("/transfers").await.assert_no_content(); 379 wire_gateway 380 .get(format!("/transfers?status={}", default_status.as_ref())) 381 .await 382 .assert_no_content(); 383 } 384 385 // TODO check subject formatting 386 387 let routine = async |req: &TransferRequest| { 388 // Check OK 389 let first = wire_gateway 390 .post("/transfer") 391 .json(req) 392 .await 393 .assert_ok_json::<TransferResponse>(); 394 // Check idempotent 395 let second = wire_gateway 396 .post("/transfer") 397 .json(req) 398 .await 399 .assert_ok_json::<TransferResponse>(); 400 assert_eq!(first.row_id, second.row_id); 401 assert_eq!(first.timestamp, second.timestamp); 402 403 // Check by id 404 let tx = wire_gateway 405 .get(format!("/transfers/{}", first.row_id)) 406 .await 407 .assert_ok_json::<TransferStatus>(); 408 assert_eq!(default_status, tx.status); 409 assert_eq!(default_amount, tx.amount); 410 assert_eq!("http://exchange.taler/", tx.exchange_base_url); 411 assert_eq!(req.wtid, tx.wtid); 412 assert_eq!(first.timestamp, tx.timestamp); 413 assert_eq!(req.metadata, tx.metadata); 414 assert_eq!(credit_account, &tx.credit_account); 415 416 // Check page 417 let list = wire_gateway 418 .get("/transfers?limit=-1") 419 .await 420 .assert_ok_json::<TransferList>(); 421 let [tx] = list.transfers.as_slice() else { 422 panic!("expected one transfer, got {list:?}"); 423 }; 424 assert_eq!(first.row_id, tx.row_id); 425 assert_eq!(default_status, tx.status); 426 assert_eq!(default_amount, tx.amount); 427 assert_eq!(first.timestamp, tx.timestamp); 428 assert_eq!(credit_account, &tx.credit_account); 429 }; 430 431 let req = TransferRequest { 432 request_uid, 433 amount: default_amount, 434 exchange_base_url: url("http://exchange.taler/"), 435 metadata: None, 436 wtid, 437 credit_account: credit_account.clone(), 438 }; 439 // Simple 440 routine(&req).await; 441 // With metadata 442 wire_gateway 443 .post("/transfer") 444 .json(&TransferRequest { 445 metadata: Some("changed".into()), 446 ..req.clone() 447 }) 448 .await 449 .assert_error(ErrorCode::BANK_TRANSFER_REQUEST_UID_REUSED); 450 451 let metadata_req = TransferRequest { 452 request_uid: HashCode::rand(), 453 wtid: ShortHashCode::rand(), 454 metadata: Some("test:metadata".into()), 455 ..req.clone() 456 }; 457 routine(&metadata_req).await; 458 wire_gateway 459 .post("/transfer") 460 .json(&TransferRequest { 461 metadata: None, 462 ..metadata_req 463 }) 464 .await 465 .assert_error(ErrorCode::BANK_TRANSFER_REQUEST_UID_REUSED); 466 467 // Check create transfer errors 468 { 469 // Changing any of these fields conflicts with the original request UID 470 for conflicting_req in [ 471 TransferRequest { 472 wtid: ShortHashCode::rand(), 473 ..req.clone() 474 }, 475 TransferRequest { 476 amount: amount(format!("{currency}:43")), 477 ..req.clone() 478 }, 479 TransferRequest { 480 exchange_base_url: url("https://other-exchange.taler/"), 481 ..req.clone() 482 }, 483 ] { 484 wire_gateway 485 .post("/transfer") 486 .json(conflicting_req) 487 .await 488 .assert_error(ErrorCode::BANK_TRANSFER_REQUEST_UID_REUSED); 489 } 490 // Check wtid reuse 491 wire_gateway 492 .post("/transfer") 493 .json(json!(valid_req + { 494 "request_uid": HashCode::rand(), 495 })) 496 .await 497 .assert_error(ErrorCode::BANK_TRANSFER_WTID_REUSED); 498 499 // Check currency mismatch 500 wire_gateway 501 .post("/transfer") 502 .json(json!(valid_req + { 503 "amount": "BAD:42" 504 })) 505 .await 506 .assert_error(ErrorCode::GENERIC_CURRENCY_MISMATCH); 507 508 // Invalid Base32 509 wire_gateway 510 .post("/transfer") 511 .json(json!(valid_req + { 512 "wtid": "I love chocolate" 513 })) 514 .await 515 .assert_error(ErrorCode::GENERIC_JSON_INVALID); 516 wire_gateway 517 .post("/transfer") 518 .json(json!(valid_req + { 519 "wtid": Base32::<31>::rand() 520 })) 521 .await 522 .assert_error(ErrorCode::GENERIC_JSON_INVALID); 523 wire_gateway 524 .post("/transfer") 525 .json(json!(valid_req + { 526 "request_uid": "I love chocolate" 527 })) 528 .await 529 .assert_error(ErrorCode::GENERIC_JSON_INVALID); 530 wire_gateway 531 .post("/transfer") 532 .json(json!(valid_req + { 533 "request_uid": Base32::<65>::rand() 534 })) 535 .await 536 .assert_error(ErrorCode::GENERIC_JSON_INVALID); 537 538 // Missing receiver-name 539 let res = wire_gateway 540 .post("/transfer") 541 .json(json!(valid_req + { 542 "credit_account": credit_account.as_ref().as_str().split('?').next().unwrap() 543 })) 544 .await; 545 if !res.status.is_success() { 546 res.assert_error(ErrorCode::GENERIC_PAYTO_URI_MALFORMED); 547 } 548 549 // Unsupported payto kind 550 wire_gateway 551 .post("/transfer") 552 .json(json!(valid_req + { "credit_account": *UNKNOWN })) 553 .await 554 .assert_error(ErrorCode::GENERIC_PAYTO_URI_MALFORMED); 555 // Malformed payto 556 wire_gateway 557 .post("/transfer") 558 .json(json!(valid_req + { "credit_account": "http://email@test.com" })) 559 .await 560 .assert_error(ErrorCode::GENERIC_JSON_INVALID); 561 562 // Bad base URL 563 for base_url in [ 564 "not-a-url", 565 "file://not.http.com/", 566 "no.transport.com/", 567 "https://not.a/base/url", 568 ] { 569 wire_gateway 570 .post("/transfer") 571 .json(&json!(valid_req + { "exchange_base_url": base_url })) 572 .await 573 .assert_error(ErrorCode::GENERIC_JSON_INVALID); 574 } 575 576 // Malformed metadata 577 for metadata in ["", "bad_id", "bad id", "bad@id.com", &"A".repeat(41)] { 578 wire_gateway 579 .post("/transfer") 580 .json(&json!(valid_req + { "metadata": metadata })) 581 .await 582 .assert_error(ErrorCode::GENERIC_JSON_INVALID); 583 } 584 } 585 586 // Check transfer by id errors 587 { 588 // Check unknown transaction 589 wire_gateway 590 .get("/transfers/42") 591 .await 592 .assert_error(ErrorCode::BANK_TRANSACTION_NOT_FOUND); 593 } 594 595 // Check transfer page 596 { 597 // Valid metadata boundaries and all supported punctuation. 598 for metadata in ["A", &"A".repeat(40), "test-1.2:metadata", "0"] { 599 wire_gateway 600 .post("/transfer") 601 .json(&json!(valid_req + { 602 "request_uid": HashCode::rand(), 603 "wtid": ShortHashCode::rand(), 604 "metadata": metadata 605 })) 606 .await 607 .assert_ok_json::<TransferResponse>(); 608 } 609 for status in [ 610 TransferState::pending, 611 TransferState::transient_failure, 612 TransferState::permanent_failure, 613 TransferState::success, 614 ] { 615 if status != default_status { 616 wire_gateway 617 .get(format!("/transfers?status={}", status.as_ref())) 618 .await 619 .assert_no_content(); 620 } 621 } 622 wire_gateway 623 .get("/transfers?status=invalid") 624 .await 625 .assert_error(ErrorCode::GENERIC_PARAMETER_MALFORMED); 626 { 627 let list = wire_gateway 628 .get("/transfers") 629 .await 630 .assert_ok_json::<TransferList>(); 631 assert_eq!(list.transfers.len(), 6); 632 assert_eq!( 633 list, 634 wire_gateway 635 .get(format!("/transfers?status={}", default_status.as_ref())) 636 .await 637 .assert_ok_json::<TransferList>() 638 ) 639 } 640 641 // Pagination test 642 routine_pagination::<TransferList>( 643 &wire_gateway.suffix("/transfers"), 644 crate::tasks!({ 645 wire_gateway 646 .post("/transfer") 647 .json(json!({ 648 "request_uid": HashCode::rand(), 649 "amount": amount(format!("{currency}:0.1")), 650 "exchange_base_url": url("http://exchange.taler"), 651 "wtid": ShortHashCode::rand(), 652 "credit_account": credit_account, 653 })) 654 .await 655 .assert_ok_json::<TransferResponse>(); 656 }), 657 ) 658 .await; 659 } 660 } 661 662 async fn add_incoming_routine( 663 wire_gateway: &Router, 664 prepared_transfer: &Router, 665 currency: &Currency, 666 kind: IncomingType, 667 debit_acount: &PaytoURI, 668 credit_account: &PaytoURI, 669 ) { 670 let (path, key) = match kind { 671 IncomingType::reserve => ("/admin/add-incoming", "reserve_pub"), 672 IncomingType::kyc => ("/admin/add-kycauth", "account_pub"), 673 IncomingType::map => ("/admin/add-mapped", "authorization_pub"), 674 }; 675 let key_pair = Ed25519KeyPair::generate().unwrap(); 676 let pub_key = EddsaPublicKey::try_from(key_pair.public_key().as_ref()).unwrap(); 677 // Valid 678 let req = RegistrationRequest { 679 credit_account: credit_account.clone(), 680 r#type: TransferType::reserve, 681 recurrent: false, 682 credit_amount: Amount::new(currency, 44, 0), 683 alg: PublicKeyAlg::EdDSA, 684 account_pub: pub_key, 685 authorization_pub: pub_key, 686 authorization_sig: EddsaSignature::ZEROED, 687 } 688 .signed(&key_pair); 689 690 prepared_transfer 691 .post("/registration") 692 .json(&req) 693 .await 694 .assert_ok_json::<RegistrationResponse>(); 695 let valid_req = json!({ 696 "amount": format!("{currency}:44"), 697 key: pub_key, 698 "debit_account": debit_acount, 699 }); 700 701 // Check OK 702 wire_gateway.post(path).json(&valid_req).await.assert_ok(); 703 704 match kind { 705 IncomingType::reserve => { 706 // Trigger conflict due to reused reserve_pub 707 wire_gateway 708 .post(path) 709 .json(&json!(valid_req + { 710 "amount": format!("{currency}:44.1"), 711 })) 712 .await 713 .assert_error(ErrorCode::BANK_DUPLICATE_RESERVE_PUB_SUBJECT) 714 } 715 IncomingType::kyc => { 716 // Non conflict on reuse 717 wire_gateway.post(path).json(&valid_req).await.assert_ok(); 718 } 719 IncomingType::map => { 720 // Trigger conflict due to reused authorization_pub 721 wire_gateway 722 .post(path) 723 .json(&valid_req) 724 .await 725 .assert_error(ErrorCode::BANK_TRANSFER_MAPPING_REUSED); 726 // Trigger conflict due to unknown authorization_pub 727 wire_gateway 728 .post(path) 729 .json(&json!(valid_req + { 730 key: EddsaPublicKey::rand() 731 })) 732 .await 733 .assert_error(ErrorCode::BANK_TRANSFER_MAPPING_UNKNOWN); 734 } 735 } 736 737 // Currency mismatch 738 wire_gateway 739 .post(path) 740 .json(&json!(valid_req + { "amount": "BAD:33" })) 741 .await 742 .assert_error(ErrorCode::GENERIC_CURRENCY_MISMATCH); 743 744 // Bad BASE32 reserve_pub 745 wire_gateway 746 .post(path) 747 .json(json!(valid_req + { key: "I love chocolate" })) 748 .await 749 .assert_error(ErrorCode::GENERIC_JSON_INVALID); 750 wire_gateway 751 .post(path) 752 .json(json!(valid_req + { key: Base32::<31>::rand() })) 753 .await 754 .assert_error(ErrorCode::GENERIC_JSON_INVALID); 755 756 // Unsupported payto kind 757 wire_gateway 758 .post(path) 759 .json(json!(valid_req + { "debit_account": *UNKNOWN })) 760 .await 761 .assert_error(ErrorCode::GENERIC_PAYTO_URI_MALFORMED); 762 763 // Malformed payto 764 wire_gateway 765 .post(path) 766 .json(json!(valid_req + { "debit_account": "http://email@test.com" })) 767 .await 768 .assert_error(ErrorCode::GENERIC_JSON_INVALID); 769 } 770 771 pub struct Tasks<F: AsyncFnMut(usize)> { 772 pub len: usize, 773 pub lambda: F, 774 } 775 776 #[macro_export] 777 macro_rules! tasks { 778 // Create new 779 ( $( $(if $cond:expr =>)? $body:block ),* $(,)? ) => { 780 $crate::tasks!(@build 0usize; 781 $( $(if $cond =>)? $body ),* 782 ) 783 }; 784 785 // Append to existing 786 ( $existing:expr ; $( $(if $cond:expr =>)? $body:block ),* $(,)? ) => {{ 787 let mut existing = $existing; 788 let mut extra = $crate::tasks![ 789 $( $(if $cond =>)? $body ),* 790 ]; 791 792 $crate::routine::Tasks { 793 len: existing.len + extra.len, 794 lambda: async move |i: usize| { 795 if existing.len == 0 && extra.len == 0 { 796 return; 797 } 798 799 let i = i % (existing.len + extra.len); 800 801 if i < existing.len { 802 (existing.lambda)(i).await; 803 } else { 804 (extra.lambda)(i - existing.len).await; 805 } 806 } 807 } 808 }}; 809 810 // Internal builder 811 (@build $idx:expr; $( $(if $cond:expr =>)? $body:block ),* ) => {{ 812 let conditions = [ $( true $( && $cond )? ),* ]; 813 let len = conditions.iter().filter(|&&x| x).count(); 814 815 $crate::routine::Tasks { 816 len, 817 lambda: async move |i: usize| { 818 if len == 0 { 819 return; 820 } 821 822 let i = i % len; 823 let mut current = 0usize; 824 825 $crate::tasks!( 826 @dispatch i, current, conditions, 0usize; 827 $( $(if $cond =>)? $body ),* 828 ); 829 830 let _ = (i, &mut current); // suppress lints 831 832 unreachable!() 833 } 834 } 835 }}; 836 837 // Recursive dispatcher 838 (@dispatch $i:expr, $current:ident, $conditions:ident, $idx:expr;) => {}; 839 840 (@dispatch 841 $i:expr, 842 $current:ident, 843 $conditions:ident, 844 $idx:expr; 845 $(if $cond:expr =>)? $body:block 846 $(, $($rest:tt)*)? 847 ) => {{ 848 if $conditions[$idx] { 849 if $i == $current { 850 (async $body).await; 851 return; 852 } 853 $current += 1; 854 } 855 856 $crate::tasks!( 857 @dispatch 858 $i, 859 $current, 860 $conditions, 861 $idx + 1usize; 862 $($($rest)*)? 863 ); 864 }}; 865 } 866 867 /// Test standard behavior of the revenue endpoints 868 pub async fn revenue_routine( 869 wire_gateway: &Router, 870 revenue_api: &Router, 871 debit_acount: &PaytoURI, 872 register: Tasks<impl AsyncFnMut(usize)>, 873 ignore: Tasks<impl AsyncFnMut(usize)>, 874 ) { 875 let currency = &get_currency(revenue_api).await; 876 routine_history::<RevenueIncomingHistory>( 877 &revenue_api.suffix("/history"), 878 tasks!(register; 879 { 880 wire_gateway 881 .post("/admin/add-incoming") 882 .json(json!({ 883 "amount": format!("{currency}:1"), 884 "reserve_pub": EddsaPublicKey::rand(), 885 "debit_account": debit_acount, 886 })) 887 .await 888 .assert_ok_json::<TransferResponse>(); 889 }, 890 { 891 wire_gateway 892 .post("/admin/add-kycauth") 893 .json(json!({ 894 "amount": format!("{currency}:2"), 895 "account_pub": EddsaPublicKey::rand(), 896 "debit_account": debit_acount, 897 })) 898 .await 899 .assert_ok_json::<TransferResponse>(); 900 } 901 ), 902 ignore, 903 ) 904 .await; 905 } 906 907 /// Test standard behavior of the outgoing history endpoint 908 pub async fn out_history_routine( 909 wire_gateway: &Router, 910 register: Tasks<impl AsyncFnMut(usize)>, 911 ignore: Tasks<impl AsyncFnMut(usize)>, 912 ) { 913 routine_history::<OutgoingHistory>(&wire_gateway.suffix("/history/outgoing"), register, ignore) 914 .await; 915 } 916 917 /// Test standard behavior of the incoming history endpoint 918 pub async fn in_history_routine( 919 wire_gateway: &Router, 920 prepared_transfer: &Router, 921 debit_account: &PaytoURI, 922 credit_account: &PaytoURI, 923 register: Tasks<impl AsyncFnMut(usize)>, 924 ignored: Tasks<impl AsyncFnMut(usize)>, 925 ) { 926 let currency = &get_currency(wire_gateway).await; 927 let mut key = Ed25519KeyPair::generate().unwrap(); 928 929 routine_history::<IncomingHistory>( 930 &wire_gateway.suffix("/history/incoming"), 931 tasks!(register; 932 { 933 wire_gateway 934 .post("/admin/add-incoming") 935 .json(json!({ 936 "amount": format!("{currency}:1"), 937 "reserve_pub": EddsaPublicKey::rand(), 938 "debit_account": debit_account, 939 })) 940 .await 941 .assert_ok_json::<TransferResponse>(); 942 }, 943 { 944 key = Ed25519KeyPair::generate().unwrap(); 945 let auth_pub = EddsaPublicKey::try_from(key.public_key().as_ref()).unwrap(); 946 let reserve_pub = EddsaPublicKey::rand(); 947 let amount = Amount::new(currency, 2, 0); 948 prepared_transfer 949 .post("/registration") 950 .json( 951 RegistrationRequest { 952 credit_account: credit_account.clone(), 953 r#type: TransferType::reserve, 954 recurrent: true, 955 credit_amount: amount, 956 alg: PublicKeyAlg::EdDSA, 957 account_pub: reserve_pub, 958 authorization_pub: auth_pub, 959 authorization_sig: EddsaSignature::ZEROED, 960 } 961 .signed(&key) 962 ) 963 .await 964 .assert_ok_json::<RegistrationResponse>(); 965 wire_gateway 966 .post("/admin/add-mapped") 967 .json(json!({ 968 "amount": amount, 969 "authorization_pub": auth_pub, 970 "debit_account": debit_account, 971 })) 972 .await 973 .assert_ok_json::<TransferResponse>(); 974 wire_gateway 975 .post("/admin/add-mapped") 976 .json(json!({ 977 "amount": amount, 978 "authorization_pub": auth_pub, 979 "debit_account": debit_account, 980 })) 981 .await 982 .assert_ok_json::<TransferResponse>(); 983 }, 984 { 985 let auth_pub = EddsaPublicKey::try_from(key.public_key().as_ref()).unwrap(); 986 let reserve_pub = EddsaPublicKey::rand(); 987 prepared_transfer 988 .post("/registration") 989 .json( 990 RegistrationRequest { 991 credit_account: credit_account.clone(), 992 r#type: TransferType::reserve, 993 recurrent: true, 994 credit_amount: Amount::new(currency, 3, 0), 995 alg: PublicKeyAlg::EdDSA, 996 account_pub: reserve_pub, 997 authorization_pub: auth_pub, 998 authorization_sig: EddsaSignature::ZEROED, 999 } 1000 .signed(&key) 1001 ) 1002 .await 1003 .assert_ok_json::<RegistrationResponse>(); 1004 }, 1005 { 1006 wire_gateway 1007 .post("/admin/add-kycauth") 1008 .json(json!({ 1009 "amount": format!("{currency}:4"), 1010 "account_pub": EddsaPublicKey::rand(), 1011 "debit_account": debit_account, 1012 })) 1013 .await 1014 .assert_ok_json::<TransferResponse>(); 1015 }, 1016 { 1017 key = Ed25519KeyPair::generate().unwrap(); 1018 let auth_pub = EddsaPublicKey::try_from(key.public_key().as_ref()).unwrap(); 1019 let account_pub = EddsaPublicKey::rand(); 1020 let amount = Amount::new(currency, 5, 0); 1021 prepared_transfer 1022 .post("/registration") 1023 .json( 1024 RegistrationRequest { 1025 credit_account: credit_account.clone(), 1026 r#type: TransferType::kyc, 1027 recurrent: true, 1028 credit_amount: amount, 1029 alg: PublicKeyAlg::EdDSA, 1030 account_pub, 1031 authorization_pub: auth_pub, 1032 authorization_sig: EddsaSignature::ZEROED, 1033 } 1034 .signed(&key) 1035 ) 1036 .await 1037 .assert_ok_json::<RegistrationResponse>(); 1038 wire_gateway 1039 .post("/admin/add-mapped") 1040 .json(json!({ 1041 "amount": amount, 1042 "authorization_pub": auth_pub, 1043 "debit_account": debit_account, 1044 })) 1045 .await 1046 .assert_ok_json::<TransferResponse>(); 1047 wire_gateway 1048 .post("/admin/add-mapped") 1049 .json(json!({ 1050 "amount": amount, 1051 "authorization_pub": auth_pub, 1052 "debit_account": debit_account, 1053 })) 1054 .await 1055 .assert_ok_json::<TransferResponse>(); 1056 }, 1057 { 1058 let auth_pub = EddsaPublicKey::try_from(key.public_key().as_ref()).unwrap(); 1059 let account_pub = EddsaPublicKey::rand(); 1060 prepared_transfer 1061 .post("/registration") 1062 .json( 1063 RegistrationRequest { 1064 credit_account: credit_account.clone(), 1065 r#type: TransferType::kyc, 1066 recurrent: true, 1067 credit_amount: Amount::new(currency, 6, 0), 1068 alg: PublicKeyAlg::EdDSA, 1069 account_pub, 1070 authorization_pub: auth_pub, 1071 authorization_sig: EddsaSignature::ZEROED, 1072 } 1073 .signed(&key) 1074 ) 1075 .await 1076 .assert_ok_json::<RegistrationResponse>(); 1077 } 1078 ), 1079 ignored, 1080 ) 1081 .await; 1082 1083 // Release an older bank payment after a consumer advances its cursor 1084 let history = wire_gateway.suffix("/history/incoming"); 1085 let incoming = async |cursor| history.get(format!("?limit=10&offset={cursor}")).await; 1086 let register = async |request: &RegistrationRequest| { 1087 prepared_transfer 1088 .post("/registration") 1089 .json(request) 1090 .await 1091 .assert_ok_json::<RegistrationResponse>() 1092 }; 1093 let initial_cursor = latest_id::<IncomingHistory>(&history).await; 1094 let pair = Ed25519KeyPair::generate().unwrap(); 1095 let authorization_pub = EddsaPublicKey::try_from(pair.public_key().as_ref()).unwrap(); 1096 let first_reserve = EddsaPublicKey::rand(); 1097 let unrelated_reserve = EddsaPublicKey::rand(); 1098 let amount = Amount::new(currency, 42, 0); 1099 let registration = RegistrationRequest { 1100 credit_account: credit_account.clone(), 1101 r#type: TransferType::reserve, 1102 recurrent: true, 1103 credit_amount: amount, 1104 alg: PublicKeyAlg::EdDSA, 1105 account_pub: first_reserve, 1106 authorization_pub, 1107 authorization_sig: EddsaSignature::ZEROED, 1108 } 1109 .signed(&pair); 1110 register(®istration).await; 1111 for _ in 0..2 { 1112 wire_gateway 1113 .post("/admin/add-mapped") 1114 .json(json!({ 1115 "amount": amount, 1116 "authorization_pub": authorization_pub, 1117 "debit_account": debit_account, 1118 })) 1119 .await 1120 .assert_ok_json::<TransferResponse>(); 1121 } 1122 wire_gateway 1123 .post("/admin/add-incoming") 1124 .json(json!({ 1125 "amount": amount, 1126 "reserve_pub": unrelated_reserve, 1127 "debit_account": debit_account, 1128 })) 1129 .await 1130 .assert_ok_json::<TransferResponse>(); 1131 let before = incoming(initial_cursor) 1132 .await 1133 .assert_ok_json::<IncomingHistory>(); 1134 assert_eq!(before.incoming_transactions.len(), 2); 1135 for (tx, expected) in before 1136 .incoming_transactions 1137 .iter() 1138 .zip([first_reserve, unrelated_reserve]) 1139 { 1140 assert!( 1141 matches!(tx, IncomingBankTransaction::Reserve { reserve_pub, .. } 1142 if *reserve_pub == expected) 1143 ); 1144 } 1145 let cursor = *before.ids().last().unwrap(); 1146 incoming(cursor).await.assert_no_content(); 1147 let release = RegistrationRequest { 1148 account_pub: EddsaPublicKey::rand(), 1149 ..registration 1150 } 1151 .signed(&pair); 1152 let (response, ()) = tokio::join!( 1153 assert_time(0..5000, async { 1154 history 1155 .get(format!("?limit=10&offset={cursor}&timeout_ms=10000")) 1156 .await 1157 }), 1158 async { 1159 sleep(Duration::from_millis(100)).await; 1160 register(&release).await; 1161 } 1162 ); 1163 let resumed = response.assert_ok_json::<IncomingHistory>(); 1164 let [ 1165 IncomingBankTransaction::Reserve { 1166 row_id, 1167 reserve_pub, 1168 amount: credited, 1169 authorization_pub: Some(auth_pub), 1170 authorization_sig: Some(auth_sig), 1171 .. 1172 }, 1173 ] = resumed.incoming_transactions.as_slice() 1174 else { 1175 panic!("expected exactly one released reserve payment"); 1176 }; 1177 assert_eq!(*reserve_pub, release.account_pub); 1178 assert_eq!(*credited, amount); 1179 assert_eq!(*auth_pub, authorization_pub); 1180 assert!(release.verify(auth_pub, auth_sig)); 1181 let next_cursor = *row_id as i64; 1182 assert!( 1183 next_cursor > cursor, 1184 "released payment must advance the history cursor" 1185 ); 1186 assert_eq!( 1187 incoming(cursor) 1188 .await 1189 .assert_ok_json::<IncomingHistory>() 1190 .incoming_transactions, 1191 resumed.incoming_transactions 1192 ); 1193 incoming(next_cursor).await.assert_no_content(); 1194 } 1195 1196 /// Test standard behavior of the admin add incoming endpoints 1197 pub async fn admin_add_incoming_routine( 1198 wire_gateway: &Router, 1199 prepared_transfer: &Router, 1200 debit_acount: &PaytoURI, 1201 credit_account: &PaytoURI, 1202 ) { 1203 let currency = &get_currency(wire_gateway).await; 1204 for kind in IncomingType::entries { 1205 add_incoming_routine( 1206 wire_gateway, 1207 prepared_transfer, 1208 currency, 1209 *kind, 1210 debit_acount, 1211 credit_account, 1212 ) 1213 .await; 1214 } 1215 } 1216 1217 #[derive(Debug, PartialEq, Eq)] 1218 pub enum Status { 1219 Simple, 1220 Pending, 1221 Bounced, 1222 Incomplete, 1223 Reserve(EddsaPublicKey), 1224 Kyc(EddsaPublicKey), 1225 } 1226 1227 /// Test standard registration behavior of the registration endpoints 1228 pub async fn registration_routine<F1: Future<Output = Vec<Status>>>( 1229 wire_gateway: &Router, 1230 prepared_transfer: &Router, 1231 debit_acount: &PaytoURI, 1232 credit_account: &PaytoURI, 1233 unknown_account: &PaytoURI, 1234 mut in_status: impl FnMut() -> F1, 1235 ) { 1236 pub use Status::*; 1237 let mut check_in = async |state: &[Status]| { 1238 let current = in_status().await; 1239 pretty_assertions::assert_eq!(state, current); 1240 }; 1241 1242 let currency = &get_currency(wire_gateway).await; 1243 let amount = amount(format!("{currency}:42")); 1244 let key_pair1 = Ed25519KeyPair::generate().unwrap(); 1245 let auth_pub1 = EddsaPublicKey::try_from(key_pair1.public_key().as_ref()).unwrap(); 1246 let req = RegistrationRequest { 1247 credit_account: credit_account.clone(), 1248 r#type: TransferType::reserve, 1249 recurrent: false, 1250 credit_amount: amount, 1251 alg: PublicKeyAlg::EdDSA, 1252 account_pub: auth_pub1, 1253 authorization_pub: auth_pub1, 1254 authorization_sig: EddsaSignature::ZEROED, 1255 }; 1256 1257 let register = async |auth_pub: &EddsaPublicKey| { 1258 wire_gateway 1259 .post("/admin/add-mapped") 1260 .json(json!({ 1261 "amount": format!("{currency}:42"), 1262 "authorization_pub": auth_pub, 1263 "debit_account": debit_acount, 1264 })) 1265 .await 1266 }; 1267 1268 /* ----- Registration ----- */ 1269 let routine = async |ty: TransferType, 1270 account_pub: EddsaPublicKey, 1271 recurrent: bool, 1272 fmt: IncomingType| { 1273 let req = RegistrationRequest { 1274 r#type: ty, 1275 account_pub, 1276 recurrent, 1277 ..req.clone() 1278 } 1279 .signed(&key_pair1); 1280 // Valid 1281 let res = prepared_transfer 1282 .post("/registration") 1283 .json(&req) 1284 .await 1285 .assert_ok_json::<RegistrationResponse>(); 1286 1287 // Idempotent 1288 assert_eq!( 1289 res, 1290 prepared_transfer 1291 .post("/registration") 1292 .json(&req) 1293 .await 1294 .assert_ok_json::<RegistrationResponse>() 1295 ); 1296 1297 assert!(!res.subjects.is_empty()); 1298 1299 for sub in res.subjects { 1300 if let TransferSubject::Simple { subject, .. } = sub { 1301 assert_eq!(subject, fmt_in_subject(fmt, &auth_pub1).to_string()); 1302 }; 1303 } 1304 }; 1305 for ty in [TransferType::reserve, TransferType::kyc] { 1306 routine(ty, auth_pub1, false, ty.into()).await; 1307 routine(ty, auth_pub1, true, IncomingType::map).await; 1308 } 1309 1310 let acc_pub1 = EddsaPublicKey::rand(); 1311 for ty in [TransferType::reserve, TransferType::kyc] { 1312 routine(ty, acc_pub1, false, IncomingType::map).await; 1313 routine(ty, acc_pub1, true, IncomingType::map).await; 1314 } 1315 1316 // Bad signature 1317 prepared_transfer 1318 .post("/registration") 1319 .json(&req) 1320 .await 1321 .assert_error(ErrorCode::BANK_BAD_SIGNATURE); 1322 1323 // Unknown account 1324 prepared_transfer 1325 .post("/registration") 1326 .json( 1327 RegistrationRequest { 1328 r#credit_account: unknown_account.clone(), 1329 ..req.clone() 1330 } 1331 .signed(&key_pair1), 1332 ) 1333 .await 1334 .assert_error(ErrorCode::BANK_UNKNOWN_CREDITOR); 1335 1336 // Unsupported payto kind 1337 prepared_transfer 1338 .post("/registration") 1339 .json( 1340 RegistrationRequest { 1341 r#credit_account: UNKNOWN.clone(), 1342 ..req.clone() 1343 } 1344 .signed(&key_pair1), 1345 ) 1346 .await 1347 .assert_error(ErrorCode::GENERIC_PAYTO_URI_MALFORMED); 1348 1349 // Malformed payto 1350 prepared_transfer 1351 .post("/registration") 1352 .json( 1353 RegistrationRequest { 1354 r#credit_account: unsafe { PaytoURI::from_raw("http://email@test.com") }, 1355 ..req.clone() 1356 } 1357 .signed(&key_pair1), 1358 ) 1359 .await 1360 .assert_error(ErrorCode::GENERIC_JSON_INVALID); 1361 1362 // Reserve pub reuse 1363 prepared_transfer 1364 .post("/registration") 1365 .json( 1366 RegistrationRequest { 1367 account_pub: acc_pub1, 1368 ..req.clone() 1369 } 1370 .signed(&key_pair1), 1371 ) 1372 .await 1373 .assert_ok_json::<RegistrationResponse>(); 1374 { 1375 let key_pair = Ed25519KeyPair::generate().unwrap(); 1376 let auth_pub = EddsaPublicKey::try_from(key_pair.public_key().as_ref()).unwrap(); 1377 prepared_transfer 1378 .post("/registration") 1379 .json( 1380 RegistrationRequest { 1381 account_pub: acc_pub1, 1382 authorization_pub: auth_pub, 1383 ..req.clone() 1384 } 1385 .signed(&key_pair), 1386 ) 1387 .await 1388 .assert_error(ErrorCode::BANK_DUPLICATE_RESERVE_PUB_SUBJECT); 1389 } 1390 1391 // Non recurrent accept one then bounce 1392 prepared_transfer 1393 .post("/registration") 1394 .json( 1395 RegistrationRequest { 1396 account_pub: acc_pub1, 1397 ..req.clone() 1398 } 1399 .signed(&key_pair1), 1400 ) 1401 .await 1402 .assert_ok_json::<RegistrationResponse>(); 1403 register(&auth_pub1) 1404 .await 1405 .assert_ok_json::<TransferResponse>(); 1406 check_in(&[Reserve(acc_pub1)]).await; 1407 register(&auth_pub1) 1408 .await 1409 .assert_error(ErrorCode::BANK_TRANSFER_MAPPING_REUSED); 1410 1411 // Again without using mapping 1412 let acc_pub2 = EddsaPublicKey::rand(); 1413 prepared_transfer 1414 .post("/registration") 1415 .json( 1416 RegistrationRequest { 1417 account_pub: acc_pub2, 1418 ..req.clone() 1419 } 1420 .signed(&key_pair1), 1421 ) 1422 .await 1423 .assert_ok_json::<RegistrationResponse>(); 1424 wire_gateway 1425 .post("/admin/add-incoming") 1426 .json(json!({ 1427 "amount": amount, 1428 "reserve_pub": acc_pub2, 1429 "debit_account": debit_acount, 1430 })) 1431 .await 1432 .assert_ok(); 1433 register(&auth_pub1) 1434 .await 1435 .assert_error(ErrorCode::BANK_TRANSFER_MAPPING_REUSED); 1436 check_in(&[Reserve(acc_pub1), Reserve(acc_pub2)]).await; 1437 1438 // Recurrent accept one and delay others 1439 let acc_pub3 = EddsaPublicKey::rand(); 1440 prepared_transfer 1441 .post("/registration") 1442 .json( 1443 RegistrationRequest { 1444 account_pub: acc_pub3, 1445 recurrent: true, 1446 ..req.clone() 1447 } 1448 .signed(&key_pair1), 1449 ) 1450 .await 1451 .assert_ok_json::<RegistrationResponse>(); 1452 for _ in 0..5 { 1453 register(&auth_pub1) 1454 .await 1455 .assert_ok_json::<TransferResponse>(); 1456 } 1457 check_in(&[ 1458 Reserve(acc_pub1), 1459 Reserve(acc_pub2), 1460 Reserve(acc_pub3), 1461 Pending, 1462 Pending, 1463 Pending, 1464 Pending, 1465 ]) 1466 .await; 1467 1468 // Complete pending on recurrent update 1469 let acc_pub4 = EddsaPublicKey::rand(); 1470 prepared_transfer 1471 .post("/registration") 1472 .json( 1473 RegistrationRequest { 1474 r#type: TransferType::kyc, 1475 account_pub: acc_pub4, 1476 recurrent: true, 1477 ..req.clone() 1478 } 1479 .signed(&key_pair1), 1480 ) 1481 .await 1482 .assert_ok_json::<RegistrationResponse>(); 1483 prepared_transfer 1484 .post("/registration") 1485 .json( 1486 RegistrationRequest { 1487 account_pub: acc_pub4, 1488 recurrent: true, 1489 ..req.clone() 1490 } 1491 .signed(&key_pair1), 1492 ) 1493 .await 1494 .assert_ok_json::<RegistrationResponse>(); 1495 check_in(&[ 1496 Reserve(acc_pub1), 1497 Reserve(acc_pub2), 1498 Reserve(acc_pub3), 1499 Kyc(acc_pub4), 1500 Reserve(acc_pub4), 1501 Pending, 1502 Pending, 1503 ]) 1504 .await; 1505 1506 // Kyc key reuse keep pending ones 1507 wire_gateway 1508 .post("/admin/add-kycauth") 1509 .json(json!({ 1510 "amount": amount, 1511 "account_pub": acc_pub4, 1512 "debit_account": debit_acount, 1513 })) 1514 .await 1515 .assert_ok_json::<TransferResponse>(); 1516 check_in(&[ 1517 Reserve(acc_pub1), 1518 Reserve(acc_pub2), 1519 Reserve(acc_pub3), 1520 Kyc(acc_pub4), 1521 Reserve(acc_pub4), 1522 Pending, 1523 Pending, 1524 Kyc(acc_pub4), 1525 ]) 1526 .await; 1527 1528 // Switching to non recurrent cancel pending 1529 let auth_pair = Ed25519KeyPair::generate().unwrap(); 1530 let auth_pub2 = EddsaPublicKey::try_from(auth_pair.public_key().as_ref()).unwrap(); 1531 prepared_transfer 1532 .post("/registration") 1533 .json( 1534 RegistrationRequest { 1535 account_pub: auth_pub2, 1536 authorization_pub: auth_pub2, 1537 recurrent: true, 1538 ..req.clone() 1539 } 1540 .signed(&auth_pair), 1541 ) 1542 .await 1543 .assert_ok_json::<RegistrationResponse>(); 1544 for _ in 0..3 { 1545 register(&auth_pub2) 1546 .await 1547 .assert_ok_json::<TransferResponse>(); 1548 } 1549 check_in(&[ 1550 Reserve(acc_pub1), 1551 Reserve(acc_pub2), 1552 Reserve(acc_pub3), 1553 Kyc(acc_pub4), 1554 Reserve(acc_pub4), 1555 Pending, 1556 Pending, 1557 Kyc(acc_pub4), 1558 Reserve(auth_pub2), 1559 Pending, 1560 Pending, 1561 ]) 1562 .await; 1563 prepared_transfer 1564 .post("/registration") 1565 .json( 1566 RegistrationRequest { 1567 r#type: TransferType::kyc, 1568 account_pub: auth_pub2, 1569 authorization_pub: auth_pub2, 1570 recurrent: false, 1571 ..req.clone() 1572 } 1573 .signed(&auth_pair), 1574 ) 1575 .await 1576 .assert_ok_json::<RegistrationResponse>(); 1577 check_in(&[ 1578 Reserve(acc_pub1), 1579 Reserve(acc_pub2), 1580 Reserve(acc_pub3), 1581 Kyc(acc_pub4), 1582 Reserve(acc_pub4), 1583 Pending, 1584 Pending, 1585 Kyc(acc_pub4), 1586 Reserve(auth_pub2), 1587 Bounced, 1588 Bounced, 1589 ]) 1590 .await; 1591 1592 // Recurrent reserve simple subject 1593 let acc_pub5 = EddsaPublicKey::rand(); 1594 prepared_transfer 1595 .post("/registration") 1596 .json( 1597 RegistrationRequest { 1598 account_pub: acc_pub5, 1599 authorization_pub: auth_pub2, 1600 recurrent: true, 1601 ..req.clone() 1602 } 1603 .signed(&auth_pair), 1604 ) 1605 .await 1606 .assert_ok_json::<RegistrationResponse>(); 1607 wire_gateway 1608 .post("/admin/add-incoming") 1609 .json(json!({ 1610 "amount": amount, 1611 "reserve_pub": acc_pub5, 1612 "debit_account": debit_acount, 1613 })) 1614 .await 1615 .assert_ok(); 1616 register(&auth_pub2) 1617 .await 1618 .assert_ok_json::<TransferResponse>(); 1619 check_in(&[ 1620 Reserve(acc_pub1), 1621 Reserve(acc_pub2), 1622 Reserve(acc_pub3), 1623 Kyc(acc_pub4), 1624 Reserve(acc_pub4), 1625 Pending, 1626 Pending, 1627 Kyc(acc_pub4), 1628 Reserve(auth_pub2), 1629 Bounced, 1630 Bounced, 1631 Reserve(acc_pub5), 1632 Pending, 1633 ]) 1634 .await; 1635 1636 // Recurrent kyc simple subject 1637 prepared_transfer 1638 .post("/registration") 1639 .json( 1640 RegistrationRequest { 1641 r#type: TransferType::kyc, 1642 account_pub: acc_pub5, 1643 authorization_pub: auth_pub2, 1644 ..req.clone() 1645 } 1646 .signed(&auth_pair), 1647 ) 1648 .await 1649 .assert_ok_json::<RegistrationResponse>(); 1650 prepared_transfer 1651 .post("/registration") 1652 .json( 1653 RegistrationRequest { 1654 r#type: TransferType::kyc, 1655 account_pub: acc_pub5, 1656 authorization_pub: auth_pub2, 1657 recurrent: true, 1658 ..req.clone() 1659 } 1660 .signed(&auth_pair), 1661 ) 1662 .await 1663 .assert_ok_json::<RegistrationResponse>(); 1664 let pair = Ed25519KeyPair::generate().unwrap(); 1665 prepared_transfer 1666 .post("/registration") 1667 .json( 1668 RegistrationRequest { 1669 r#type: TransferType::kyc, 1670 account_pub: acc_pub5, 1671 authorization_pub: EddsaPublicKey::try_from(pair.public_key().as_ref()).unwrap(), 1672 recurrent: true, 1673 ..req.clone() 1674 } 1675 .signed(&pair), 1676 ) 1677 .await 1678 .assert_ok_json::<RegistrationResponse>(); 1679 wire_gateway 1680 .post("/admin/add-kycauth") 1681 .json(json!({ 1682 "amount": amount, 1683 "account_pub": acc_pub5, 1684 "debit_account": debit_acount, 1685 })) 1686 .await 1687 .assert_ok(); 1688 for _ in 0..2 { 1689 register(&auth_pub2) 1690 .await 1691 .assert_ok_json::<TransferResponse>(); 1692 } 1693 check_in(&[ 1694 Reserve(acc_pub1), 1695 Reserve(acc_pub2), 1696 Reserve(acc_pub3), 1697 Kyc(acc_pub4), 1698 Reserve(acc_pub4), 1699 Pending, 1700 Pending, 1701 Kyc(acc_pub4), 1702 Reserve(auth_pub2), 1703 Bounced, 1704 Bounced, 1705 Reserve(acc_pub5), 1706 Bounced, 1707 Kyc(acc_pub5), 1708 Kyc(acc_pub5), 1709 Pending, 1710 ]) 1711 .await; 1712 1713 // Kyc without using mapping 1714 1715 /* ----- Unregistration ----- */ 1716 let un_req = Unregistration { 1717 timestamp: TalerTimestamp::Timestamp(Timestamp::now()), 1718 authorization_pub: auth_pub2, 1719 authorization_sig: EddsaSignature::ZEROED, 1720 }; 1721 let signed = un_req.clone().signed(&auth_pair); 1722 1723 // Delete 1724 prepared_transfer 1725 .post("/unregistration") 1726 .json(&signed) 1727 .await 1728 .assert_no_content(); 1729 1730 // Check bounce pending on deletion 1731 check_in(&[ 1732 Reserve(acc_pub1), 1733 Reserve(acc_pub2), 1734 Reserve(acc_pub3), 1735 Kyc(acc_pub4), 1736 Reserve(acc_pub4), 1737 Pending, 1738 Pending, 1739 Kyc(acc_pub4), 1740 Reserve(auth_pub2), 1741 Bounced, 1742 Bounced, 1743 Reserve(acc_pub5), 1744 Bounced, 1745 Kyc(acc_pub5), 1746 Kyc(acc_pub5), 1747 Bounced, 1748 ]) 1749 .await; 1750 1751 // Idempotent 1752 prepared_transfer 1753 .post("/unregistration") 1754 .json(&signed) 1755 .await 1756 .assert_error(ErrorCode::BANK_TRANSACTION_NOT_FOUND); 1757 1758 // Bad signature 1759 prepared_transfer 1760 .post("/unregistration") 1761 .json(&un_req) 1762 .await 1763 .assert_error(ErrorCode::BANK_BAD_SIGNATURE); 1764 1765 // Old timestamp 1766 prepared_transfer 1767 .post("/unregistration") 1768 .json( 1769 Unregistration { 1770 timestamp: TalerTimestamp::Timestamp( 1771 Timestamp::now() - SignedDuration::from_mins(10), 1772 ), 1773 ..un_req.clone() 1774 } 1775 .signed(&auth_pair), 1776 ) 1777 .await 1778 .assert_error(ErrorCode::BANK_OLD_TIMESTAMP); 1779 1780 // Never timestamp 1781 prepared_transfer 1782 .post("/unregistration") 1783 .json( 1784 Unregistration { 1785 timestamp: TalerTimestamp::Never, 1786 ..un_req.clone() 1787 } 1788 .signed(&auth_pair), 1789 ) 1790 .await 1791 .assert_error(ErrorCode::BANK_OLD_TIMESTAMP); 1792 1793 // Future timestamp 1794 prepared_transfer 1795 .post("/unregistration") 1796 .json( 1797 Unregistration { 1798 timestamp: TalerTimestamp::Timestamp( 1799 Timestamp::now() + SignedDuration::from_mins(1), 1800 ), 1801 ..un_req.clone() 1802 } 1803 .signed(&auth_pair), 1804 ) 1805 .await 1806 .assert_error(ErrorCode::BANK_OLD_TIMESTAMP); 1807 1808 // A KYC payment with a reserve's account key must not consume that 1809 // reserve's prepared registration. The mapped reserve is still valid. 1810 let reserve_pair = Ed25519KeyPair::generate().unwrap(); 1811 let authorization_pub = EddsaPublicKey::try_from(reserve_pair.public_key().as_ref()).unwrap(); 1812 let reserve_pub = EddsaPublicKey::rand(); 1813 prepared_transfer 1814 .post("/registration") 1815 .json( 1816 RegistrationRequest { 1817 account_pub: reserve_pub, 1818 authorization_pub, 1819 ..req.clone() 1820 } 1821 .signed(&reserve_pair), 1822 ) 1823 .await 1824 .assert_ok_json::<RegistrationResponse>(); 1825 wire_gateway 1826 .post("/admin/add-kycauth") 1827 .json(json!({ 1828 "amount": amount, 1829 "account_pub": reserve_pub, 1830 "debit_account": debit_acount, 1831 })) 1832 .await 1833 .assert_ok_json::<TransferResponse>(); 1834 register(&authorization_pub) 1835 .await 1836 .assert_ok_json::<TransferResponse>(); 1837 1838 // Re-registering the same key with changed signed information must 1839 // replace the signature retained for the next mapped transfer. 1840 let replacement_pair = Ed25519KeyPair::generate().unwrap(); 1841 let replacement_auth_pub = 1842 EddsaPublicKey::try_from(replacement_pair.public_key().as_ref()).unwrap(); 1843 let replacement_reserve_pub = EddsaPublicKey::rand(); 1844 let original_registration = RegistrationRequest { 1845 account_pub: replacement_reserve_pub, 1846 authorization_pub: replacement_auth_pub, 1847 ..req.clone() 1848 } 1849 .signed(&replacement_pair); 1850 prepared_transfer 1851 .post("/registration") 1852 .json(&original_registration) 1853 .await 1854 .assert_ok_json::<RegistrationResponse>(); 1855 let replacement_registration = RegistrationRequest { 1856 credit_amount: taler_common::types::amount::amount(format!("{currency}:43")), 1857 ..original_registration 1858 } 1859 .signed(&replacement_pair); 1860 prepared_transfer 1861 .post("/registration") 1862 .json(&replacement_registration) 1863 .await 1864 .assert_ok_json::<RegistrationResponse>(); 1865 let before_replacement_transfer = 1866 latest_id::<IncomingHistory>(&wire_gateway.suffix("/history/incoming")).await; 1867 register(&replacement_auth_pub) 1868 .await 1869 .assert_ok_json::<TransferResponse>(); 1870 let latest = wire_gateway 1871 .get(format!( 1872 "/history/incoming?limit=1&offset={before_replacement_transfer}" 1873 )) 1874 .await 1875 .assert_ok_json::<IncomingHistory>(); 1876 assert!(matches!( 1877 &latest.incoming_transactions[0], 1878 IncomingBankTransaction::Reserve { 1879 reserve_pub, 1880 authorization_pub: Some(auth_pub), 1881 authorization_sig: Some(auth_sig), 1882 .. 1883 } if *reserve_pub == replacement_reserve_pub 1884 && *auth_pub == replacement_auth_pub 1885 && replacement_registration.verify(auth_pub, auth_sig) 1886 )); 1887 1888 /* ----- API ----- */ 1889 1890 let history: Vec<_> = wire_gateway 1891 .get("/history/incoming?limit=20") 1892 .await 1893 .assert_ok_json::<IncomingHistory>() 1894 .incoming_transactions 1895 .into_iter() 1896 .map(|tx| { 1897 let (acc_pub, auth_pub, auth_sig) = match tx { 1898 IncomingBankTransaction::Reserve { 1899 reserve_pub, 1900 authorization_pub, 1901 authorization_sig, 1902 .. 1903 } => (reserve_pub, authorization_pub, authorization_sig), 1904 IncomingBankTransaction::Wad { .. } => unreachable!(), 1905 IncomingBankTransaction::Kyc { 1906 account_pub, 1907 authorization_pub, 1908 authorization_sig, 1909 .. 1910 } => (account_pub, authorization_pub, authorization_sig), 1911 }; 1912 assert_eq!(auth_pub.is_some(), auth_sig.is_some()); 1913 (acc_pub, auth_pub) 1914 }) 1915 .collect(); 1916 pretty_assertions::assert_eq!( 1917 history, 1918 [ 1919 (acc_pub1, Some(auth_pub1)), 1920 (acc_pub2, None), 1921 (acc_pub3, Some(auth_pub1)), 1922 (acc_pub4, Some(auth_pub1)), 1923 (acc_pub4, Some(auth_pub1)), 1924 (acc_pub4, None), 1925 (auth_pub2, Some(auth_pub2)), 1926 (acc_pub5, None), 1927 (acc_pub5, None), 1928 (acc_pub5, Some(auth_pub2)), 1929 (reserve_pub, None), 1930 (reserve_pub, Some(authorization_pub)), 1931 (replacement_reserve_pub, Some(replacement_auth_pub)), 1932 ] 1933 ) 1934 }