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