foundry_wallets/wallet_browser/
mod.rs

1pub mod error;
2pub mod server;
3pub mod signer;
4pub mod state;
5
6mod app;
7mod handlers;
8mod queue;
9mod router;
10mod types;
11
12#[cfg(test)]
13mod tests {
14    use std::time::Duration;
15
16    use alloy_primitives::{Address, Bytes, TxHash, TxKind, U256, address};
17    use alloy_rpc_types::TransactionRequest;
18    use axum::http::{HeaderMap, HeaderValue};
19    use tokio::task::JoinHandle;
20    use uuid::Uuid;
21
22    use crate::wallet_browser::{
23        error::BrowserWalletError,
24        server::BrowserWalletServer,
25        types::{
26            BrowserApiResponse, BrowserSignRequest, BrowserSignResponse, BrowserTransactionRequest,
27            BrowserTransactionResponse, Connection, SignRequest, SignType,
28        },
29    };
30
31    const ALICE: Address = address!("0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266");
32    const BOB: Address = address!("0x70997970C51812dc3A010C7d01b50e0d17dc79C8");
33
34    const DEFAULT_TIMEOUT: Duration = Duration::from_secs(1);
35    const DEFAULT_DEVELOPMENT: bool = false;
36
37    #[tokio::test]
38    async fn test_setup_server() {
39        let mut server = create_server();
40        let client = client_with_token(&server);
41
42        // Check initial state
43        assert!(!server.is_connected().await);
44        assert!(!server.open_browser());
45        assert!(server.timeout() == DEFAULT_TIMEOUT);
46
47        // Start server
48        server.start().await.unwrap();
49
50        // Check that the transaction request queue is empty
51        check_transaction_request_queue_empty(&client, &server).await;
52
53        // Stop server
54        server.stop().await.unwrap();
55    }
56
57    #[tokio::test]
58    async fn test_connect_disconnect_wallet() {
59        let mut server = create_server();
60        let client = client_with_token(&server);
61        server.start().await.unwrap();
62
63        // Check that the transaction request queue is empty
64        check_transaction_request_queue_empty(&client, &server).await;
65
66        // Connect Alice's wallet
67        connect_wallet(&client, &server, Connection::new(ALICE, 1)).await;
68
69        // Check connection state
70        let Connection { address, chain_id } =
71            server.get_connection().await.expect("expected an active wallet connection");
72        assert_eq!(address, ALICE);
73        assert_eq!(chain_id, 1);
74
75        // Disconnect wallet
76        disconnect_wallet(&client, &server).await;
77
78        // Check disconnected state
79        assert!(!server.is_connected().await);
80
81        // Connect Bob's wallet
82        connect_wallet(&client, &server, Connection::new(BOB, 42)).await;
83
84        // Check connection state
85        let Connection { address, chain_id } =
86            server.get_connection().await.expect("expected an active wallet connection");
87        assert_eq!(address, BOB);
88        assert_eq!(chain_id, 42);
89
90        // Stop server
91        server.stop().await.unwrap();
92    }
93
94    #[tokio::test]
95    async fn test_switch_wallet() {
96        let mut server = create_server();
97        let client = client_with_token(&server);
98        server.start().await.unwrap();
99
100        // Connect Alice, assert connected
101        connect_wallet(&client, &server, Connection::new(ALICE, 1)).await;
102        let Connection { address, chain_id } =
103            server.get_connection().await.expect("expected an active wallet connection");
104        assert_eq!(address, ALICE);
105        assert_eq!(chain_id, 1);
106
107        // Connect Bob, assert switched
108        connect_wallet(&client, &server, Connection::new(BOB, 42)).await;
109        let Connection { address, chain_id } =
110            server.get_connection().await.expect("expected an active wallet connection");
111        assert_eq!(address, BOB);
112        assert_eq!(chain_id, 42);
113
114        server.stop().await.unwrap();
115    }
116
117    #[tokio::test]
118    async fn test_transaction_response_both_hash_and_error_rejected() {
119        let mut server = create_server();
120        let client = client_with_token(&server);
121        server.start().await.unwrap();
122        connect_wallet(&client, &server, Connection::new(ALICE, 1)).await;
123
124        // Enqueue a tx
125        let (tx_request_id, tx_request) = create_browser_transaction_request();
126        let _handle = wait_for_transaction_signing(&server, tx_request).await;
127        check_transaction_request_content(&client, &server, tx_request_id).await;
128
129        // Wallet posts both hash and error -> should be rejected
130        let resp = client
131            .post(format!("http://localhost:{}/api/transaction/response", server.port()))
132            .json(&BrowserTransactionResponse {
133                id: tx_request_id,
134                hash: Some(TxHash::random()),
135                error: Some("should not have both".into()),
136            })
137            .send()
138            .await
139            .unwrap()
140            .error_for_status()
141            .unwrap();
142
143        let api: BrowserApiResponse<()> = resp.json().await.unwrap();
144        match api {
145            BrowserApiResponse::Error { message } => {
146                assert_eq!(message, "Only one of hash or error can be provided");
147            }
148            _ => panic!("expected error response"),
149        }
150    }
151
152    #[tokio::test]
153    async fn test_transaction_response_neither_hash_nor_error_rejected() {
154        let mut server = create_server();
155        let client = client_with_token(&server);
156        server.start().await.unwrap();
157        connect_wallet(&client, &server, Connection::new(ALICE, 1)).await;
158
159        let (tx_request_id, tx_request) = create_browser_transaction_request();
160        let _handle = wait_for_transaction_signing(&server, tx_request).await;
161        check_transaction_request_content(&client, &server, tx_request_id).await;
162
163        // Neither hash nor error -> rejected
164        let resp = client
165            .post(format!("http://localhost:{}/api/transaction/response", server.port()))
166            .json(&BrowserTransactionResponse { id: tx_request_id, hash: None, error: None })
167            .send()
168            .await
169            .unwrap()
170            .error_for_status()
171            .unwrap();
172
173        let api: BrowserApiResponse<()> = resp.json().await.unwrap();
174        match api {
175            BrowserApiResponse::Error { message } => {
176                assert_eq!(message, "Either hash or error must be provided");
177            }
178            _ => panic!("expected error response"),
179        }
180    }
181
182    #[tokio::test]
183    async fn test_transaction_response_zero_hash_rejected() {
184        let mut server = create_server();
185        let client = client_with_token(&server);
186        server.start().await.unwrap();
187        connect_wallet(&client, &server, Connection::new(ALICE, 1)).await;
188
189        let (tx_request_id, tx_request) = create_browser_transaction_request();
190        let _handle = wait_for_transaction_signing(&server, tx_request).await;
191        check_transaction_request_content(&client, &server, tx_request_id).await;
192
193        // Zero hash -> rejected
194        let zero = TxHash::new([0u8; 32]);
195        let resp = client
196            .post(format!("http://localhost:{}/api/transaction/response", server.port()))
197            .json(&BrowserTransactionResponse { id: tx_request_id, hash: Some(zero), error: None })
198            .send()
199            .await
200            .unwrap()
201            .error_for_status()
202            .unwrap();
203
204        let api: BrowserApiResponse<()> = resp.json().await.unwrap();
205        match api {
206            BrowserApiResponse::Error { message } => {
207                // Message text per your handler; adjust if you use a different string.
208                assert!(
209                    message.contains("Invalid") || message.contains("Malformed"),
210                    "unexpected message: {message}"
211                );
212            }
213            _ => panic!("expected error response"),
214        }
215    }
216
217    #[tokio::test]
218    async fn test_send_transaction_client_accept() {
219        let mut server = create_server();
220        let client = client_with_token(&server);
221        server.start().await.unwrap();
222        connect_wallet(&client, &server, Connection::new(ALICE, 1)).await;
223
224        let (tx_request_id, tx_request) = create_browser_transaction_request();
225        let handle = wait_for_transaction_signing(&server, tx_request).await;
226        check_transaction_request_content(&client, &server, tx_request_id).await;
227
228        // Simulate the wallet accepting and signing the tx
229        let resp = client
230            .post(format!("http://localhost:{}/api/transaction/response", server.port()))
231            .json(&BrowserTransactionResponse {
232                id: tx_request_id,
233                hash: Some(TxHash::random()),
234                error: None,
235            })
236            .send()
237            .await
238            .unwrap()
239            .error_for_status()
240            .unwrap();
241        assert_eq!(resp.status(), reqwest::StatusCode::OK);
242
243        // The join handle should now return the tx hash
244        let res = handle.await.expect("task panicked");
245        match res {
246            Ok(hash) => {
247                assert!(hash != TxHash::new([0; 32]));
248            }
249            other => panic!("expected success, got {other:?}"),
250        }
251    }
252
253    #[tokio::test]
254    async fn test_send_transaction_client_not_requested() {
255        let mut server = create_server();
256        let client = client_with_token(&server);
257        server.start().await.unwrap();
258        connect_wallet(&client, &server, Connection::new(ALICE, 1)).await;
259
260        // Create a random transaction response without a matching request
261        let tx_request_id = Uuid::new_v4();
262
263        // Simulate the wallet sending a response for an unknown request
264        let resp = client
265            .post(format!("http://localhost:{}/api/transaction/response", server.port()))
266            .json(&BrowserTransactionResponse {
267                id: tx_request_id,
268                hash: Some(TxHash::random()),
269                error: None,
270            })
271            .send()
272            .await
273            .unwrap()
274            .error_for_status()
275            .unwrap();
276
277        assert_eq!(resp.status(), reqwest::StatusCode::OK);
278
279        // Assert that no transaction without a matching request is accepted
280        let api: BrowserApiResponse<()> = resp.json().await.unwrap();
281        match api {
282            BrowserApiResponse::Error { message } => {
283                assert_eq!(message, "Unknown transaction id");
284            }
285            _ => panic!("expected error response"),
286        }
287    }
288
289    #[tokio::test]
290    async fn test_send_transaction_invalid_response_format() {
291        let mut server = create_server();
292        let client = client_with_token(&server);
293        server.start().await.unwrap();
294        connect_wallet(&client, &server, Connection::new(ALICE, 1)).await;
295
296        // Simulate the wallet sending a response with an invalid UUID
297        let resp = client
298            .post(format!("http://localhost:{}/api/transaction/response", server.port()))
299            .body(
300                r#"{
301                "id": "invalid-uuid",
302                "hash": "invalid-hash",
303                "error": null
304            }"#,
305            )
306            .header("Content-Type", "application/json")
307            .send()
308            .await
309            .unwrap();
310
311        // The server should respond with a 422 Unprocessable Entity status
312        assert_eq!(resp.status(), reqwest::StatusCode::UNPROCESSABLE_ENTITY);
313    }
314
315    #[tokio::test]
316    async fn test_send_transaction_client_reject() {
317        let mut server = create_server();
318        let client = client_with_token(&server);
319        server.start().await.unwrap();
320        connect_wallet(&client, &server, Connection::new(ALICE, 1)).await;
321
322        // Create a browser transaction request
323        let (tx_request_id, tx_request) = create_browser_transaction_request();
324
325        // Spawn the transaction signing flow in the background
326        let handle = wait_for_transaction_signing(&server, tx_request).await;
327
328        // Check transaction request
329        check_transaction_request_content(&client, &server, tx_request_id).await;
330
331        // Simulate the wallet rejecting the tx
332        let resp = client
333            .post(format!("http://localhost:{}/api/transaction/response", server.port()))
334            .json(&BrowserTransactionResponse {
335                id: tx_request_id,
336                hash: None,
337                error: Some("User rejected the transaction".into()),
338            })
339            .send()
340            .await
341            .unwrap()
342            .error_for_status()
343            .unwrap();
344        assert_eq!(resp.status(), reqwest::StatusCode::OK);
345
346        // The join handle should now return a rejection error
347        let res = handle.await.expect("task panicked");
348        match res {
349            Err(BrowserWalletError::Rejected { operation, reason }) => {
350                assert_eq!(operation, "Transaction");
351                assert_eq!(reason, "User rejected the transaction");
352            }
353            other => panic!("expected rejection, got {other:?}"),
354        }
355    }
356
357    #[tokio::test]
358    async fn test_send_multiple_transaction_requests() {
359        let mut server = create_server();
360        let client = client_with_token(&server);
361        server.start().await.unwrap();
362        connect_wallet(&client, &server, Connection::new(ALICE, 1)).await;
363
364        // Create multiple browser transaction requests
365        let (tx_request_id1, tx_request1) = create_browser_transaction_request();
366        let (tx_request_id2, tx_request2) = create_different_browser_transaction_request();
367
368        // Spawn signing flows for both transactions concurrently
369        let handle1 = wait_for_transaction_signing(&server, tx_request1.clone()).await;
370        let handle2 = wait_for_transaction_signing(&server, tx_request2.clone()).await;
371
372        // Check first transaction request
373        {
374            let resp = client
375                .get(format!("http://localhost:{}/api/transaction/request", server.port()))
376                .send()
377                .await
378                .unwrap();
379
380            let BrowserApiResponse::Ok(pending_tx) =
381                resp.json::<BrowserApiResponse<BrowserTransactionRequest>>().await.unwrap()
382            else {
383                panic!("expected BrowserApiResponse::Ok with a pending transaction");
384            };
385
386            assert_eq!(
387                pending_tx.id, tx_request_id1,
388                "expected the first transaction to be at the front of the queue"
389            );
390            assert_eq!(pending_tx.request.from, tx_request1.request.from);
391            assert_eq!(pending_tx.request.to, tx_request1.request.to);
392            assert_eq!(pending_tx.request.value, tx_request1.request.value);
393        }
394
395        // Simulate the wallet accepting and signing the first transaction
396        let resp1 = client
397            .post(format!("http://localhost:{}/api/transaction/response", server.port()))
398            .json(&BrowserTransactionResponse {
399                id: tx_request_id1,
400                hash: Some(TxHash::random()),
401                error: None,
402            })
403            .send()
404            .await
405            .unwrap()
406            .error_for_status()
407            .unwrap();
408        assert_eq!(resp1.status(), reqwest::StatusCode::OK);
409
410        let res1 = handle1.await.expect("first signing flow panicked");
411        match res1 {
412            Ok(hash) => assert!(!hash.is_zero(), "first tx hash should not be zero"),
413            other => panic!("expected success, got {other:?}"),
414        }
415
416        // Check second transaction request
417        {
418            let resp = client
419                .get(format!("http://localhost:{}/api/transaction/request", server.port()))
420                .send()
421                .await
422                .unwrap();
423
424            let BrowserApiResponse::Ok(pending_tx) =
425                resp.json::<BrowserApiResponse<BrowserTransactionRequest>>().await.unwrap()
426            else {
427                panic!("expected BrowserApiResponse::Ok with a pending transaction");
428            };
429
430            assert_eq!(
431                pending_tx.id, tx_request_id2,
432                "expected the second transaction to be pending after the first one completed"
433            );
434            assert_eq!(pending_tx.request.from, tx_request2.request.from);
435            assert_eq!(pending_tx.request.to, tx_request2.request.to);
436            assert_eq!(pending_tx.request.value, tx_request2.request.value);
437        }
438
439        // Simulate the wallet rejecting the second transaction
440        let resp2 = client
441            .post(format!("http://localhost:{}/api/transaction/response", server.port()))
442            .json(&BrowserTransactionResponse {
443                id: tx_request_id2,
444                hash: None,
445                error: Some("User rejected the transaction".into()),
446            })
447            .send()
448            .await
449            .unwrap()
450            .error_for_status()
451            .unwrap();
452        assert_eq!(resp2.status(), reqwest::StatusCode::OK);
453
454        let res2 = handle2.await.expect("second signing flow panicked");
455        match res2 {
456            Err(BrowserWalletError::Rejected { operation, reason }) => {
457                assert_eq!(operation, "Transaction");
458                assert_eq!(reason, "User rejected the transaction");
459            }
460            other => panic!("expected BrowserWalletError::Rejected, got {other:?}"),
461        }
462
463        check_transaction_request_queue_empty(&client, &server).await;
464
465        server.stop().await.unwrap();
466    }
467
468    #[tokio::test]
469    async fn test_send_sign_response_both_signature_and_error_rejected() {
470        let mut server = create_server();
471        let client = client_with_token(&server);
472        server.start().await.unwrap();
473        connect_wallet(&client, &server, Connection::new(ALICE, 1)).await;
474
475        let (sign_request_id, sign_request) = create_browser_sign_request();
476        let _handle = wait_for_message_signing(&server, sign_request).await;
477        check_sign_request_content(&client, &server, sign_request_id).await;
478
479        // Both signature and error -> should be rejected
480        let resp = client
481            .post(format!("http://localhost:{}/api/signing/response", server.port()))
482            .json(&BrowserSignResponse {
483                id: sign_request_id,
484                signature: Some(Bytes::from("Hello World")),
485                error: Some("Should not have both".into()),
486            })
487            .send()
488            .await
489            .unwrap()
490            .error_for_status()
491            .unwrap();
492
493        let api: BrowserApiResponse<()> = resp.json().await.unwrap();
494        match api {
495            BrowserApiResponse::Error { message } => {
496                assert_eq!(message, "Only one of signature or error can be provided");
497            }
498            _ => panic!("expected error response"),
499        }
500    }
501
502    #[tokio::test]
503    async fn test_send_sign_response_neither_hash_nor_error_rejected() {
504        let mut server = create_server();
505        let client = client_with_token(&server);
506        server.start().await.unwrap();
507        connect_wallet(&client, &server, Connection::new(ALICE, 1)).await;
508
509        let (sign_request_id, sign_request) = create_browser_sign_request();
510        let _handle = wait_for_message_signing(&server, sign_request).await;
511        check_sign_request_content(&client, &server, sign_request_id).await;
512
513        // Neither signature nor error -> rejected
514        let resp = client
515            .post(format!("http://localhost:{}/api/signing/response", server.port()))
516            .json(&BrowserSignResponse { id: sign_request_id, signature: None, error: None })
517            .send()
518            .await
519            .unwrap()
520            .error_for_status()
521            .unwrap();
522
523        let api: BrowserApiResponse<()> = resp.json().await.unwrap();
524        match api {
525            BrowserApiResponse::Error { message } => {
526                assert_eq!(message, "Either signature or error must be provided");
527            }
528            _ => panic!("expected error response"),
529        }
530    }
531
532    #[tokio::test]
533    async fn test_send_sign_client_accept() {
534        let mut server = create_server();
535        let client = client_with_token(&server);
536        server.start().await.unwrap();
537        connect_wallet(&client, &server, Connection::new(ALICE, 1)).await;
538
539        let (sign_request_id, sign_request) = create_browser_sign_request();
540        let handle = wait_for_message_signing(&server, sign_request).await;
541        check_sign_request_content(&client, &server, sign_request_id).await;
542
543        // Simulate the wallet accepting and signing the message
544        let resp = client
545            .post(format!("http://localhost:{}/api/signing/response", server.port()))
546            .json(&BrowserSignResponse {
547                id: sign_request_id,
548                signature: Some(Bytes::from("FakeSignature")),
549                error: None,
550            })
551            .send()
552            .await
553            .unwrap()
554            .error_for_status()
555            .unwrap();
556        assert_eq!(resp.status(), reqwest::StatusCode::OK);
557
558        // The join handle should now return the signature
559        let res = handle.await.expect("task panicked");
560        match res {
561            Ok(signature) => {
562                assert_eq!(signature, Bytes::from("FakeSignature"));
563            }
564            other => panic!("expected success, got {other:?}"),
565        }
566    }
567
568    #[tokio::test]
569    async fn test_send_sign_client_not_requested() {
570        let mut server = create_server();
571        let client = client_with_token(&server);
572        server.start().await.unwrap();
573        connect_wallet(&client, &server, Connection::new(ALICE, 1)).await;
574
575        // Create a random signing response without a matching request
576        let sign_request_id = Uuid::new_v4();
577
578        // Simulate the wallet sending a response for an unknown request
579        let resp = client
580            .post(format!("http://localhost:{}/api/signing/response", server.port()))
581            .json(&BrowserSignResponse {
582                id: sign_request_id,
583                signature: Some(Bytes::from("FakeSignature")),
584                error: None,
585            })
586            .send()
587            .await
588            .unwrap()
589            .error_for_status()
590            .unwrap();
591
592        assert_eq!(resp.status(), reqwest::StatusCode::OK);
593
594        // Assert that no signing response without a matching request is accepted
595        let api: BrowserApiResponse<()> = resp.json().await.unwrap();
596        match api {
597            BrowserApiResponse::Error { message } => {
598                assert_eq!(message, "Unknown signing request id");
599            }
600            _ => panic!("expected error response"),
601        }
602    }
603
604    #[tokio::test]
605    async fn test_send_sign_invalid_response_format() {
606        let mut server = create_server();
607        let client = client_with_token(&server);
608        server.start().await.unwrap();
609        connect_wallet(&client, &server, Connection::new(ALICE, 1)).await;
610
611        // Simulate the wallet sending a response with an invalid UUID
612        let resp = client
613            .post(format!("http://localhost:{}/api/signing/response", server.port()))
614            .body(
615                r#"{
616                "id": "invalid-uuid",
617                "signature": "invalid-signature",
618                "error": null
619            }"#,
620            )
621            .header("Content-Type", "application/json")
622            .send()
623            .await
624            .unwrap();
625
626        // The server should respond with a 422 Unprocessable Entity status
627        assert_eq!(resp.status(), reqwest::StatusCode::UNPROCESSABLE_ENTITY);
628    }
629
630    #[tokio::test]
631    async fn test_send_sign_client_reject() {
632        let mut server = create_server();
633        let client = client_with_token(&server);
634        server.start().await.unwrap();
635        connect_wallet(&client, &server, Connection::new(ALICE, 1)).await;
636
637        let (sign_request_id, sign_request) = create_browser_sign_request();
638        let handle = wait_for_message_signing(&server, sign_request).await;
639        check_sign_request_content(&client, &server, sign_request_id).await;
640
641        // Simulate the wallet rejecting the signing request
642        let resp = client
643            .post(format!("http://localhost:{}/api/signing/response", server.port()))
644            .json(&BrowserSignResponse {
645                id: sign_request_id,
646                signature: None,
647                error: Some("User rejected the signing request".into()),
648            })
649            .send()
650            .await
651            .unwrap()
652            .error_for_status()
653            .unwrap();
654        assert_eq!(resp.status(), reqwest::StatusCode::OK);
655
656        // The join handle should now return a rejection error
657        let res = handle.await.expect("task panicked");
658        match res {
659            Err(BrowserWalletError::Rejected { operation, reason }) => {
660                assert_eq!(operation, "Signing");
661                assert_eq!(reason, "User rejected the signing request");
662            }
663            other => panic!("expected rejection, got {other:?}"),
664        }
665    }
666
667    #[tokio::test]
668    async fn test_send_multiple_sign_requests() {
669        let mut server = create_server();
670        let client = client_with_token(&server);
671        server.start().await.unwrap();
672        connect_wallet(&client, &server, Connection::new(ALICE, 1)).await;
673
674        // Create multiple browser sign requests
675        let (sign_request_id1, sign_request1) = create_browser_sign_request();
676        let (sign_request_id2, sign_request2) = create_different_browser_sign_request();
677
678        // Spawn signing flows for both sign requests concurrently
679        let handle1 = wait_for_message_signing(&server, sign_request1.clone()).await;
680        let handle2 = wait_for_message_signing(&server, sign_request2.clone()).await;
681
682        // Check first sign request
683        {
684            let resp = client
685                .get(format!("http://localhost:{}/api/signing/request", server.port()))
686                .send()
687                .await
688                .unwrap();
689
690            let BrowserApiResponse::Ok(pending_sign) =
691                resp.json::<BrowserApiResponse<BrowserSignRequest>>().await.unwrap()
692            else {
693                panic!("expected BrowserApiResponse::Ok with a pending sign request");
694            };
695
696            assert_eq!(pending_sign.id, sign_request_id1);
697            assert_eq!(pending_sign.sign_type, sign_request1.sign_type);
698            assert_eq!(pending_sign.request.address, sign_request1.request.address);
699            assert_eq!(pending_sign.request.message, sign_request1.request.message);
700        }
701
702        // Simulate the wallet accepting and signing the first sign request
703        let resp1 = client
704            .post(format!("http://localhost:{}/api/signing/response", server.port()))
705            .json(&BrowserSignResponse {
706                id: sign_request_id1,
707                signature: Some(Bytes::from("Signature1")),
708                error: None,
709            })
710            .send()
711            .await
712            .unwrap()
713            .error_for_status()
714            .unwrap();
715        assert_eq!(resp1.status(), reqwest::StatusCode::OK);
716
717        let res1 = handle1.await.expect("first signing flow panicked");
718        match res1 {
719            Ok(signature) => assert_eq!(signature, Bytes::from("Signature1")),
720            other => panic!("expected success, got {other:?}"),
721        }
722
723        // Check second sign request
724        {
725            let resp = client
726                .get(format!("http://localhost:{}/api/signing/request", server.port()))
727                .send()
728                .await
729                .unwrap();
730
731            let BrowserApiResponse::Ok(pending_sign) =
732                resp.json::<BrowserApiResponse<BrowserSignRequest>>().await.unwrap()
733            else {
734                panic!("expected BrowserApiResponse::Ok with a pending sign request");
735            };
736
737            assert_eq!(pending_sign.id, sign_request_id2,);
738            assert_eq!(pending_sign.sign_type, sign_request2.sign_type);
739            assert_eq!(pending_sign.request.address, sign_request2.request.address);
740            assert_eq!(pending_sign.request.message, sign_request2.request.message);
741        }
742
743        // Simulate the wallet rejecting the second sign request
744        let resp2 = client
745            .post(format!("http://localhost:{}/api/signing/response", server.port()))
746            .json(&BrowserSignResponse {
747                id: sign_request_id2,
748                signature: None,
749                error: Some("User rejected the signing request".into()),
750            })
751            .send()
752            .await
753            .unwrap()
754            .error_for_status()
755            .unwrap();
756        assert_eq!(resp2.status(), reqwest::StatusCode::OK);
757
758        let res2 = handle2.await.expect("second signing flow panicked");
759        match res2 {
760            Err(BrowserWalletError::Rejected { operation, reason }) => {
761                assert_eq!(operation, "Signing");
762                assert_eq!(reason, "User rejected the signing request");
763            }
764            other => panic!("expected BrowserWalletError::Rejected, got {other:?}"),
765        }
766
767        check_sign_request_queue_empty(&client, &server).await;
768
769        server.stop().await.unwrap();
770    }
771
772    /// Helper to create a default browser wallet server.
773    fn create_server() -> BrowserWalletServer {
774        BrowserWalletServer::new(0, false, DEFAULT_TIMEOUT, DEFAULT_DEVELOPMENT)
775    }
776
777    /// Helper to create a reqwest client with the session token header.
778    fn client_with_token(server: &BrowserWalletServer) -> reqwest::Client {
779        let mut headers = HeaderMap::new();
780        headers.insert("X-Session-Token", HeaderValue::from_str(server.session_token()).unwrap());
781        reqwest::Client::builder().default_headers(headers).build().unwrap()
782    }
783
784    /// Helper to connect a wallet to the server.
785    async fn connect_wallet(
786        client: &reqwest::Client,
787        server: &BrowserWalletServer,
788        connection: Connection,
789    ) {
790        let resp = client
791            .post(format!("http://localhost:{}/api/connection", server.port()))
792            .json(&connection)
793            .send();
794        assert!(resp.await.is_ok());
795    }
796
797    /// Helper to disconnect a wallet from the server.
798    async fn disconnect_wallet(client: &reqwest::Client, server: &BrowserWalletServer) {
799        let resp = client
800            .post(format!("http://localhost:{}/api/connection", server.port()))
801            .json(&Option::<Connection>::None)
802            .send();
803        assert!(resp.await.is_ok());
804    }
805
806    /// Spawn the transaction signing flow in the background and return the join handle.
807    async fn wait_for_transaction_signing(
808        server: &BrowserWalletServer,
809        tx_request: BrowserTransactionRequest,
810    ) -> JoinHandle<Result<TxHash, BrowserWalletError>> {
811        // Spawn the signing flow in the background
812        let browser_server = server.clone();
813        let join_handle =
814            tokio::spawn(async move { browser_server.request_transaction(tx_request).await });
815        tokio::task::yield_now().await;
816        tokio::time::sleep(Duration::from_millis(100)).await;
817
818        join_handle
819    }
820
821    /// Spawn the message signing flow in the background and return the join handle.
822    async fn wait_for_message_signing(
823        server: &BrowserWalletServer,
824        sign_request: BrowserSignRequest,
825    ) -> JoinHandle<Result<Bytes, BrowserWalletError>> {
826        // Spawn the signing flow in the background
827        let browser_server = server.clone();
828        let join_handle =
829            tokio::spawn(async move { browser_server.request_signing(sign_request).await });
830        tokio::task::yield_now().await;
831        tokio::time::sleep(Duration::from_millis(100)).await;
832
833        join_handle
834    }
835
836    /// Create a simple browser transaction request.
837    fn create_browser_transaction_request() -> (Uuid, BrowserTransactionRequest) {
838        let id = Uuid::new_v4();
839        let tx = BrowserTransactionRequest {
840            id,
841            request: TransactionRequest {
842                from: Some(ALICE),
843                to: Some(TxKind::Call(BOB)),
844                value: Some(U256::from(1000)),
845                ..Default::default()
846            },
847        };
848        (id, tx)
849    }
850
851    /// Create a different browser transaction request (from the first one).
852    fn create_different_browser_transaction_request() -> (Uuid, BrowserTransactionRequest) {
853        let id = Uuid::new_v4();
854        let tx = BrowserTransactionRequest {
855            id,
856            request: TransactionRequest {
857                from: Some(BOB),
858                to: Some(TxKind::Call(ALICE)),
859                value: Some(U256::from(2000)),
860                ..Default::default()
861            },
862        };
863        (id, tx)
864    }
865
866    /// Create a simple browser sign request.
867    fn create_browser_sign_request() -> (Uuid, BrowserSignRequest) {
868        let id = Uuid::new_v4();
869        let req = BrowserSignRequest {
870            id,
871            sign_type: SignType::PersonalSign,
872            request: SignRequest { message: "Hello, world!".into(), address: ALICE },
873        };
874        (id, req)
875    }
876
877    /// Create a different browser sign request (from the first one).
878    fn create_different_browser_sign_request() -> (Uuid, BrowserSignRequest) {
879        let id = Uuid::new_v4();
880        let req = BrowserSignRequest {
881            id,
882            sign_type: SignType::SignTypedDataV4,
883            request: SignRequest { message: "Different message".into(), address: BOB },
884        };
885        (id, req)
886    }
887
888    /// Check that the transaction request queue is empty, if not panic.
889    async fn check_transaction_request_queue_empty(
890        client: &reqwest::Client,
891        server: &BrowserWalletServer,
892    ) {
893        let resp = client
894            .get(format!("http://localhost:{}/api/transaction/request", server.port()))
895            .send()
896            .await
897            .unwrap();
898
899        let BrowserApiResponse::Error { message } =
900            resp.json::<BrowserApiResponse<BrowserTransactionRequest>>().await.unwrap()
901        else {
902            panic!("expected BrowserApiResponse::Error (no pending transaction), but got Ok");
903        };
904
905        assert_eq!(message, "No pending transaction request");
906    }
907
908    /// Check that the transaction request matches the expected request ID and fields.
909    async fn check_transaction_request_content(
910        client: &reqwest::Client,
911        server: &BrowserWalletServer,
912        tx_request_id: Uuid,
913    ) {
914        let resp = client
915            .get(format!("http://localhost:{}/api/transaction/request", server.port()))
916            .send()
917            .await
918            .unwrap();
919
920        let BrowserApiResponse::Ok(pending_tx) =
921            resp.json::<BrowserApiResponse<BrowserTransactionRequest>>().await.unwrap()
922        else {
923            panic!("expected BrowserApiResponse::Ok with a pending transaction");
924        };
925
926        assert_eq!(pending_tx.id, tx_request_id);
927        assert_eq!(pending_tx.request.from, Some(ALICE));
928        assert_eq!(pending_tx.request.to, Some(TxKind::Call(BOB)));
929        assert_eq!(pending_tx.request.value, Some(U256::from(1000)));
930    }
931
932    /// Check that the sign request queue is empty, if not panic.
933    async fn check_sign_request_queue_empty(
934        client: &reqwest::Client,
935        server: &BrowserWalletServer,
936    ) {
937        let resp = client
938            .get(format!("http://localhost:{}/api/signing/request", server.port()))
939            .send()
940            .await
941            .unwrap();
942
943        let BrowserApiResponse::Error { message } =
944            resp.json::<BrowserApiResponse<BrowserSignRequest>>().await.unwrap()
945        else {
946            panic!("expected BrowserApiResponse::Error (no pending signing request), but got Ok");
947        };
948
949        assert_eq!(message, "No pending signing request");
950    }
951
952    /// Check that the sign request matches the expected request ID and fields.
953    async fn check_sign_request_content(
954        client: &reqwest::Client,
955        server: &BrowserWalletServer,
956        sign_request_id: Uuid,
957    ) {
958        let resp = client
959            .get(format!("http://localhost:{}/api/signing/request", server.port()))
960            .send()
961            .await
962            .unwrap();
963
964        let BrowserApiResponse::Ok(pending_req) =
965            resp.json::<BrowserApiResponse<BrowserSignRequest>>().await.unwrap()
966        else {
967            panic!("expected BrowserApiResponse::Ok with a pending signing request");
968        };
969
970        assert_eq!(pending_req.id, sign_request_id);
971        assert_eq!(pending_req.sign_type, SignType::PersonalSign);
972        assert_eq!(pending_req.request.address, ALICE);
973        assert_eq!(pending_req.request.message, "Hello, world!");
974    }
975}