Skip to main content

foundry_wallets/wallet_browser/
mod.rs

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