spark_rust/wallet/internal_handlers/implementations/
authenticate.rs

1use crate::error::{network::NetworkError, validation::ValidationError, SparkSdkError};
2use crate::rpc::connections::connection::SparkConnection;
3use crate::rpc::traits::SparkRpcConnection;
4use crate::signer::traits::SparkSigner;
5use crate::wallet::config::spark::Session;
6use crate::wallet::internal_handlers::traits::authenticate::AuthenticateInternalHandlers;
7use crate::SparkSdk;
8use prost::Message;
9use spark_protos::authn::GetChallengeRequest;
10use spark_protos::authn::VerifyChallengeRequest;
11use tonic::async_trait;
12use tonic::Request;
13
14#[async_trait]
15impl<S: SparkSigner + Send + Sync + Clone + 'static> AuthenticateInternalHandlers<S>
16    for SparkSdk<S>
17{
18    #[cfg_attr(feature = "telemetry", tracing::instrument(skip_all))]
19    async fn authenticate(&self) -> Result<Vec<Session>, SparkSdkError> {
20        let spark_operators = self.config.spark_config.operator_pool.operators.clone();
21
22        let mut operator_sessions = Vec::new();
23
24        for operator in spark_operators {
25            let client = SparkConnection::establish_connection(operator.address.clone()).await?;
26            let mut auth_client = client.get_new_spark_authn_service_connection()?;
27
28            // prepare the request using the identity public key
29            let pk = self.get_spark_address()?;
30            let challenge_req = GetChallengeRequest {
31                public_key: pk.serialize().to_vec(),
32            };
33
34            // get the challenge from Spark Authn Service
35            let spark_authn_response = auth_client
36                .get_challenge(Request::new(challenge_req))
37                .await
38                .map_err(|status| SparkSdkError::from(NetworkError::Status(status)))?
39                .into_inner();
40
41            let protected_challenge =
42                spark_authn_response
43                    .protected_challenge
44                    .ok_or(SparkSdkError::from(ValidationError::InvalidInput {
45                        field: "No ProtectedChallenge returned by server".to_string(),
46                    }))?;
47
48            // sign the challenge
49            let challenge = protected_challenge
50                .challenge
51                .clone()
52                .ok_or(SparkSdkError::from(ValidationError::InvalidInput {
53                    field: "Missing Challenge within ProtectedChallenge".to_string(),
54                }))?;
55
56            // Serialize the challenge to match what the server uses
57            let challenge_bytes = challenge.encode_to_vec(); // This is the same as proto.Marshal in Go.
58            let network = self.config.spark_config.network.to_bitcoin_network();
59            let signature = self.signer.sign_message_ecdsa_with_identity_key(
60                &challenge_bytes,
61                true,
62                network,
63            )?;
64
65            let verify_req = VerifyChallengeRequest {
66                protected_challenge: Some(protected_challenge),
67                signature: signature.serialize_der().to_vec(),
68                public_key: pk.serialize().to_vec(),
69            };
70
71            let verify_resp = auth_client
72                .verify_challenge(Request::new(verify_req))
73                .await
74                .map_err(|status| SparkSdkError::from(NetworkError::Status(status)))?
75                .into_inner();
76
77            let session = Session {
78                session_token: verify_resp.session_token,
79                _expiration_timestamp: verify_resp.expiration_timestamp,
80            };
81
82            operator_sessions.push(session);
83        }
84
85        Ok(operator_sessions)
86    }
87}