Skip to main content

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_network::{Ethereum, Network, TransactionBuilder};
17    use alloy_primitives::{Address, Bytes, TxHash, TxKind, U256, address};
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::<Ethereum>();
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::<Ethereum>();
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::<Ethereum>();
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::<Ethereum>();
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::<Ethereum>();
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::<Ethereum>();
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::<Ethereum>();
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::<Ethereum>();
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::<Ethereum>();
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::<Ethereum>();
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::<Ethereum>();
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) = resp
381                .json::<BrowserApiResponse<BrowserTransactionRequest<Ethereum>>>()
382                .await
383                .unwrap()
384            else {
385                panic!("expected BrowserApiResponse::Ok with a pending transaction");
386            };
387
388            assert_eq!(
389                pending_tx.id, tx_request_id1,
390                "expected the first transaction to be at the front of the queue"
391            );
392            assert_eq!(pending_tx.request.from, tx_request1.request.from);
393            assert_eq!(pending_tx.request.to, tx_request1.request.to);
394            assert_eq!(pending_tx.request.value, tx_request1.request.value);
395        }
396
397        // Simulate the wallet accepting and signing the first transaction
398        let resp1 = client
399            .post(format!("http://localhost:{}/api/transaction/response", server.port()))
400            .json(&BrowserTransactionResponse {
401                id: tx_request_id1,
402                hash: Some(TxHash::random()),
403                error: None,
404            })
405            .send()
406            .await
407            .unwrap()
408            .error_for_status()
409            .unwrap();
410        assert_eq!(resp1.status(), reqwest::StatusCode::OK);
411
412        let res1 = handle1.await.expect("first signing flow panicked");
413        match res1 {
414            Ok(hash) => assert!(!hash.is_zero(), "first tx hash should not be zero"),
415            other => panic!("expected success, got {other:?}"),
416        }
417
418        // Check second transaction request
419        {
420            let resp = client
421                .get(format!("http://localhost:{}/api/transaction/request", server.port()))
422                .send()
423                .await
424                .unwrap();
425
426            let BrowserApiResponse::Ok(pending_tx) = resp
427                .json::<BrowserApiResponse<BrowserTransactionRequest<Ethereum>>>()
428                .await
429                .unwrap()
430            else {
431                panic!("expected BrowserApiResponse::Ok with a pending transaction");
432            };
433
434            assert_eq!(
435                pending_tx.id, tx_request_id2,
436                "expected the second transaction to be pending after the first one completed"
437            );
438            assert_eq!(pending_tx.request.from, tx_request2.request.from);
439            assert_eq!(pending_tx.request.to, tx_request2.request.to);
440            assert_eq!(pending_tx.request.value, tx_request2.request.value);
441        }
442
443        // Simulate the wallet rejecting the second transaction
444        let resp2 = client
445            .post(format!("http://localhost:{}/api/transaction/response", server.port()))
446            .json(&BrowserTransactionResponse {
447                id: tx_request_id2,
448                hash: None,
449                error: Some("User rejected the transaction".into()),
450            })
451            .send()
452            .await
453            .unwrap()
454            .error_for_status()
455            .unwrap();
456        assert_eq!(resp2.status(), reqwest::StatusCode::OK);
457
458        let res2 = handle2.await.expect("second signing flow panicked");
459        match res2 {
460            Err(BrowserWalletError::Rejected { operation, reason }) => {
461                assert_eq!(operation, "Transaction");
462                assert_eq!(reason, "User rejected the transaction");
463            }
464            other => panic!("expected BrowserWalletError::Rejected, got {other:?}"),
465        }
466
467        check_transaction_request_queue_empty(&client, &server).await;
468
469        server.stop().await.unwrap();
470    }
471
472    #[tokio::test]
473    async fn test_send_sign_response_both_signature_and_error_rejected() {
474        let mut server = create_server::<Ethereum>();
475        let client = client_with_token(&server);
476        server.start().await.unwrap();
477        connect_wallet(&client, &server, Connection::new(ALICE, 1)).await;
478
479        let (sign_request_id, sign_request) = create_browser_sign_request();
480        let _handle = wait_for_message_signing(&server, sign_request).await;
481        check_sign_request_content(&client, &server, sign_request_id).await;
482
483        // Both signature and error -> should be rejected
484        let resp = client
485            .post(format!("http://localhost:{}/api/signing/response", server.port()))
486            .json(&BrowserSignResponse {
487                id: sign_request_id,
488                signature: Some(Bytes::from("Hello World")),
489                error: Some("Should not have both".into()),
490            })
491            .send()
492            .await
493            .unwrap()
494            .error_for_status()
495            .unwrap();
496
497        let api: BrowserApiResponse<()> = resp.json().await.unwrap();
498        match api {
499            BrowserApiResponse::Error { message } => {
500                assert_eq!(message, "Only one of signature or error can be provided");
501            }
502            _ => panic!("expected error response"),
503        }
504    }
505
506    #[tokio::test]
507    async fn test_send_sign_response_neither_hash_nor_error_rejected() {
508        let mut server = create_server::<Ethereum>();
509        let client = client_with_token(&server);
510        server.start().await.unwrap();
511        connect_wallet(&client, &server, Connection::new(ALICE, 1)).await;
512
513        let (sign_request_id, sign_request) = create_browser_sign_request();
514        let _handle = wait_for_message_signing(&server, sign_request).await;
515        check_sign_request_content(&client, &server, sign_request_id).await;
516
517        // Neither signature nor error -> rejected
518        let resp = client
519            .post(format!("http://localhost:{}/api/signing/response", server.port()))
520            .json(&BrowserSignResponse { id: sign_request_id, signature: None, error: None })
521            .send()
522            .await
523            .unwrap()
524            .error_for_status()
525            .unwrap();
526
527        let api: BrowserApiResponse<()> = resp.json().await.unwrap();
528        match api {
529            BrowserApiResponse::Error { message } => {
530                assert_eq!(message, "Either signature or error must be provided");
531            }
532            _ => panic!("expected error response"),
533        }
534    }
535
536    #[tokio::test]
537    async fn test_send_sign_client_accept() {
538        let mut server = create_server::<Ethereum>();
539        let client = client_with_token(&server);
540        server.start().await.unwrap();
541        connect_wallet(&client, &server, Connection::new(ALICE, 1)).await;
542
543        let (sign_request_id, sign_request) = create_browser_sign_request();
544        let handle = wait_for_message_signing(&server, sign_request).await;
545        check_sign_request_content(&client, &server, sign_request_id).await;
546
547        // Simulate the wallet accepting and signing the message
548        let resp = client
549            .post(format!("http://localhost:{}/api/signing/response", server.port()))
550            .json(&BrowserSignResponse {
551                id: sign_request_id,
552                signature: Some(Bytes::from("FakeSignature")),
553                error: None,
554            })
555            .send()
556            .await
557            .unwrap()
558            .error_for_status()
559            .unwrap();
560        assert_eq!(resp.status(), reqwest::StatusCode::OK);
561
562        // The join handle should now return the signature
563        let res = handle.await.expect("task panicked");
564        match res {
565            Ok(signature) => {
566                assert_eq!(signature, Bytes::from("FakeSignature"));
567            }
568            other => panic!("expected success, got {other:?}"),
569        }
570    }
571
572    #[tokio::test]
573    async fn test_send_sign_client_not_requested() {
574        let mut server = create_server::<Ethereum>();
575        let client = client_with_token(&server);
576        server.start().await.unwrap();
577        connect_wallet(&client, &server, Connection::new(ALICE, 1)).await;
578
579        // Create a random signing response without a matching request
580        let sign_request_id = Uuid::new_v4();
581
582        // Simulate the wallet sending a response for an unknown request
583        let resp = client
584            .post(format!("http://localhost:{}/api/signing/response", server.port()))
585            .json(&BrowserSignResponse {
586                id: sign_request_id,
587                signature: Some(Bytes::from("FakeSignature")),
588                error: None,
589            })
590            .send()
591            .await
592            .unwrap()
593            .error_for_status()
594            .unwrap();
595
596        assert_eq!(resp.status(), reqwest::StatusCode::OK);
597
598        // Assert that no signing response without a matching request is accepted
599        let api: BrowserApiResponse<()> = resp.json().await.unwrap();
600        match api {
601            BrowserApiResponse::Error { message } => {
602                assert_eq!(message, "Unknown signing request id");
603            }
604            _ => panic!("expected error response"),
605        }
606    }
607
608    #[tokio::test]
609    async fn test_send_sign_invalid_response_format() {
610        let mut server = create_server::<Ethereum>();
611        let client = client_with_token(&server);
612        server.start().await.unwrap();
613        connect_wallet(&client, &server, Connection::new(ALICE, 1)).await;
614
615        // Simulate the wallet sending a response with an invalid UUID
616        let resp = client
617            .post(format!("http://localhost:{}/api/signing/response", server.port()))
618            .body(
619                r#"{
620                "id": "invalid-uuid",
621                "signature": "invalid-signature",
622                "error": null
623            }"#,
624            )
625            .header("Content-Type", "application/json")
626            .send()
627            .await
628            .unwrap();
629
630        // The server should respond with a 422 Unprocessable Entity status
631        assert_eq!(resp.status(), reqwest::StatusCode::UNPROCESSABLE_ENTITY);
632    }
633
634    #[tokio::test]
635    async fn test_send_sign_client_reject() {
636        let mut server = create_server::<Ethereum>();
637        let client = client_with_token(&server);
638        server.start().await.unwrap();
639        connect_wallet(&client, &server, Connection::new(ALICE, 1)).await;
640
641        let (sign_request_id, sign_request) = create_browser_sign_request();
642        let handle = wait_for_message_signing(&server, sign_request).await;
643        check_sign_request_content(&client, &server, sign_request_id).await;
644
645        // Simulate the wallet rejecting the signing request
646        let resp = client
647            .post(format!("http://localhost:{}/api/signing/response", server.port()))
648            .json(&BrowserSignResponse {
649                id: sign_request_id,
650                signature: None,
651                error: Some("User rejected the signing request".into()),
652            })
653            .send()
654            .await
655            .unwrap()
656            .error_for_status()
657            .unwrap();
658        assert_eq!(resp.status(), reqwest::StatusCode::OK);
659
660        // The join handle should now return a rejection error
661        let res = handle.await.expect("task panicked");
662        match res {
663            Err(BrowserWalletError::Rejected { operation, reason }) => {
664                assert_eq!(operation, "Signing");
665                assert_eq!(reason, "User rejected the signing request");
666            }
667            other => panic!("expected rejection, got {other:?}"),
668        }
669    }
670
671    #[tokio::test]
672    async fn test_send_multiple_sign_requests() {
673        let mut server = create_server::<Ethereum>();
674        let client = client_with_token(&server);
675        server.start().await.unwrap();
676        connect_wallet(&client, &server, Connection::new(ALICE, 1)).await;
677
678        // Create multiple browser sign requests
679        let (sign_request_id1, sign_request1) = create_browser_sign_request();
680        let (sign_request_id2, sign_request2) = create_different_browser_sign_request();
681
682        // Spawn signing flows for both sign requests concurrently
683        let handle1 = wait_for_message_signing(&server, sign_request1.clone()).await;
684        let handle2 = wait_for_message_signing(&server, sign_request2.clone()).await;
685
686        // Check first sign request
687        {
688            let resp = client
689                .get(format!("http://localhost:{}/api/signing/request", server.port()))
690                .send()
691                .await
692                .unwrap();
693
694            let BrowserApiResponse::Ok(pending_sign) =
695                resp.json::<BrowserApiResponse<BrowserSignRequest>>().await.unwrap()
696            else {
697                panic!("expected BrowserApiResponse::Ok with a pending sign request");
698            };
699
700            assert_eq!(pending_sign.id, sign_request_id1);
701            assert_eq!(pending_sign.sign_type, sign_request1.sign_type);
702            assert_eq!(pending_sign.request.address, sign_request1.request.address);
703            assert_eq!(pending_sign.request.message, sign_request1.request.message);
704        }
705
706        // Simulate the wallet accepting and signing the first sign request
707        let resp1 = client
708            .post(format!("http://localhost:{}/api/signing/response", server.port()))
709            .json(&BrowserSignResponse {
710                id: sign_request_id1,
711                signature: Some(Bytes::from("Signature1")),
712                error: None,
713            })
714            .send()
715            .await
716            .unwrap()
717            .error_for_status()
718            .unwrap();
719        assert_eq!(resp1.status(), reqwest::StatusCode::OK);
720
721        let res1 = handle1.await.expect("first signing flow panicked");
722        match res1 {
723            Ok(signature) => assert_eq!(signature, Bytes::from("Signature1")),
724            other => panic!("expected success, got {other:?}"),
725        }
726
727        // Check second sign request
728        {
729            let resp = client
730                .get(format!("http://localhost:{}/api/signing/request", server.port()))
731                .send()
732                .await
733                .unwrap();
734
735            let BrowserApiResponse::Ok(pending_sign) =
736                resp.json::<BrowserApiResponse<BrowserSignRequest>>().await.unwrap()
737            else {
738                panic!("expected BrowserApiResponse::Ok with a pending sign request");
739            };
740
741            assert_eq!(pending_sign.id, sign_request_id2,);
742            assert_eq!(pending_sign.sign_type, sign_request2.sign_type);
743            assert_eq!(pending_sign.request.address, sign_request2.request.address);
744            assert_eq!(pending_sign.request.message, sign_request2.request.message);
745        }
746
747        // Simulate the wallet rejecting the second sign request
748        let resp2 = client
749            .post(format!("http://localhost:{}/api/signing/response", server.port()))
750            .json(&BrowserSignResponse {
751                id: sign_request_id2,
752                signature: None,
753                error: Some("User rejected the signing request".into()),
754            })
755            .send()
756            .await
757            .unwrap()
758            .error_for_status()
759            .unwrap();
760        assert_eq!(resp2.status(), reqwest::StatusCode::OK);
761
762        let res2 = handle2.await.expect("second signing flow panicked");
763        match res2 {
764            Err(BrowserWalletError::Rejected { operation, reason }) => {
765                assert_eq!(operation, "Signing");
766                assert_eq!(reason, "User rejected the signing request");
767            }
768            other => panic!("expected BrowserWalletError::Rejected, got {other:?}"),
769        }
770
771        check_sign_request_queue_empty(&client, &server).await;
772
773        server.stop().await.unwrap();
774    }
775
776    /// Helper to create a default browser wallet server.
777    fn create_server<N: Network>() -> BrowserWalletServer<N> {
778        BrowserWalletServer::new(0, false, DEFAULT_TIMEOUT, DEFAULT_DEVELOPMENT)
779    }
780
781    /// Helper to create a reqwest client with the session token header.
782    fn client_with_token<N: Network>(server: &BrowserWalletServer<N>) -> reqwest::Client {
783        let mut headers = HeaderMap::new();
784        headers.insert("X-Session-Token", HeaderValue::from_str(server.session_token()).unwrap());
785        reqwest::Client::builder().default_headers(headers).build().unwrap()
786    }
787
788    /// Helper to connect a wallet to the server.
789    async fn connect_wallet<N: Network>(
790        client: &reqwest::Client,
791        server: &BrowserWalletServer<N>,
792        connection: Connection,
793    ) {
794        let resp = client
795            .post(format!("http://localhost:{}/api/connection", server.port()))
796            .json(&connection)
797            .send();
798        assert!(resp.await.is_ok());
799    }
800
801    /// Helper to disconnect a wallet from the server.
802    async fn disconnect_wallet<N: Network>(
803        client: &reqwest::Client,
804        server: &BrowserWalletServer<N>,
805    ) {
806        let resp = client
807            .post(format!("http://localhost:{}/api/connection", server.port()))
808            .json(&Option::<Connection>::None)
809            .send();
810        assert!(resp.await.is_ok());
811    }
812
813    /// Spawn the transaction signing flow in the background and return the join handle.
814    async fn wait_for_transaction_signing<N: Network>(
815        server: &BrowserWalletServer<N>,
816        tx_request: BrowserTransactionRequest<N>,
817    ) -> JoinHandle<Result<TxHash, BrowserWalletError>> {
818        // Spawn the signing flow in the background
819        let browser_server = server.clone();
820        let join_handle =
821            tokio::spawn(async move { browser_server.request_transaction(tx_request).await });
822        tokio::task::yield_now().await;
823        tokio::time::sleep(Duration::from_millis(100)).await;
824
825        join_handle
826    }
827
828    /// Spawn the message signing flow in the background and return the join handle.
829    async fn wait_for_message_signing<N: Network>(
830        server: &BrowserWalletServer<N>,
831        sign_request: BrowserSignRequest,
832    ) -> JoinHandle<Result<Bytes, BrowserWalletError>> {
833        // Spawn the signing flow in the background
834        let browser_server = server.clone();
835        let join_handle =
836            tokio::spawn(async move { browser_server.request_signing(sign_request).await });
837        tokio::task::yield_now().await;
838        tokio::time::sleep(Duration::from_millis(100)).await;
839
840        join_handle
841    }
842
843    /// Create a simple browser transaction request.
844    fn create_browser_transaction_request<N: Network>() -> (Uuid, BrowserTransactionRequest<N>) {
845        let id = Uuid::new_v4();
846        let request = N::TransactionRequest::default()
847            .with_from(ALICE)
848            .with_to(BOB)
849            .with_value(U256::from(1000));
850        let tx = BrowserTransactionRequest { id, request };
851        (id, tx)
852    }
853
854    /// Create a different browser transaction request (from the first one).
855    fn create_different_browser_transaction_request<N: Network>()
856    -> (Uuid, BrowserTransactionRequest<N>) {
857        let id = Uuid::new_v4();
858        let request = N::TransactionRequest::default()
859            .with_from(BOB)
860            .with_to(ALICE)
861            .with_value(U256::from(2000));
862        let tx = BrowserTransactionRequest { id, request };
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<N: Network>(
890        client: &reqwest::Client,
891        server: &BrowserWalletServer<N>,
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<N>>>().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<N: Network>(
910        client: &reqwest::Client,
911        server: &BrowserWalletServer<N>,
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<N>>>().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.kind(), 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<N: Network>(
934        client: &reqwest::Client,
935        server: &BrowserWalletServer<N>,
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<N: Network>(
954        client: &reqwest::Client,
955        server: &BrowserWalletServer<N>,
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}