lit_rust_sdk/
client.rs

1use crate::accs::{
2    canonicalize_unified_access_control_conditions, hash_unified_access_control_conditions,
3};
4use crate::auth::{
5    auth_config_from_delegation_auth_sig, create_siwe_message_with_resources,
6    generate_session_key_pair, pkp_eth_address_from_pubkey, validate_delegation_auth_sig,
7    AuthConfig, AuthContext, AuthData, AuthSig, CustomAuthParams, SessionKeyPair,
8};
9use crate::crypto::{
10    bls_encrypt, bls_verify_and_decrypt_with_signature_shares, combine_bls_signature_shares,
11};
12use crate::e2ee::{
13    wallet_decrypt, wallet_decrypt_with_any_key, wallet_encrypt, EncryptedPayload,
14    GenericEncryptedPayload,
15};
16use crate::error::LitSdkError;
17use crate::network::{Endpoint, NetworkConfig};
18use crate::session::{issue_session_sigs, issue_session_sigs_with_max_price};
19use crate::sev_snp::SevSnp;
20use crate::types::{
21    DecryptParams, DecryptResponse, EncryptParams, EncryptResponse, ExecuteJsResponse,
22    HandshakeRequestData, OrchestrateHandshakeResponse, RawHandshakeResponse,
23    ResolvedHandshakeResponse,
24};
25use base64ct::Encoding;
26use ethers::providers::{Http, Provider};
27use ethers::types::{Address, U256};
28use rand::RngCore;
29use reqwest::header::{HeaderMap, HeaderValue};
30use serde::{Deserialize, Serialize};
31use sha2::{Digest, Sha256, Sha384};
32use sha3::{Keccak256, Keccak384};
33use std::collections::{BTreeMap, HashMap, HashSet};
34use std::sync::Arc;
35use tokio::time::{timeout, Duration};
36
37ethers::contract::abigen!(
38    StakingContract,
39    r#"[{
40        "inputs":[{"internalType":"uint256","name":"realmId","type":"uint256"}],
41        "name":"getActiveUnkickedValidatorStructsAndCounts",
42        "outputs":[
43            {"components":[
44                {"internalType":"uint256","name":"epochLength","type":"uint256"},
45                {"internalType":"uint256","name":"number","type":"uint256"},
46                {"internalType":"uint256","name":"rewardEpochNumber","type":"uint256"},
47                {"internalType":"uint256","name":"nextRewardEpochNumber","type":"uint256"},
48                {"internalType":"uint256","name":"endTime","type":"uint256"},
49                {"internalType":"uint256","name":"retries","type":"uint256"},
50                {"internalType":"uint256","name":"timeout","type":"uint256"},
51                {"internalType":"uint256","name":"startTime","type":"uint256"},
52                {"internalType":"uint256","name":"lastAdvanceVoteTime","type":"uint256"}
53            ],"internalType":"struct LibStakingStorage.Epoch","name":"","type":"tuple"},
54            {"internalType":"uint256","name":"minNodeCount","type":"uint256"},
55            {"components":[
56                {"internalType":"uint32","name":"ip","type":"uint32"},
57                {"internalType":"uint128","name":"ipv6","type":"uint128"},
58                {"internalType":"uint32","name":"port","type":"uint32"},
59                {"internalType":"address","name":"nodeAddress","type":"address"},
60                {"internalType":"uint256","name":"reward","type":"uint256"},
61                {"internalType":"uint256","name":"senderPubKey","type":"uint256"},
62                {"internalType":"uint256","name":"receiverPubKey","type":"uint256"},
63                {"internalType":"uint256","name":"lastActiveEpoch","type":"uint256"},
64                {"internalType":"uint256","name":"commissionRate","type":"uint256"},
65                {"internalType":"uint256","name":"lastRewardEpoch","type":"uint256"},
66                {"internalType":"uint256","name":"lastRealmId","type":"uint256"},
67                {"internalType":"uint256","name":"delegatedStakeAmount","type":"uint256"},
68                {"internalType":"uint256","name":"delegatedStakeWeight","type":"uint256"},
69                {"internalType":"uint256","name":"lastRewardEpochClaimedFixedCostRewards","type":"uint256"},
70                {"internalType":"uint256","name":"lastRewardEpochClaimedCommission","type":"uint256"},
71                {"internalType":"address","name":"operatorAddress","type":"address"},
72                {"internalType":"uint256","name":"uniqueDelegatingStakerCount","type":"uint256"},
73                {"internalType":"bool","name":"registerAttestedWalletDisabled","type":"bool"}
74            ],"internalType":"struct LibStakingStorage.Validator[]","name":"","type":"tuple[]"}
75        ],
76        "stateMutability":"view",
77        "type":"function"
78    }]"#,
79);
80
81ethers::contract::abigen!(
82    PriceFeedContract,
83    r#"[{
84        "inputs":[
85            {"internalType":"uint256","name":"realmId","type":"uint256"},
86            {"internalType":"uint256[]","name":"productIds","type":"uint256[]"}
87        ],
88        "name":"getNodesForRequest",
89        "outputs":[
90            {"internalType":"uint256","name":"","type":"uint256"},
91            {"internalType":"uint256","name":"","type":"uint256"},
92            {"components":[
93                {"components":[
94                    {"internalType":"uint32","name":"ip","type":"uint32"},
95                    {"internalType":"uint128","name":"ipv6","type":"uint128"},
96                    {"internalType":"uint32","name":"port","type":"uint32"},
97                    {"internalType":"address","name":"nodeAddress","type":"address"},
98                    {"internalType":"uint256","name":"reward","type":"uint256"},
99                    {"internalType":"uint256","name":"senderPubKey","type":"uint256"},
100                    {"internalType":"uint256","name":"receiverPubKey","type":"uint256"},
101                    {"internalType":"uint256","name":"lastActiveEpoch","type":"uint256"},
102                    {"internalType":"uint256","name":"commissionRate","type":"uint256"},
103                    {"internalType":"uint256","name":"lastRewardEpoch","type":"uint256"},
104                    {"internalType":"uint256","name":"lastRealmId","type":"uint256"},
105                    {"internalType":"uint256","name":"delegatedStakeAmount","type":"uint256"},
106                    {"internalType":"uint256","name":"delegatedStakeWeight","type":"uint256"},
107                    {"internalType":"uint256","name":"lastRewardEpochClaimedFixedCostRewards","type":"uint256"},
108                    {"internalType":"uint256","name":"lastRewardEpochClaimedCommission","type":"uint256"},
109                    {"internalType":"address","name":"operatorAddress","type":"address"},
110                    {"internalType":"uint256","name":"uniqueDelegatingStakerCount","type":"uint256"},
111                    {"internalType":"bool","name":"registerAttestedWalletDisabled","type":"bool"}
112                ],"internalType":"struct LibStakingStorage.Validator","name":"validator","type":"tuple"},
113                {"internalType":"uint256[]","name":"prices","type":"uint256[]"}
114            ],"internalType":"struct LibPriceFeedStorage.NodeInfoAndPrices[]","name":"","type":"tuple[]"}
115        ],
116        "stateMutability":"view",
117        "type":"function"
118    }]"#,
119);
120
121const PRODUCT_ID_DECRYPTION: usize = 0;
122const PRODUCT_ID_SIGN: usize = 1;
123const PRODUCT_ID_LIT_ACTION: usize = 2;
124const PRODUCT_ID_SIGN_SESSION_KEY: usize = 3;
125
126fn int_to_ip(ip: u32) -> String {
127    let b = ip.to_be_bytes();
128    format!("{}.{}.{}.{}", b[0], b[1], b[2], b[3])
129}
130
131fn price_feed_address_for(network: &str) -> Option<Address> {
132    match network {
133        "naga-dev" => Some("0xa997f8DE767d59ecb47A76B421E0C5a1764dD945".parse().ok()?),
134        "naga-test" => Some("0x556955025dD0981Bac684fbDEcE14cDa897d0837".parse().ok()?),
135        "naga-staging" => Some("0x651d3282E1F083036Bb136dBbe7df17aCC39A330".parse().ok()?),
136        "naga-proto" => Some("0xFF4ceEC38572fEd4a48f6D3DF2bed7ccadD115a6".parse().ok()?),
137        "naga" => Some("0x88F5535Fa6dA5C225a3C06489fE4e3405b87608C".parse().ok()?),
138        _ => None,
139    }
140}
141
142fn staking_address_for(network: &str) -> Option<Address> {
143    match network {
144        "naga-dev" => Some("0x544ac098670a266d3598B543aefBEbAb0A2C86C6".parse().ok()?),
145        "naga-test" => Some("0x9f3cE810695180C5f693a7cD2a0203A381fd57E1".parse().ok()?),
146        "naga-staging" => Some("0x9b8Ed3FD964Bc38dDc32CF637439e230CD50e3Dd".parse().ok()?),
147        "naga-proto" => Some("0x28759afC5989B961D0A8EB236C9074c4141Baea1".parse().ok()?),
148        "naga" => Some("0x8a861B3640c1ff058CCB109ba11CA3224d228159".parse().ok()?),
149        _ => None,
150    }
151}
152
153#[derive(Clone)]
154pub struct LitClient {
155    http: reqwest::Client,
156    config: NetworkConfig,
157    handshake: OrchestrateHandshakeResponse,
158    version: String,
159}
160
161#[derive(Clone, Copy, Debug, Default)]
162pub struct ExecuteJsOptions {
163    pub use_single_node: bool,
164    pub user_max_price_wei: Option<U256>,
165    pub response_strategy: ExecuteJsResponseStrategy,
166}
167
168#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
169pub enum ExecuteJsResponseStrategy {
170    #[default]
171    LeastCommon,
172    MostCommon,
173}
174
175#[derive(Clone, Debug)]
176struct JitKeyPair {
177    public_key: [u8; 32],
178    secret_key: [u8; 32],
179}
180
181#[derive(Clone, Debug)]
182struct NagaJitContext {
183    key_set: HashMap<String, JitKeyPair>,
184}
185
186pub async fn create_lit_client(config: NetworkConfig) -> Result<LitClient, LitSdkError> {
187    let mut config = config;
188    if config.bootstrap_urls.is_empty() {
189        config.bootstrap_urls = discover_bootstrap_urls(&config).await?;
190    }
191
192    let http = reqwest::Client::builder()
193        .user_agent("lit-sdk-rust/0.1.0")
194        .build()?;
195
196    // Nodes use this for request handling/telemetry; mirror the JS SDK format (`<sdkVersion>-<network>`).
197    // NOTE: The Rust crate version is not the same as the JS SDK version.
198    let version = format!("8.0.0-{}", config.network);
199    let handshake = orchestrate_handshake(&http, &config, &version).await?;
200
201    Ok(LitClient {
202        http,
203        config,
204        handshake,
205        version,
206    })
207}
208
209async fn discover_bootstrap_urls(config: &NetworkConfig) -> Result<Vec<String>, LitSdkError> {
210    let rpc_url = config.rpc_url.as_deref().ok_or_else(|| {
211        LitSdkError::Config(
212            "rpc_url is required to auto-discover bootstrap_urls (or provide bootstrap_urls)"
213                .into(),
214        )
215    })?;
216    let staking_addr = staking_address_for(config.network).ok_or_else(|| {
217        LitSdkError::Config(format!(
218            "unknown Staking contract address for network {}",
219            config.network
220        ))
221    })?;
222
223    let provider = Arc::new(
224        Provider::<Http>::try_from(rpc_url).map_err(|e| LitSdkError::Config(e.to_string()))?,
225    );
226    let contract = StakingContract::new(staking_addr, provider);
227
228    let realm_id: U256 = 1u64.into();
229    let (_epoch, min_node_count, validators) = contract
230        .get_active_unkicked_validator_structs_and_counts(realm_id)
231        .call()
232        .await
233        .map_err(|e| LitSdkError::Network(e.to_string()))?;
234
235    let min = min_node_count.as_usize().max(1);
236    let mut urls: Vec<String> = validators
237        .iter()
238        .map(|v| format!("{}{}:{}", config.http_protocol, int_to_ip(v.ip), v.port))
239        .collect();
240    urls.sort();
241    urls.dedup();
242
243    if urls.len() < min {
244        return Err(LitSdkError::Network(format!(
245            "validator set below minNodeCount: min={min} got={}",
246            urls.len()
247        )));
248    }
249
250    Ok(urls)
251}
252
253impl LitClient {
254    pub fn network_name(&self) -> &'static str {
255        self.config.network
256    }
257
258    pub fn handshake_result(&self) -> &OrchestrateHandshakeResponse {
259        &self.handshake
260    }
261
262    fn is_node_payload_decryption_error(err: &LitSdkError) -> bool {
263        match err {
264            LitSdkError::Network(msg) => {
265                msg.contains("can't decrypt")
266                    || msg.contains("encrypted payload decryption failed")
267                    || msg.contains("E2EE decryption failed")
268            }
269            _ => false,
270        }
271    }
272
273    pub async fn encrypt(&self, params: EncryptParams) -> Result<EncryptResponse, LitSdkError> {
274        let subnet_pub_key = &self.handshake.core_node_config.subnet_pub_key;
275
276        let data_hash_hex = sha256_hex(&params.data_to_encrypt);
277
278        let conditions_hash_hex = if let Some(hex) = params.hashed_access_control_conditions_hex {
279            hex
280        } else if let Some(unified) = params.unified_access_control_conditions {
281            let hash_bytes = hash_unified_access_control_conditions(&unified)?;
282            hex::encode(hash_bytes)
283        } else {
284            return Err(LitSdkError::Accs(
285                "provide unified_access_control_conditions or hashed_access_control_conditions_hex"
286                    .into(),
287            ));
288        };
289
290        let identity_param = format!(
291            "lit-accesscontrolcondition://{}/{}",
292            conditions_hash_hex, data_hash_hex
293        );
294        let identity_bytes = identity_param.as_bytes();
295
296        let ciphertext_b64 = bls_encrypt(subnet_pub_key, &params.data_to_encrypt, identity_bytes)?;
297
298        Ok(EncryptResponse {
299            ciphertext_base64: ciphertext_b64,
300            data_to_encrypt_hash_hex: data_hash_hex,
301            metadata: params.metadata,
302        })
303    }
304
305    pub async fn decrypt(
306        &self,
307        params: DecryptParams,
308        auth_context: &AuthContext,
309        chain: &str,
310    ) -> Result<DecryptResponse, LitSdkError> {
311        let res = self
312            .decrypt_inner(params.clone(), auth_context, chain)
313            .await;
314        match res {
315            Ok(v) => Ok(v),
316            Err(e) if Self::is_node_payload_decryption_error(&e) => {
317                let refreshed = create_lit_client(self.config.clone()).await?;
318                refreshed.decrypt_inner(params, auth_context, chain).await
319            }
320            Err(e) => Err(e),
321        }
322    }
323
324    async fn decrypt_inner(
325        &self,
326        params: DecryptParams,
327        auth_context: &AuthContext,
328        chain: &str,
329    ) -> Result<DecryptResponse, LitSdkError> {
330        let jit = self.create_jit_context()?;
331        let threshold = self.handshake.threshold.max(1);
332        let node_urls = match self.select_priced_nodes(PRODUCT_ID_DECRYPTION).await {
333            Ok(urls) => urls,
334            Err(_) => self
335                .handshake
336                .connected_nodes
337                .iter()
338                .take(threshold)
339                .cloned()
340                .collect(),
341        };
342        if node_urls.len() < threshold {
343            return Err(LitSdkError::Network(format!(
344                "insufficient node urls for decrypt: got {}, need {}",
345                node_urls.len(),
346                threshold
347            )));
348        }
349
350        let session_sigs = issue_session_sigs(
351            &auth_context.session_key_pair,
352            &auth_context.auth_config,
353            &auth_context.delegation_auth_sig,
354            &node_urls,
355        )?;
356
357        let unified_canonical = params
358            .unified_access_control_conditions
359            .as_ref()
360            .map(canonicalize_unified_access_control_conditions)
361            .transpose()?;
362
363        let conditions_hash_hex = if let Some(hex) = params.hashed_access_control_conditions_hex {
364            hex
365        } else if let Some(unified) = unified_canonical.clone() {
366            let hash_bytes = hash_unified_access_control_conditions(&unified)?;
367            hex::encode(hash_bytes)
368        } else {
369            return Err(LitSdkError::Accs(
370                "provide unified_access_control_conditions or hashed_access_control_conditions_hex"
371                    .into(),
372            ));
373        };
374
375        let identity_param = format!(
376            "lit-accesscontrolcondition://{}/{}",
377            conditions_hash_hex, params.data_to_encrypt_hash_hex
378        );
379
380        let request_id = random_hex(16);
381        let epoch = self.handshake.epoch;
382
383        let mut requests = vec![];
384        let http = self.http.clone();
385        let version = self.version.clone();
386        for (url, sig) in &session_sigs {
387            let request_data = serde_json::json!({
388                "ciphertext": params.ciphertext_base64,
389                "dataToEncryptHash": params.data_to_encrypt_hash_hex,
390                "authSig": sig,
391                "chain": chain,
392                "unifiedAccessControlConditions": unified_canonical.clone().unwrap_or_else(|| serde_json::json!([])),
393            });
394
395            let encrypted = wallet_encrypt(
396                &jit.key_set[url].secret_key,
397                &jit.key_set[url].public_key,
398                request_data.to_string().as_bytes(),
399            )?;
400
401            let full_url = compose_lit_url(url, &self.config.endpoints.encryption_sign);
402            let body = serde_json::to_value(&encrypted).unwrap();
403            let secret_key = jit.key_set[url].secret_key;
404            let http_cl = http.clone();
405            let version_cl = version.clone();
406            let request_id_cl = request_id.clone();
407            requests.push(async move {
408                send_encrypted_node_request(
409                    &http_cl,
410                    &full_url,
411                    body,
412                    &secret_key,
413                    &request_id_cl,
414                    &version_cl,
415                    epoch,
416                )
417                .await
418            });
419        }
420
421        let results = futures::future::join_all(requests).await;
422        let batch = merge_encrypted_batch(results, threshold)?;
423
424        let decrypted_nodes: Vec<DecryptNodeResponse> =
425            decrypt_batch_response(&batch, &jit, |v| {
426                let data_val = v
427                    .get("data")
428                    .cloned()
429                    .ok_or_else(|| LitSdkError::Network("missing data field".into()))?;
430                serde_json::from_value::<DecryptNodeResponse>(data_val)
431                    .map_err(|e| LitSdkError::Network(e.to_string()))
432            })?;
433
434        let shares_json: Vec<String> = decrypted_nodes
435            .iter()
436            .map(|n| {
437                serde_json::to_string(&serde_json::json!({
438                    "ProofOfPossession": {
439                        "identifier": n.signature_share.proof_of_possession.identifier,
440                        "value": n.signature_share.proof_of_possession.value,
441                    }
442                }))
443                .unwrap()
444            })
445            .collect();
446
447        let plaintext = bls_verify_and_decrypt_with_signature_shares(
448            &self.handshake.core_node_config.subnet_pub_key,
449            identity_param.as_bytes(),
450            &params.ciphertext_base64,
451            &shares_json,
452        )?;
453
454        Ok(DecryptResponse {
455            decrypted_data: plaintext,
456            metadata: None,
457        })
458    }
459
460    pub async fn execute_js(
461        &self,
462        code: Option<String>,
463        ipfs_id: Option<String>,
464        js_params: Option<serde_json::Value>,
465        auth_context: &AuthContext,
466    ) -> Result<ExecuteJsResponse, LitSdkError> {
467        self.execute_js_with_options(
468            code,
469            ipfs_id,
470            js_params,
471            auth_context,
472            ExecuteJsOptions::default(),
473        )
474        .await
475    }
476
477    pub async fn execute_js_with_options(
478        &self,
479        code: Option<String>,
480        ipfs_id: Option<String>,
481        js_params: Option<serde_json::Value>,
482        auth_context: &AuthContext,
483        options: ExecuteJsOptions,
484    ) -> Result<ExecuteJsResponse, LitSdkError> {
485        let res = self
486            .execute_js_with_options_inner(
487                code.clone(),
488                ipfs_id.clone(),
489                js_params.clone(),
490                auth_context,
491                options,
492            )
493            .await;
494        match res {
495            Ok(v) => Ok(v),
496            Err(e) if Self::is_node_payload_decryption_error(&e) => {
497                let refreshed = create_lit_client(self.config.clone()).await?;
498                refreshed
499                    .execute_js_with_options_inner(code, ipfs_id, js_params, auth_context, options)
500                    .await
501            }
502            Err(e) => Err(e),
503        }
504    }
505
506    async fn execute_js_with_options_inner(
507        &self,
508        code: Option<String>,
509        ipfs_id: Option<String>,
510        js_params: Option<serde_json::Value>,
511        auth_context: &AuthContext,
512        options: ExecuteJsOptions,
513    ) -> Result<ExecuteJsResponse, LitSdkError> {
514        let jit = self.create_jit_context()?;
515        let execute_threshold = if options.use_single_node {
516            1
517        } else {
518            self.handshake.threshold.max(1)
519        };
520
521        let mut node_urls = match self.select_priced_nodes(PRODUCT_ID_LIT_ACTION).await {
522            Ok(urls) => urls,
523            Err(_) => self.handshake.connected_nodes.clone(),
524        };
525        node_urls.truncate(execute_threshold);
526        if node_urls.len() < execute_threshold {
527            return Err(LitSdkError::Network(format!(
528                "insufficient node urls for executeJs: got {}, need {}",
529                node_urls.len(),
530                execute_threshold
531            )));
532        }
533
534        let per_node_max_price = options
535            .user_max_price_wei
536            .map(|p| p / ethers::types::U256::from(execute_threshold as u64));
537        let session_sigs = issue_session_sigs_with_max_price(
538            &auth_context.session_key_pair,
539            &auth_context.auth_config,
540            &auth_context.delegation_auth_sig,
541            &node_urls,
542            per_node_max_price,
543        )?;
544
545        let node_set = node_set_from_urls(&node_urls);
546        let request_id = random_hex(16);
547        let epoch = self.handshake.epoch;
548
549        let mut requests = vec![];
550        let http = self.http.clone();
551        let version = self.version.clone();
552        for (url, sig) in &session_sigs {
553            let mut request_data = serde_json::json!({
554                "authSig": sig,
555                "nodeSet": node_set.clone(),
556            });
557            if let Some(code_str) = &code {
558                request_data["code"] =
559                    serde_json::json!(base64ct::Base64::encode_string(code_str.as_bytes()));
560            }
561            if let Some(id) = &ipfs_id {
562                request_data["ipfsId"] = serde_json::json!(id);
563            }
564            if let Some(params) = &js_params {
565                request_data["jsParams"] = serde_json::json!({ "jsParams": params.clone() });
566            }
567
568            let encrypted = wallet_encrypt(
569                &jit.key_set[url].secret_key,
570                &jit.key_set[url].public_key,
571                request_data.to_string().as_bytes(),
572            )?;
573
574            let full_url = compose_lit_url(url, &self.config.endpoints.execute_js);
575            let body = serde_json::to_value(&encrypted).unwrap();
576            let secret_key = jit.key_set[url].secret_key;
577            let http_cl = http.clone();
578            let version_cl = version.clone();
579            let request_id_cl = request_id.clone();
580            requests.push(async move {
581                send_encrypted_node_request(
582                    &http_cl,
583                    &full_url,
584                    body,
585                    &secret_key,
586                    &request_id_cl,
587                    &version_cl,
588                    epoch,
589                )
590                .await
591            });
592        }
593
594        let results = futures::future::join_all(requests).await;
595        let batch = merge_encrypted_batch(results, execute_threshold)?;
596
597        let node_values: Vec<ExecuteJsNodeValue> = decrypt_batch_response(&batch, &jit, |v| {
598            let data = v
599                .get("data")
600                .cloned()
601                .ok_or_else(|| LitSdkError::Network("missing data field".into()))?;
602            serde_json::from_value::<ExecuteJsNodeValue>(data)
603                .map_err(|e| LitSdkError::Network(e.to_string()))
604        })?;
605
606        let successful: Vec<&ExecuteJsNodeValue> =
607            node_values.iter().filter(|v| v.success).collect();
608        if successful.len() < execute_threshold {
609            return Err(LitSdkError::Network(format!(
610                "insufficient successful executeJs responses: got {}, need {}",
611                successful.len(),
612                execute_threshold
613            )));
614        }
615
616        let mut freq: Vec<(&str, usize)> = vec![];
617        for v in &successful {
618            let resp = v.response.as_str();
619            if let Some((_, count)) = freq.iter_mut().find(|(r, _)| *r == resp) {
620                *count += 1;
621            } else {
622                freq.push((resp, 1));
623            }
624        }
625
626        let mut least_common = freq[0].0;
627        let mut least_common_count = freq[0].1;
628        let mut most_common = freq[0].0;
629        let mut most_common_count = freq[0].1;
630        for (resp, count) in &freq {
631            if *count < least_common_count {
632                least_common = *resp;
633                least_common_count = *count;
634            }
635            if *count > most_common_count {
636                most_common = *resp;
637                most_common_count = *count;
638            }
639        }
640
641        let selected_response = match options.response_strategy {
642            ExecuteJsResponseStrategy::LeastCommon => least_common,
643            ExecuteJsResponseStrategy::MostCommon => most_common,
644        };
645
646        let logs = successful
647            .iter()
648            .find(|v| v.response == selected_response)
649            .map(|v| v.logs.clone())
650            .unwrap_or_default();
651
652        let mut signatures: HashMap<String, serde_json::Value> = HashMap::new();
653        let mut signature_keys: HashSet<String> = HashSet::new();
654        for v in &successful {
655            for k in v.signed_data.keys() {
656                signature_keys.insert(k.clone());
657            }
658        }
659
660        for sig_name in signature_keys {
661            let signature_threshold = self.handshake.threshold.max(1);
662            let mut shares: Vec<String> = vec![];
663            for v in &successful {
664                let Some(entry) = v.signed_data.get(&sig_name) else {
665                    continue;
666                };
667                let Some(share_val) = entry.get("signatureShare") else {
668                    continue;
669                };
670                if let Some(s) = share_val.as_str() {
671                    shares.push(s.to_string());
672                } else {
673                    shares.push(
674                        serde_json::to_string(share_val)
675                            .map_err(|e| LitSdkError::Network(e.to_string()))?,
676                    );
677                }
678            }
679
680            if shares.is_empty() {
681                continue;
682            }
683            if shares.len() < signature_threshold {
684                return Err(LitSdkError::Network(format!(
685                    "insufficient signature shares for {sig_name}: got {}, need {}",
686                    shares.len(),
687                    signature_threshold
688                )));
689            }
690
691            let combined = crate::crypto::combine_and_verify(shares)?;
692            let combined_value: serde_json::Value =
693                serde_json::from_str(&combined).map_err(|e| LitSdkError::Crypto(e.to_string()))?;
694            signatures.insert(sig_name, combined_value);
695        }
696
697        let response_val: serde_json::Value = serde_json::from_str(selected_response)
698            .unwrap_or_else(|_| serde_json::Value::String(selected_response.to_string()));
699
700        Ok(ExecuteJsResponse {
701            success: true,
702            signatures,
703            response: response_val,
704            logs,
705        })
706    }
707
708    pub async fn create_pkp_auth_context(
709        &self,
710        pkp_public_key: &str,
711        auth_data: AuthData,
712        auth_config: AuthConfig,
713        session_key_pair: Option<SessionKeyPair>,
714        delegation_auth_sig: Option<AuthSig>,
715        user_max_price_wei: Option<U256>,
716    ) -> Result<AuthContext, LitSdkError> {
717        let has_session_key_pair = session_key_pair.is_some();
718        let has_delegation_auth_sig = delegation_auth_sig.is_some();
719        if has_session_key_pair != has_delegation_auth_sig {
720            return Err(LitSdkError::Config(
721                "Both sessionKeyPair and delegationAuthSig must be provided together, or neither should be provided".into(),
722            ));
723        }
724
725        let session_key_pair = session_key_pair.unwrap_or_else(generate_session_key_pair);
726        let delegation_auth_sig = match delegation_auth_sig {
727            Some(sig) => sig,
728            None => {
729                self.sign_session_key_for_pkp(
730                    pkp_public_key,
731                    &auth_data,
732                    &auth_config,
733                    &session_key_pair,
734                    user_max_price_wei,
735                )
736                .await?
737            }
738        };
739
740        validate_delegation_auth_sig(&delegation_auth_sig, &session_key_pair.public_key)?;
741
742        Ok(AuthContext {
743            session_key_pair,
744            auth_config,
745            delegation_auth_sig,
746        })
747    }
748
749    pub async fn create_custom_auth_context(
750        &self,
751        pkp_public_key: &str,
752        auth_config: AuthConfig,
753        custom_auth_params: CustomAuthParams,
754        session_key_pair: Option<SessionKeyPair>,
755        delegation_auth_sig: Option<AuthSig>,
756        user_max_price_wei: Option<U256>,
757    ) -> Result<AuthContext, LitSdkError> {
758        let has_session_key_pair = session_key_pair.is_some();
759        let has_delegation_auth_sig = delegation_auth_sig.is_some();
760        if has_session_key_pair != has_delegation_auth_sig {
761            return Err(LitSdkError::Config(
762                "Both sessionKeyPair and delegationAuthSig must be provided together, or neither should be provided".into(),
763            ));
764        }
765
766        let session_key_pair = session_key_pair.unwrap_or_else(generate_session_key_pair);
767        let delegation_auth_sig = match delegation_auth_sig {
768            Some(sig) => sig,
769            None => {
770                self.sign_custom_session_key_for_pkp(
771                    pkp_public_key,
772                    &auth_config,
773                    &custom_auth_params,
774                    &session_key_pair,
775                    user_max_price_wei,
776                )
777                .await?
778            }
779        };
780
781        validate_delegation_auth_sig(&delegation_auth_sig, &session_key_pair.public_key)?;
782
783        Ok(AuthContext {
784            session_key_pair,
785            auth_config,
786            delegation_auth_sig,
787        })
788    }
789
790    pub fn create_pkp_auth_context_from_pre_generated(
791        &self,
792        session_key_pair: SessionKeyPair,
793        delegation_auth_sig: AuthSig,
794    ) -> Result<AuthContext, LitSdkError> {
795        validate_delegation_auth_sig(&delegation_auth_sig, &session_key_pair.public_key)?;
796        let auth_config = auth_config_from_delegation_auth_sig(&delegation_auth_sig)?;
797
798        Ok(AuthContext {
799            session_key_pair,
800            auth_config,
801            delegation_auth_sig,
802        })
803    }
804
805    async fn sign_session_key_for_pkp(
806        &self,
807        pkp_public_key: &str,
808        auth_data: &AuthData,
809        auth_config: &AuthConfig,
810        session_key_pair: &SessionKeyPair,
811        user_max_price_wei: Option<U256>,
812    ) -> Result<AuthSig, LitSdkError> {
813        let res = self
814            .sign_session_key_for_pkp_inner(
815                pkp_public_key,
816                auth_data,
817                auth_config,
818                session_key_pair,
819                user_max_price_wei,
820            )
821            .await;
822        match res {
823            Ok(v) => Ok(v),
824            Err(e) if Self::is_node_payload_decryption_error(&e) => {
825                let refreshed = create_lit_client(self.config.clone()).await?;
826                refreshed
827                    .sign_session_key_for_pkp_inner(
828                        pkp_public_key,
829                        auth_data,
830                        auth_config,
831                        session_key_pair,
832                        user_max_price_wei,
833                    )
834                    .await
835            }
836            Err(e) => Err(e),
837        }
838    }
839
840    async fn sign_session_key_for_pkp_inner(
841        &self,
842        pkp_public_key: &str,
843        auth_data: &AuthData,
844        auth_config: &AuthConfig,
845        session_key_pair: &SessionKeyPair,
846        user_max_price_wei: Option<U256>,
847    ) -> Result<AuthSig, LitSdkError> {
848        let jit = self.create_jit_context()?;
849        let threshold = self.handshake.threshold.max(1);
850
851        let selected_urls = match self.select_priced_nodes(PRODUCT_ID_SIGN_SESSION_KEY).await {
852            Ok(urls) => urls,
853            Err(_) => self
854                .handshake
855                .connected_nodes
856                .iter()
857                .take(threshold)
858                .cloned()
859                .collect(),
860        };
861        if selected_urls.len() < threshold {
862            return Err(LitSdkError::Network(format!(
863                "insufficient node urls for signSessionKey: got {}, need {}",
864                selected_urls.len(),
865                threshold
866            )));
867        }
868
869        let node_set = node_set_from_urls(&selected_urls);
870        let request_id = random_hex(16);
871        let epoch = self.handshake.epoch;
872
873        let pkp_eth_address = pkp_eth_address_from_pubkey(pkp_public_key)?;
874
875        let mut pkp_auth_config = auth_config.clone();
876        let base_stmt = "Lit Protocol PKP session signature";
877        let extra = pkp_auth_config.statement.trim();
878        pkp_auth_config.statement = if extra.is_empty() {
879            base_stmt.to_string()
880        } else {
881            format!("{base_stmt} {extra}")
882        };
883        if pkp_auth_config.domain.trim().is_empty() {
884            pkp_auth_config.domain = "localhost".into();
885        }
886
887        let nonce = self.handshake.core_node_config.latest_blockhash.clone();
888        let session_public_key_hex = session_key_pair
889            .public_key
890            .strip_prefix("lit:session:")
891            .unwrap_or(&session_key_pair.public_key);
892        let session_key_uri = format!("lit:session:{session_public_key_hex}");
893
894        let siwe_message = create_siwe_message_with_resources(
895            &pkp_eth_address,
896            session_public_key_hex,
897            &pkp_auth_config,
898            &nonce,
899        )?;
900
901        #[derive(Debug, Clone, Serialize)]
902        #[serde(rename_all = "camelCase")]
903        struct AuthMethodForRequest {
904            auth_method_type: u32,
905            access_token: String,
906        }
907
908        let auth_method = AuthMethodForRequest {
909            auth_method_type: auth_data.auth_method_type,
910            access_token: auth_data.access_token.clone(),
911        };
912
913        let max_price = user_max_price_wei.unwrap_or_else(|| U256::from(u128::MAX));
914
915        let mut requests = vec![];
916        let http = self.http.clone();
917        let version = self.version.clone();
918        for url in &selected_urls {
919            let request_data = serde_json::json!({
920                "sessionKey": session_key_uri.clone(),
921                "authMethods": [auth_method.clone()],
922                "pkpPublicKey": pkp_public_key,
923                "siweMessage": siwe_message.clone(),
924                "curveType": "BLS",
925                "epoch": epoch,
926                "nodeSet": node_set.clone(),
927                "maxPrice": max_price.to_string(),
928            });
929
930            let encrypted = wallet_encrypt(
931                &jit.key_set[url].secret_key,
932                &jit.key_set[url].public_key,
933                request_data.to_string().as_bytes(),
934            )?;
935
936            let full_url = compose_lit_url(url, &self.config.endpoints.sign_session_key);
937            let body = serde_json::to_value(&encrypted).unwrap();
938            let secret_key = jit.key_set[url].secret_key;
939            let http_cl = http.clone();
940            let version_cl = version.clone();
941            let request_id_cl = request_id.clone();
942            requests.push(async move {
943                send_encrypted_node_request(
944                    &http_cl,
945                    &full_url,
946                    body,
947                    &secret_key,
948                    &request_id_cl,
949                    &version_cl,
950                    epoch,
951                )
952                .await
953            });
954        }
955
956        let results = futures::future::join_all(requests).await;
957        let batch = merge_encrypted_batch(results, threshold)?;
958
959        #[derive(Debug, Deserialize)]
960        #[serde(rename_all = "camelCase")]
961        struct SignSessionKeyNodeData {
962            #[serde(rename = "signatureShare")]
963            signature_share: SignatureShareWrapper,
964            #[serde(rename = "siweMessage")]
965            siwe_message: String,
966        }
967
968        let node_values: Vec<SignSessionKeyNodeData> = decrypt_batch_response(&batch, &jit, |v| {
969            let data = v
970                .get("data")
971                .cloned()
972                .ok_or_else(|| LitSdkError::Network("missing data field".into()))?;
973            serde_json::from_value::<SignSessionKeyNodeData>(data)
974                .map_err(|e| LitSdkError::Network(e.to_string()))
975        })?;
976
977        let shares_json: Vec<String> = node_values
978            .iter()
979            .map(|n| {
980                serde_json::to_string(&serde_json::json!({
981                    "ProofOfPossession": {
982                        "identifier": n.signature_share.proof_of_possession.identifier,
983                        "value": n.signature_share.proof_of_possession.value,
984                    }
985                }))
986                .unwrap()
987            })
988            .collect();
989        if shares_json.len() < threshold {
990            return Err(LitSdkError::Network(format!(
991                "insufficient signature shares for signSessionKey: got {}, need {}",
992                shares_json.len(),
993                threshold
994            )));
995        }
996
997        let combined = combine_bls_signature_shares(&shares_json)?;
998        if combined.len() != 192 {
999            return Err(LitSdkError::Crypto(format!(
1000                "combined BLS signature must be 192 hex chars; got {}",
1001                combined.len()
1002            )));
1003        }
1004
1005        let most_common_siwe =
1006            most_common_value(node_values.iter().map(|v| v.siwe_message.clone()).collect())
1007                .unwrap_or_else(|| siwe_message.clone());
1008
1009        let sig_json = serde_json::to_string(&serde_json::json!({
1010            "ProofOfPossession": combined,
1011        }))
1012        .map_err(|e| LitSdkError::Crypto(e.to_string()))?;
1013
1014        Ok(AuthSig {
1015            sig: sig_json,
1016            derived_via: "lit.bls".into(),
1017            signed_message: most_common_siwe,
1018            address: pkp_eth_address,
1019            algo: Some("LIT_BLS".into()),
1020        })
1021    }
1022
1023    async fn sign_custom_session_key_for_pkp(
1024        &self,
1025        pkp_public_key: &str,
1026        auth_config: &AuthConfig,
1027        custom_auth_params: &CustomAuthParams,
1028        session_key_pair: &SessionKeyPair,
1029        user_max_price_wei: Option<U256>,
1030    ) -> Result<AuthSig, LitSdkError> {
1031        if custom_auth_params.lit_action_ipfs_id.is_none()
1032            && custom_auth_params.lit_action_code.is_none()
1033        {
1034            return Err(LitSdkError::Config(
1035                "custom auth requires lit_action_ipfs_id or lit_action_code".into(),
1036            ));
1037        }
1038
1039        let res = self
1040            .sign_custom_session_key_for_pkp_inner(
1041                pkp_public_key,
1042                auth_config,
1043                custom_auth_params,
1044                session_key_pair,
1045                user_max_price_wei,
1046            )
1047            .await;
1048        match res {
1049            Ok(v) => Ok(v),
1050            Err(e) if Self::is_node_payload_decryption_error(&e) => {
1051                let refreshed = create_lit_client(self.config.clone()).await?;
1052                refreshed
1053                    .sign_custom_session_key_for_pkp_inner(
1054                        pkp_public_key,
1055                        auth_config,
1056                        custom_auth_params,
1057                        session_key_pair,
1058                        user_max_price_wei,
1059                    )
1060                    .await
1061            }
1062            Err(e) => Err(e),
1063        }
1064    }
1065
1066    async fn sign_custom_session_key_for_pkp_inner(
1067        &self,
1068        pkp_public_key: &str,
1069        auth_config: &AuthConfig,
1070        custom_auth_params: &CustomAuthParams,
1071        session_key_pair: &SessionKeyPair,
1072        user_max_price_wei: Option<U256>,
1073    ) -> Result<AuthSig, LitSdkError> {
1074        let jit = self.create_jit_context()?;
1075        let threshold = self.handshake.threshold.max(1);
1076
1077        let selected_urls = match self.select_priced_nodes(PRODUCT_ID_LIT_ACTION).await {
1078            Ok(urls) => urls,
1079            Err(_) => self
1080                .handshake
1081                .connected_nodes
1082                .iter()
1083                .take(threshold)
1084                .cloned()
1085                .collect(),
1086        };
1087        if selected_urls.len() < threshold {
1088            return Err(LitSdkError::Network(format!(
1089                "insufficient node urls for signCustomSessionKey: got {}, need {}",
1090                selected_urls.len(),
1091                threshold
1092            )));
1093        }
1094
1095        let node_set = node_set_from_urls(&selected_urls);
1096        let request_id = random_hex(16);
1097        let epoch = self.handshake.epoch;
1098
1099        let pkp_eth_address = pkp_eth_address_from_pubkey(pkp_public_key)?;
1100
1101        let mut pkp_auth_config = auth_config.clone();
1102        let base_stmt = "Lit Protocol PKP session signature";
1103        let extra = pkp_auth_config.statement.trim();
1104        pkp_auth_config.statement = if extra.is_empty() {
1105            base_stmt.to_string()
1106        } else {
1107            format!("{base_stmt} {extra}")
1108        };
1109        if pkp_auth_config.domain.trim().is_empty() {
1110            pkp_auth_config.domain = "localhost".into();
1111        }
1112
1113        let nonce = self.handshake.core_node_config.latest_blockhash.clone();
1114        let session_public_key_hex = session_key_pair
1115            .public_key
1116            .strip_prefix("lit:session:")
1117            .unwrap_or(&session_key_pair.public_key);
1118        let session_key_uri = format!("lit:session:{session_public_key_hex}");
1119
1120        let siwe_message = create_siwe_message_with_resources(
1121            &pkp_eth_address,
1122            session_public_key_hex,
1123            &pkp_auth_config,
1124            &nonce,
1125        )?;
1126
1127        let max_price = user_max_price_wei.unwrap_or_else(|| U256::from(u128::MAX));
1128
1129        let mut requests = vec![];
1130        let http = self.http.clone();
1131        let version = self.version.clone();
1132        for url in &selected_urls {
1133            let mut request_data = serde_json::json!({
1134                "sessionKey": session_key_uri.clone(),
1135                "authMethods": [],
1136                "pkpPublicKey": pkp_public_key,
1137                "siweMessage": siwe_message.clone(),
1138                "curveType": "BLS",
1139                "epoch": epoch,
1140                "nodeSet": node_set.clone(),
1141                "maxPrice": max_price.to_string(),
1142            });
1143
1144            if let Some(ipfs_id) = &custom_auth_params.lit_action_ipfs_id {
1145                if let Some(obj) = request_data.as_object_mut() {
1146                    obj.insert(
1147                        "litActionIpfsId".into(),
1148                        serde_json::Value::String(ipfs_id.clone()),
1149                    );
1150                }
1151            }
1152            if let Some(code) = &custom_auth_params.lit_action_code {
1153                if let Some(obj) = request_data.as_object_mut() {
1154                    obj.insert(
1155                        "litActionCode".into(),
1156                        serde_json::Value::String(code.clone()),
1157                    );
1158                }
1159            }
1160            if let Some(js_params) = &custom_auth_params.js_params {
1161                if let Some(obj) = request_data.as_object_mut() {
1162                    obj.insert(
1163                        "jsParams".into(),
1164                        serde_json::json!({ "jsParams": js_params.clone() }),
1165                    );
1166                }
1167            }
1168
1169            let encrypted = wallet_encrypt(
1170                &jit.key_set[url].secret_key,
1171                &jit.key_set[url].public_key,
1172                request_data.to_string().as_bytes(),
1173            )?;
1174
1175            let full_url = compose_lit_url(url, &self.config.endpoints.sign_session_key);
1176            let body = serde_json::to_value(&encrypted).unwrap();
1177            let secret_key = jit.key_set[url].secret_key;
1178            let http_cl = http.clone();
1179            let version_cl = version.clone();
1180            let request_id_cl = request_id.clone();
1181            requests.push(async move {
1182                send_encrypted_node_request(
1183                    &http_cl,
1184                    &full_url,
1185                    body,
1186                    &secret_key,
1187                    &request_id_cl,
1188                    &version_cl,
1189                    epoch,
1190                )
1191                .await
1192            });
1193        }
1194
1195        let results = futures::future::join_all(requests).await;
1196        let batch = merge_encrypted_batch(results, threshold)?;
1197
1198        #[derive(Debug, Deserialize)]
1199        #[serde(rename_all = "camelCase")]
1200        struct SignSessionKeyNodeData {
1201            #[serde(rename = "signatureShare")]
1202            signature_share: SignatureShareWrapper,
1203            #[serde(rename = "siweMessage")]
1204            siwe_message: String,
1205        }
1206
1207        let node_values: Vec<SignSessionKeyNodeData> = decrypt_batch_response(&batch, &jit, |v| {
1208            let data = v
1209                .get("data")
1210                .cloned()
1211                .ok_or_else(|| LitSdkError::Network("missing data field".into()))?;
1212            serde_json::from_value::<SignSessionKeyNodeData>(data)
1213                .map_err(|e| LitSdkError::Network(e.to_string()))
1214        })?;
1215
1216        let shares_json: Vec<String> = node_values
1217            .iter()
1218            .map(|n| {
1219                serde_json::to_string(&serde_json::json!({
1220                    "ProofOfPossession": {
1221                        "identifier": n.signature_share.proof_of_possession.identifier,
1222                        "value": n.signature_share.proof_of_possession.value,
1223                    }
1224                }))
1225                .unwrap()
1226            })
1227            .collect();
1228        if shares_json.len() < threshold {
1229            return Err(LitSdkError::Network(format!(
1230                "insufficient signature shares for signCustomSessionKey: got {}, need {}",
1231                shares_json.len(),
1232                threshold
1233            )));
1234        }
1235
1236        let combined = combine_bls_signature_shares(&shares_json)?;
1237        if combined.len() != 192 {
1238            return Err(LitSdkError::Crypto(format!(
1239                "combined BLS signature must be 192 hex chars; got {}",
1240                combined.len()
1241            )));
1242        }
1243
1244        let most_common_siwe =
1245            most_common_value(node_values.iter().map(|v| v.siwe_message.clone()).collect())
1246                .unwrap_or_else(|| siwe_message.clone());
1247
1248        let sig_json = serde_json::to_string(&serde_json::json!({
1249            "ProofOfPossession": combined,
1250        }))
1251        .map_err(|e| LitSdkError::Crypto(e.to_string()))?;
1252
1253        Ok(AuthSig {
1254            sig: sig_json,
1255            derived_via: "lit.bls".into(),
1256            signed_message: most_common_siwe,
1257            address: pkp_eth_address,
1258            algo: Some("LIT_BLS".into()),
1259        })
1260    }
1261
1262    pub async fn pkp_sign_ethereum(
1263        &self,
1264        pkp_pubkey: &str,
1265        to_sign: &[u8],
1266        auth_context: &AuthContext,
1267        user_max_price_wei: Option<ethers::types::U256>,
1268    ) -> Result<serde_json::Value, LitSdkError> {
1269        self.pkp_sign_ethereum_with_options(
1270            pkp_pubkey,
1271            to_sign,
1272            auth_context,
1273            user_max_price_wei,
1274            false,
1275        )
1276        .await
1277    }
1278
1279    pub async fn pkp_sign_ethereum_with_options(
1280        &self,
1281        pkp_pubkey: &str,
1282        to_sign: &[u8],
1283        auth_context: &AuthContext,
1284        user_max_price_wei: Option<ethers::types::U256>,
1285        bypass_auto_hashing: bool,
1286    ) -> Result<serde_json::Value, LitSdkError> {
1287        self.pkp_sign_raw_with_options(
1288            "ethereum",
1289            "EcdsaK256Sha256",
1290            pkp_pubkey,
1291            to_sign,
1292            auth_context,
1293            user_max_price_wei,
1294            bypass_auto_hashing,
1295        )
1296        .await
1297    }
1298
1299    pub async fn pkp_sign_raw(
1300        &self,
1301        chain: &str,
1302        signing_scheme: &str,
1303        pkp_pubkey: &str,
1304        to_sign: &[u8],
1305        auth_context: &AuthContext,
1306        user_max_price_wei: Option<ethers::types::U256>,
1307    ) -> Result<serde_json::Value, LitSdkError> {
1308        self.pkp_sign_raw_with_options(
1309            chain,
1310            signing_scheme,
1311            pkp_pubkey,
1312            to_sign,
1313            auth_context,
1314            user_max_price_wei,
1315            false,
1316        )
1317        .await
1318    }
1319
1320    #[allow(clippy::too_many_arguments)]
1321    pub async fn pkp_sign_raw_with_options(
1322        &self,
1323        chain: &str,
1324        signing_scheme: &str,
1325        pkp_pubkey: &str,
1326        to_sign: &[u8],
1327        auth_context: &AuthContext,
1328        user_max_price_wei: Option<ethers::types::U256>,
1329        bypass_auto_hashing: bool,
1330    ) -> Result<serde_json::Value, LitSdkError> {
1331        let res = self
1332            .pkp_sign_raw_with_options_inner(
1333                chain,
1334                signing_scheme,
1335                pkp_pubkey,
1336                to_sign,
1337                auth_context,
1338                user_max_price_wei,
1339                bypass_auto_hashing,
1340            )
1341            .await;
1342        match res {
1343            Ok(v) => Ok(v),
1344            Err(e) if Self::is_node_payload_decryption_error(&e) => {
1345                let refreshed = create_lit_client(self.config.clone()).await?;
1346                refreshed
1347                    .pkp_sign_raw_with_options_inner(
1348                        chain,
1349                        signing_scheme,
1350                        pkp_pubkey,
1351                        to_sign,
1352                        auth_context,
1353                        user_max_price_wei,
1354                        bypass_auto_hashing,
1355                    )
1356                    .await
1357            }
1358            Err(e) => Err(e),
1359        }
1360    }
1361
1362    #[allow(clippy::too_many_arguments)]
1363    async fn pkp_sign_raw_with_options_inner(
1364        &self,
1365        chain: &str,
1366        signing_scheme: &str,
1367        pkp_pubkey: &str,
1368        to_sign: &[u8],
1369        auth_context: &AuthContext,
1370        user_max_price_wei: Option<ethers::types::U256>,
1371        bypass_auto_hashing: bool,
1372    ) -> Result<serde_json::Value, LitSdkError> {
1373        let to_sign_data =
1374            pkp_sign_message_bytes(chain, signing_scheme, to_sign, bypass_auto_hashing)?;
1375
1376        let jit = self.create_jit_context()?;
1377
1378        let threshold = self.handshake.threshold.max(1);
1379        let selected_urls = match self.select_priced_nodes(PRODUCT_ID_SIGN).await {
1380            Ok(urls) => urls,
1381            Err(_) => self
1382                .handshake
1383                .connected_nodes
1384                .iter()
1385                .take(threshold)
1386                .cloned()
1387                .collect(),
1388        };
1389        if selected_urls.len() < threshold {
1390            return Err(LitSdkError::Network(format!(
1391                "insufficient node urls for pkpSign: got {}, need {}",
1392                selected_urls.len(),
1393                threshold
1394            )));
1395        }
1396
1397        let per_node_max_price =
1398            user_max_price_wei.map(|p| p / ethers::types::U256::from(threshold as u64));
1399
1400        let session_sigs = issue_session_sigs_with_max_price(
1401            &auth_context.session_key_pair,
1402            &auth_context.auth_config,
1403            &auth_context.delegation_auth_sig,
1404            &selected_urls,
1405            per_node_max_price,
1406        )?;
1407
1408        let node_set = node_set_from_urls(&selected_urls);
1409        let request_id = random_hex(16);
1410        let epoch = self.handshake.epoch;
1411
1412        let mut requests = vec![];
1413        let http = self.http.clone();
1414        let version = self.version.clone();
1415        for (url, sig) in &session_sigs {
1416            let to_sign_vec = to_sign_data.clone();
1417            let request_data = serde_json::json!({
1418                "toSign": to_sign_vec,
1419                "signingScheme": signing_scheme,
1420                "pubkey": pkp_pubkey,
1421                "authSig": sig,
1422                "nodeSet": node_set.clone(),
1423                "epoch": epoch,
1424                "authMethods": [],
1425            });
1426
1427            let encrypted = wallet_encrypt(
1428                &jit.key_set[url].secret_key,
1429                &jit.key_set[url].public_key,
1430                request_data.to_string().as_bytes(),
1431            )?;
1432
1433            let full_url = compose_lit_url(url, &self.config.endpoints.pkp_sign);
1434            let body = serde_json::to_value(&encrypted).unwrap();
1435            let secret_key = jit.key_set[url].secret_key;
1436            let http_cl = http.clone();
1437            let version_cl = version.clone();
1438            let request_id_cl = request_id.clone();
1439            requests.push(async move {
1440                send_encrypted_node_request(
1441                    &http_cl,
1442                    &full_url,
1443                    body,
1444                    &secret_key,
1445                    &request_id_cl,
1446                    &version_cl,
1447                    epoch,
1448                )
1449                .await
1450            });
1451        }
1452
1453        let results = futures::future::join_all(requests).await;
1454        let batch = merge_encrypted_batch(results, threshold)?;
1455
1456        let node_values: Vec<serde_json::Value> = decrypt_batch_response(&batch, &jit, |v| {
1457            v.get("data")
1458                .cloned()
1459                .ok_or_else(|| LitSdkError::Network("missing data field".into()))
1460        })?;
1461
1462        let combiner_shares: Vec<String> = node_values
1463            .iter()
1464            .filter(|v| v.get("success").and_then(|s| s.as_bool()) == Some(true))
1465            .filter_map(|v| v.get("signatureShare"))
1466            .filter_map(|s| serde_json::to_string(s).ok())
1467            .collect();
1468        if combiner_shares.len() < threshold {
1469            return Err(LitSdkError::Network(format!(
1470                "insufficient signature shares: got {}, need {}",
1471                combiner_shares.len(),
1472                threshold
1473            )));
1474        }
1475
1476        let combined = crate::crypto::combine_and_verify(combiner_shares)?;
1477
1478        let combined_value: serde_json::Value =
1479            serde_json::from_str(&combined).map_err(|e| LitSdkError::Crypto(e.to_string()))?;
1480
1481        Ok(combined_value)
1482    }
1483
1484    async fn select_priced_nodes(&self, product_id: usize) -> Result<Vec<String>, LitSdkError> {
1485        let rpc_url =
1486            self.config.rpc_url.as_deref().ok_or_else(|| {
1487                LitSdkError::Config("rpc_url is required for priced requests".into())
1488            })?;
1489        let price_feed_addr = price_feed_address_for(self.config.network).ok_or_else(|| {
1490            LitSdkError::Config(format!(
1491                "unknown PriceFeed contract address for network {}",
1492                self.config.network
1493            ))
1494        })?;
1495
1496        let provider = Arc::new(
1497            Provider::<Http>::try_from(rpc_url).map_err(|e| LitSdkError::Config(e.to_string()))?,
1498        );
1499        let contract = PriceFeedContract::new(price_feed_addr, provider);
1500
1501        let realm_id: U256 = 1u64.into();
1502        let product_ids: Vec<U256> = vec![0u64.into(), 1u64.into(), 2u64.into(), 3u64.into()];
1503        let (_epoch_id, min_node_count, nodes_and_prices) = contract
1504            .get_nodes_for_request(realm_id, product_ids)
1505            .call()
1506            .await
1507            .map_err(|e| LitSdkError::Network(e.to_string()))?;
1508
1509        let required = self.handshake.threshold.max(1);
1510        let contract_min = (min_node_count.as_u64() as usize).max(1);
1511        if required < contract_min {
1512            return Err(LitSdkError::Config(format!(
1513                "minimum_threshold ({required}) is below chain minNodeCount ({contract_min})"
1514            )));
1515        }
1516        let connected: HashSet<&str> = self
1517            .handshake
1518            .connected_nodes
1519            .iter()
1520            .map(|s| s.as_str())
1521            .collect();
1522
1523        let mut candidates: Vec<(String, U256)> = nodes_and_prices
1524            .into_iter()
1525            .filter_map(|node| {
1526                let price = *node.prices.get(product_id)?;
1527                let url = format!(
1528                    "{}{}:{}",
1529                    self.config.http_protocol,
1530                    int_to_ip(node.validator.ip),
1531                    node.validator.port
1532                );
1533                connected.contains(url.as_str()).then_some((url, price))
1534            })
1535            .collect();
1536
1537        candidates.sort_by(|a, b| a.1.cmp(&b.1));
1538        let selected: Vec<String> = candidates
1539            .into_iter()
1540            .take(required)
1541            .map(|(url, _)| url)
1542            .collect();
1543
1544        if selected.len() < required {
1545            return Err(LitSdkError::Network(format!(
1546                "price feed returned only {} usable nodes, need {}",
1547                selected.len(),
1548                required
1549            )));
1550        }
1551
1552        Ok(selected)
1553    }
1554
1555    fn create_jit_context(&self) -> Result<NagaJitContext, LitSdkError> {
1556        let mut key_set = HashMap::new();
1557        for url in self.handshake.server_keys.keys() {
1558            let server_key = &self.handshake.server_keys[url];
1559            let pk_hex = server_key.node_identity_key.trim_start_matches("0x");
1560            let pk_bytes = hex::decode(pk_hex).map_err(|e| {
1561                LitSdkError::Config(format!("invalid node identity key for {url}: {e}"))
1562            })?;
1563            let mut pk32 = [0u8; 32];
1564            if pk_bytes.len() != 32 {
1565                return Err(LitSdkError::Config(
1566                    "node identity key must be 32 bytes".into(),
1567                ));
1568            }
1569            pk32.copy_from_slice(&pk_bytes);
1570
1571            let mut sk32 = [0u8; 32];
1572            rand::thread_rng().fill_bytes(&mut sk32);
1573
1574            key_set.insert(
1575                url.clone(),
1576                JitKeyPair {
1577                    public_key: pk32,
1578                    secret_key: sk32,
1579                },
1580            );
1581        }
1582
1583        Ok(NagaJitContext { key_set })
1584    }
1585}
1586
1587fn compose_lit_url(base: &str, endpoint: &Endpoint) -> String {
1588    format!("{}{}{}", base, endpoint.path, endpoint.version)
1589}
1590
1591async fn send_node_request<T: serde::de::DeserializeOwned>(
1592    http: &reqwest::Client,
1593    full_url: &str,
1594    mut body: serde_json::Value,
1595    request_id: &str,
1596    version: &str,
1597    epoch: u64,
1598) -> Result<T, LitSdkError> {
1599    let mut headers = HeaderMap::new();
1600    headers.insert("Content-Type", HeaderValue::from_static("application/json"));
1601    headers.insert("Accept", HeaderValue::from_static("application/json"));
1602    headers.insert("X-Lit-SDK-Version", HeaderValue::from_str(version).unwrap());
1603    headers.insert("X-Lit-SDK-Type", HeaderValue::from_static("Typescript"));
1604    headers.insert("X-Request-Id", HeaderValue::from_str(request_id).unwrap());
1605
1606    if let Some(obj) = body.as_object_mut() {
1607        obj.insert("epoch".into(), serde_json::json!(epoch));
1608    }
1609
1610    let res = http
1611        .post(full_url)
1612        .headers(headers)
1613        .json(&body)
1614        .send()
1615        .await?;
1616    let status = res.status();
1617
1618    if !status.is_success() {
1619        let text = res.text().await.unwrap_or_default();
1620        return Err(LitSdkError::Network(format!(
1621            "node request failed {}: {}",
1622            status, text
1623        )));
1624    }
1625
1626    let value: serde_json::Value = res.json().await?;
1627    match serde_json::from_value::<T>(value.clone()) {
1628        Ok(parsed) => Ok(parsed),
1629        Err(err) => {
1630            if let Some(data) = value.get("data").cloned() {
1631                serde_json::from_value::<T>(data).map_err(|e| LitSdkError::Network(e.to_string()))
1632            } else {
1633                Err(LitSdkError::Network(format!(
1634                    "unexpected response shape: {err}; body={value}"
1635                )))
1636            }
1637        }
1638    }
1639}
1640
1641/// Send a request whose body is an E2EE encrypted payload.
1642///
1643/// Nodes sometimes respond with an encrypted error payload and a non-2xx status.
1644/// This helper attempts to decrypt such errors using the per-node JIT secret key
1645/// so callers can see the underlying cause.
1646async fn send_encrypted_node_request(
1647    http: &reqwest::Client,
1648    full_url: &str,
1649    mut body: serde_json::Value,
1650    jit_secret_key: &[u8; 32],
1651    request_id: &str,
1652    version: &str,
1653    epoch: u64,
1654) -> Result<GenericEncryptedPayload, LitSdkError> {
1655    let mut headers = HeaderMap::new();
1656    headers.insert("Content-Type", HeaderValue::from_static("application/json"));
1657    headers.insert("Accept", HeaderValue::from_static("application/json"));
1658    headers.insert("X-Lit-SDK-Version", HeaderValue::from_str(version).unwrap());
1659    headers.insert("X-Lit-SDK-Type", HeaderValue::from_static("Typescript"));
1660    headers.insert("X-Request-Id", HeaderValue::from_str(request_id).unwrap());
1661
1662    if let Some(obj) = body.as_object_mut() {
1663        obj.insert("epoch".into(), serde_json::json!(epoch));
1664    }
1665
1666    let res = http
1667        .post(full_url)
1668        .headers(headers)
1669        .json(&body)
1670        .send()
1671        .await?;
1672    let status = res.status();
1673    let text = res.text().await.unwrap_or_default();
1674
1675    let value: serde_json::Value =
1676        serde_json::from_str(&text).unwrap_or_else(|_| serde_json::json!({ "raw": text }));
1677
1678    if status.is_success() {
1679        if let Ok(parsed) = serde_json::from_value::<GenericEncryptedPayload>(value.clone()) {
1680            return Ok(parsed);
1681        }
1682        if let Some(data) = value.get("data").cloned() {
1683            if let Ok(parsed) = serde_json::from_value::<GenericEncryptedPayload>(data.clone()) {
1684                return Ok(parsed);
1685            }
1686            // Some nodes return the encrypted payload directly under `data`.
1687            if let Ok(enc) = serde_json::from_value::<EncryptedPayload>(data) {
1688                return Ok(GenericEncryptedPayload {
1689                    success: true,
1690                    values: vec![enc],
1691                    error: None,
1692                });
1693            }
1694        }
1695        // Some nodes return a single encrypted payload on success.
1696        if let Ok(enc) = serde_json::from_value::<EncryptedPayload>(value.clone()) {
1697            return Ok(GenericEncryptedPayload {
1698                success: true,
1699                values: vec![enc],
1700                error: None,
1701            });
1702        }
1703        return Err(LitSdkError::Network(format!(
1704            "unexpected response shape; body={value}"
1705        )));
1706    }
1707
1708    // Try to decrypt an encrypted error payload.
1709    if let Ok(encrypted_err) = serde_json::from_value::<EncryptedPayload>(value.clone()) {
1710        if let Ok(bytes) = wallet_decrypt(jit_secret_key, &encrypted_err) {
1711            if let Ok(decrypted_text) = String::from_utf8(bytes) {
1712                return Err(LitSdkError::Network(format!(
1713                    "node returned encrypted error: {decrypted_text}"
1714                )));
1715            }
1716        }
1717    }
1718
1719    Err(LitSdkError::Network(format!(
1720        "node request failed {}: {}",
1721        status, text
1722    )))
1723}
1724
1725fn merge_encrypted_batch(
1726    results: Vec<Result<GenericEncryptedPayload, LitSdkError>>,
1727    minimum_successes: usize,
1728) -> Result<GenericEncryptedPayload, LitSdkError> {
1729    let mut values = vec![];
1730    let mut errors = vec![];
1731    let mut success_count = 0usize;
1732    for r in results {
1733        match r {
1734            Ok(batch) if batch.success => {
1735                success_count += 1;
1736                values.extend(batch.values)
1737            }
1738            Ok(batch) => {
1739                let err = batch
1740                    .error
1741                    .clone()
1742                    .unwrap_or_else(|| serde_json::json!(batch));
1743                errors.push(err);
1744            }
1745            Err(e) => errors.push(serde_json::json!({ "error": e.to_string() })),
1746        }
1747    }
1748
1749    if values.is_empty() || success_count < minimum_successes {
1750        return Err(LitSdkError::Network(format!(
1751            "insufficient successful encrypted responses: got {success_count}, need {minimum_successes}; errors={}",
1752            serde_json::to_string(&errors).unwrap_or_default()
1753        )));
1754    }
1755
1756    Ok(GenericEncryptedPayload {
1757        success: true,
1758        values,
1759        error: None,
1760    })
1761}
1762
1763fn decrypt_batch_response<T, F>(
1764    encrypted_result: &GenericEncryptedPayload,
1765    jit: &NagaJitContext,
1766    extract: F,
1767) -> Result<Vec<T>, LitSdkError>
1768where
1769    F: Fn(serde_json::Value) -> Result<T, LitSdkError>,
1770{
1771    if !encrypted_result.success {
1772        return Err(LitSdkError::Network("batch decrypt failed".into()));
1773    }
1774
1775    // Collect all secret keys for trying decryption
1776    let secret_keys: Vec<[u8; 32]> = jit.key_set.values().map(|kp| kp.secret_key).collect();
1777
1778    let mut out = vec![];
1779    for encrypted in &encrypted_result.values {
1780        let decrypted_bytes = wallet_decrypt_with_any_key(&secret_keys, encrypted)?;
1781        let decrypted_text =
1782            String::from_utf8(decrypted_bytes).map_err(|e| LitSdkError::Network(e.to_string()))?;
1783        let json_val: serde_json::Value = serde_json::from_str(&decrypted_text)
1784            .map_err(|e| LitSdkError::Network(e.to_string()))?;
1785
1786        out.push(extract(json_val)?);
1787    }
1788
1789    Ok(out)
1790}
1791
1792#[derive(Debug, Deserialize)]
1793#[serde(rename_all = "camelCase")]
1794struct DecryptNodeResponse {
1795    #[serde(rename = "signatureShare")]
1796    signature_share: SignatureShareWrapper,
1797    #[serde(rename = "shareId")]
1798    #[allow(dead_code)]
1799    share_id: String,
1800}
1801
1802#[derive(Debug, Deserialize)]
1803#[serde(rename_all = "camelCase")]
1804struct SignatureShareWrapper {
1805    #[serde(rename = "ProofOfPossession")]
1806    proof_of_possession: ProofOfPossession,
1807}
1808
1809#[derive(Debug, Deserialize)]
1810#[serde(rename_all = "camelCase")]
1811struct ProofOfPossession {
1812    identifier: String,
1813    value: String,
1814}
1815
1816#[derive(Debug, Deserialize)]
1817#[serde(rename_all = "camelCase")]
1818struct ExecuteJsNodeValue {
1819    success: bool,
1820    #[serde(default)]
1821    #[allow(dead_code)]
1822    claim_data: HashMap<String, serde_json::Value>,
1823    #[serde(default)]
1824    #[allow(dead_code)]
1825    decrypted_data: serde_json::Value,
1826    #[serde(default)]
1827    logs: String,
1828    #[serde(default)]
1829    response: String,
1830    #[serde(default)]
1831    signed_data: HashMap<String, serde_json::Value>,
1832}
1833
1834#[derive(Debug, Clone, Serialize)]
1835#[serde(rename_all = "camelCase")]
1836struct NodeSetEntry {
1837    socket_address: String,
1838    value: u64,
1839}
1840
1841fn pkp_sign_message_bytes(
1842    chain: &str,
1843    signing_scheme: &str,
1844    to_sign: &[u8],
1845    bypass_auto_hashing: bool,
1846) -> Result<Vec<u8>, LitSdkError> {
1847    if bypass_auto_hashing {
1848        return Ok(to_sign.to_vec());
1849    }
1850
1851    match signing_scheme {
1852        "EcdsaK256Sha256" | "EcdsaP256Sha256" => match chain {
1853            "ethereum" => Ok(Keccak256::digest(to_sign).to_vec()),
1854            "bitcoin" | "cosmos" => Ok(Sha256::digest(to_sign).to_vec()),
1855            other => Err(LitSdkError::Config(format!(
1856                "chain \"{other}\" does not support ECDSA signing with Lit yet"
1857            ))),
1858        },
1859        "EcdsaP384Sha384" => match chain {
1860            "ethereum" => Ok(Keccak384::digest(to_sign).to_vec()),
1861            "bitcoin" | "cosmos" => Ok(Sha384::digest(to_sign).to_vec()),
1862            other => Err(LitSdkError::Config(format!(
1863                "chain \"{other}\" does not support ECDSA signing with Lit yet"
1864            ))),
1865        },
1866        _ => Ok(to_sign.to_vec()),
1867    }
1868}
1869
1870fn node_set_from_urls(urls: &[String]) -> Vec<NodeSetEntry> {
1871    urls.iter()
1872        .map(|url| NodeSetEntry {
1873            socket_address: url.replace("http://", "").replace("https://", ""),
1874            value: 1,
1875        })
1876        .collect()
1877}
1878
1879async fn orchestrate_handshake(
1880    http: &reqwest::Client,
1881    config: &NetworkConfig,
1882    version: &str,
1883) -> Result<OrchestrateHandshakeResponse, LitSdkError> {
1884    let request_id = random_hex(16);
1885    let timeout_dur = Duration::from_millis(config.abort_timeout_ms);
1886
1887    let fut = async {
1888        let mut server_keys: HashMap<String, RawHandshakeResponse> = HashMap::new();
1889
1890        let mut tasks = vec![];
1891        for url in &config.bootstrap_urls {
1892            let full_url = compose_lit_url(url, &config.endpoints.handshake);
1893            let challenge = random_hex(32);
1894            let body = serde_json::to_value(HandshakeRequestData {
1895                client_public_key: "test".into(),
1896                challenge: challenge.clone(),
1897                epoch: Some(0),
1898            })
1899            .unwrap();
1900
1901            let url_clone = url.clone();
1902            let request_id_clone = request_id.clone();
1903            let challenge_clone = challenge.clone();
1904            tasks.push(async move {
1905                let res: Result<RawHandshakeResponse, LitSdkError> =
1906                    send_node_request(http, &full_url, body, &request_id_clone, version, 0).await;
1907                (url_clone, challenge_clone, res)
1908            });
1909        }
1910
1911        let results = futures::future::join_all(tasks).await;
1912
1913        let mut successful_urls = vec![];
1914        let mut failed_urls = vec![];
1915        let mut errors: Vec<String> = vec![];
1916
1917        for (url, challenge, res) in results {
1918            match res {
1919                Ok(keys) => {
1920                    if config.required_attestation {
1921                        let Some(attestation) = keys.attestation.as_ref() else {
1922                            errors.push(format!("{url}: missing attestation"));
1923                            failed_urls.push(url);
1924                            continue;
1925                        };
1926                        if let Err(e) =
1927                            verify_sev_snp_attestation(http, attestation, &challenge).await
1928                        {
1929                            errors.push(format!("{url}: {e}"));
1930                            failed_urls.push(url);
1931                            continue;
1932                        }
1933                    }
1934                    let pk_hex = keys.node_identity_key.trim_start_matches("0x");
1935                    match hex::decode(pk_hex) {
1936                        Ok(bytes) if bytes.len() == 32 => {}
1937                        _ => {
1938                            errors.push(format!("{url}: invalid nodeIdentityKey"));
1939                            failed_urls.push(url);
1940                            continue;
1941                        }
1942                    }
1943                    successful_urls.push(url.clone());
1944                    server_keys.insert(url, keys);
1945                }
1946                Err(e) => {
1947                    errors.push(format!("{url}: {e}"));
1948                    failed_urls.push(url);
1949                }
1950            }
1951        }
1952
1953        let minimum_required = config.minimum_threshold.max(1);
1954
1955        if successful_urls.len() < minimum_required {
1956            return Err(LitSdkError::Handshake(format!(
1957                "insufficient successful handshakes: got {}, need {}; errors={}",
1958                successful_urls.len(),
1959                minimum_required,
1960                errors.join("; ")
1961            )));
1962        }
1963
1964        let core = resolve_handshake_response(&server_keys, &request_id)?;
1965        let epoch = most_common_u64(server_keys.values().map(|k| k.epoch).collect()).unwrap_or(0);
1966
1967        Ok(OrchestrateHandshakeResponse {
1968            server_keys,
1969            connected_nodes: successful_urls,
1970            core_node_config: core,
1971            threshold: minimum_required,
1972            epoch,
1973        })
1974    };
1975
1976    timeout(timeout_dur, fut).await.map_err(|_| {
1977        LitSdkError::Handshake(format!(
1978            "handshake timed out after {}ms",
1979            config.abort_timeout_ms
1980        ))
1981    })?
1982}
1983
1984async fn verify_sev_snp_attestation(
1985    http: &reqwest::Client,
1986    attestation: &serde_json::Value,
1987    challenge_hex: &str,
1988) -> Result<(), LitSdkError> {
1989    let att_obj = attestation
1990        .as_object()
1991        .ok_or_else(|| LitSdkError::Handshake("invalid attestation: expected object".into()))?;
1992
1993    let typ = att_obj
1994        .get("type")
1995        .and_then(|v| v.as_str())
1996        .ok_or_else(|| LitSdkError::Handshake("invalid attestation: missing type".into()))?;
1997
1998    if typ != "AMD_SEV_SNP" {
1999        return Err(LitSdkError::Handshake(format!(
2000            "unsupported attestation type {typ}"
2001        )));
2002    }
2003
2004    let challenge_bytes = hex::decode(challenge_hex)
2005        .map_err(|e| LitSdkError::Handshake(format!("invalid attestation challenge hex: {e}")))?;
2006    if challenge_bytes.len() != 32 {
2007        return Err(LitSdkError::Handshake(format!(
2008            "attestation challenge must be 32 bytes; got {}",
2009            challenge_bytes.len()
2010        )));
2011    }
2012
2013    let noonce_val = att_obj
2014        .get("noonce")
2015        .ok_or_else(|| LitSdkError::Handshake("invalid attestation: missing noonce".into()))?;
2016    let noonce = parse_attestation_bytes(noonce_val, "noonce")?;
2017    if noonce != challenge_bytes {
2018        return Err(LitSdkError::Handshake(
2019            "attestation noonce does not match challenge".into(),
2020        ));
2021    }
2022
2023    let report_val = att_obj
2024        .get("report")
2025        .ok_or_else(|| LitSdkError::Handshake("invalid attestation: missing report".into()))?;
2026    let report_bytes = parse_attestation_bytes(report_val, "report")?;
2027
2028    let mut data_map: BTreeMap<String, Vec<u8>> = BTreeMap::new();
2029    let data_obj = att_obj
2030        .get("data")
2031        .and_then(|v| v.as_object())
2032        .ok_or_else(|| LitSdkError::Handshake("invalid attestation: missing data".into()))?;
2033    for (k, v) in data_obj {
2034        data_map.insert(k.clone(), parse_attestation_bytes(v, &format!("data.{k}"))?);
2035    }
2036
2037    let mut signatures: Vec<Vec<u8>> = vec![];
2038    if let Some(sig_arr) = att_obj.get("signatures").and_then(|v| v.as_array()) {
2039        for (idx, v) in sig_arr.iter().enumerate() {
2040            signatures.push(parse_attestation_bytes(v, &format!("signatures[{idx}]"))?);
2041        }
2042    }
2043
2044    use lit_sdk::sev::certs::snp::Certificate;
2045    use lit_sdk::sev::firmware::guest::AttestationReport;
2046    use lit_sdk::sev::parser::ByteParser;
2047
2048    let report = AttestationReport::from_bytes(&report_bytes).map_err(|e| {
2049        LitSdkError::Handshake(format!("invalid SEV-SNP attestation report bytes: {e}"))
2050    })?;
2051
2052    let vcek_url = SevSnp::vcek_url(&report);
2053    let res = http.get(&vcek_url).send().await?;
2054    let status = res.status();
2055    let cert_bytes = res.bytes().await?;
2056    if !status.is_success() {
2057        return Err(LitSdkError::Handshake(format!(
2058            "failed to fetch VCEK certificate ({status}) from {vcek_url}"
2059        )));
2060    }
2061
2062    let cert = Certificate::from_der(cert_bytes.as_ref())
2063        .or_else(|_| Certificate::from_pem(cert_bytes.as_ref()))
2064        .map_err(|e| LitSdkError::Handshake(format!("invalid VCEK certificate: {e}")))?;
2065
2066    SevSnp::verify(&report, &data_map, &signatures, &challenge_bytes, &cert)?;
2067    Ok(())
2068}
2069
2070fn parse_attestation_bytes(v: &serde_json::Value, field: &str) -> Result<Vec<u8>, LitSdkError> {
2071    match v {
2072        serde_json::Value::String(s) => base64ct::Base64::decode_vec(s)
2073            .map_err(|e| LitSdkError::Handshake(format!("invalid base64 for {field}: {e}"))),
2074        serde_json::Value::Array(arr) => {
2075            let mut out = Vec::with_capacity(arr.len());
2076            for (idx, entry) in arr.iter().enumerate() {
2077                let n = entry.as_u64().ok_or_else(|| {
2078                    LitSdkError::Handshake(format!(
2079                        "invalid byte value for {field}[{idx}]: expected number"
2080                    ))
2081                })?;
2082                if n > 255 {
2083                    return Err(LitSdkError::Handshake(format!(
2084                        "invalid byte value for {field}[{idx}]: {n}"
2085                    )));
2086                }
2087                out.push(n as u8);
2088            }
2089            Ok(out)
2090        }
2091        serde_json::Value::Null => Err(LitSdkError::Handshake(format!("invalid {field}: null"))),
2092        other => Err(LitSdkError::Handshake(format!(
2093            "invalid {field}: expected base64 string or byte array, got {other}"
2094        ))),
2095    }
2096}
2097
2098fn resolve_handshake_response(
2099    server_keys: &HashMap<String, RawHandshakeResponse>,
2100    request_id: &str,
2101) -> Result<ResolvedHandshakeResponse, LitSdkError> {
2102    let latest_blockhash = most_common_value(
2103        server_keys
2104            .values()
2105            .map(|k| k.latest_blockhash.clone())
2106            .collect(),
2107    )
2108    .ok_or_else(|| {
2109        LitSdkError::Handshake(format!(
2110            "latestBlockhash unavailable for request {}",
2111            request_id
2112        ))
2113    })?;
2114
2115    let subnet_pub_key = most_common_value(
2116        server_keys
2117            .values()
2118            .map(|k| k.subnet_public_key.clone())
2119            .collect(),
2120    )
2121    .unwrap_or_default();
2122
2123    let network_pub_key = most_common_value(
2124        server_keys
2125            .values()
2126            .map(|k| k.network_public_key.clone())
2127            .collect(),
2128    )
2129    .unwrap_or_default();
2130
2131    let network_pub_key_set = most_common_value(
2132        server_keys
2133            .values()
2134            .map(|k| k.network_public_key_set.clone())
2135            .collect(),
2136    )
2137    .unwrap_or_default();
2138
2139    let hd_root_pubkeys = most_common_value_vec(
2140        server_keys
2141            .values()
2142            .map(|k| k.hd_root_pubkeys.clone())
2143            .collect(),
2144    )
2145    .unwrap_or_default();
2146
2147    Ok(ResolvedHandshakeResponse {
2148        subnet_pub_key,
2149        network_pub_key,
2150        network_pub_key_set,
2151        hd_root_pubkeys,
2152        latest_blockhash,
2153    })
2154}
2155
2156fn most_common_value(values: Vec<String>) -> Option<String> {
2157    let mut counts: HashMap<String, usize> = HashMap::new();
2158    for v in values {
2159        *counts.entry(v).or_insert(0) += 1;
2160    }
2161    counts.into_iter().max_by_key(|(_, c)| *c).map(|(v, _)| v)
2162}
2163
2164fn most_common_value_vec(values: Vec<Vec<String>>) -> Option<Vec<String>> {
2165    let mut counts: HashMap<String, usize> = HashMap::new();
2166    for v in values {
2167        let key = serde_json::to_string(&v).ok()?;
2168        *counts.entry(key).or_insert(0) += 1;
2169    }
2170    counts
2171        .into_iter()
2172        .max_by_key(|(_, c)| *c)
2173        .and_then(|(k, _)| serde_json::from_str(&k).ok())
2174}
2175
2176fn most_common_u64(values: Vec<u64>) -> Option<u64> {
2177    let mut counts: HashMap<u64, usize> = HashMap::new();
2178    for v in values {
2179        *counts.entry(v).or_insert(0) += 1;
2180    }
2181    counts.into_iter().max_by_key(|(_, c)| *c).map(|(v, _)| v)
2182}
2183
2184fn sha256_hex(bytes: &[u8]) -> String {
2185    let mut hasher = Sha256::new();
2186    hasher.update(bytes);
2187    hex::encode(hasher.finalize())
2188}
2189
2190fn random_hex(num_bytes: usize) -> String {
2191    let mut b = vec![0u8; num_bytes];
2192    rand::thread_rng().fill_bytes(&mut b);
2193    hex::encode(b)
2194}