1use base64::{engine::general_purpose::STANDARD, Engine};
2use solana_sdk::{pubkey::Pubkey, signature::Signature};
3use std::str::FromStr;
4
5mod types;
6mod utils;
7
8pub use types::*;
9pub use utils::*;
10
11impl PrivyConfig {
12 pub fn from_env() -> Self {
14 Self {
15 app_id: std::env::var("PRIVY_APP_ID").ok(),
16 app_secret: std::env::var("PRIVY_APP_SECRET").ok(),
17 wallet_id: std::env::var("PRIVY_WALLET_ID").ok(),
18 }
19 }
20
21 pub fn merge_with_cli(
23 mut self,
24 app_id: Option<String>,
25 app_secret: Option<String>,
26 wallet_id: Option<String>,
27 ) -> Self {
28 if app_id.is_some() {
29 self.app_id = app_id;
30 }
31 if app_secret.is_some() {
32 self.app_secret = app_secret;
33 }
34 if wallet_id.is_some() {
35 self.wallet_id = wallet_id;
36 }
37 self
38 }
39
40 pub fn build(self) -> Result<PrivySigner, PrivyError> {
42 Ok(PrivySigner {
43 app_id: self.app_id.ok_or(PrivyError::MissingConfig("app_id"))?,
44 app_secret: self.app_secret.ok_or(PrivyError::MissingConfig("app_secret"))?,
45 wallet_id: self.wallet_id.ok_or(PrivyError::MissingConfig("wallet_id"))?,
46 api_base_url: "https://api.privy.io/v1".to_string(),
47 client: reqwest::Client::new(),
48 public_key: Pubkey::default(), })
50 }
51}
52
53impl PrivySigner {
54 pub fn new(app_id: String, app_secret: String, wallet_id: String) -> Self {
56 Self {
57 app_id,
58 app_secret,
59 wallet_id,
60 api_base_url: "https://api.privy.io/v1".to_string(),
61 client: reqwest::Client::new(),
62 public_key: Pubkey::default(),
63 }
64 }
65
66 pub async fn init(&mut self) -> Result<(), PrivyError> {
68 let pubkey = self.get_public_key().await?;
69 self.public_key = pubkey;
70 Ok(())
71 }
72
73 pub fn solana_pubkey(&self) -> Pubkey {
75 self.public_key
76 }
77
78 fn get_auth_header(&self) -> String {
80 let credentials = format!("{}:{}", self.app_id, self.app_secret);
81 format!("Basic {}", STANDARD.encode(credentials))
82 }
83
84 pub async fn get_public_key(&self) -> Result<Pubkey, PrivyError> {
86 let url = format!("{}/wallets/{}", self.api_base_url, self.wallet_id);
87
88 let response = self
89 .client
90 .get(&url)
91 .header("Authorization", self.get_auth_header())
92 .header("privy-app-id", &self.app_id)
93 .send()
94 .await?;
95
96 if !response.status().is_success() {
97 return Err(PrivyError::ApiError(response.status().as_u16()));
98 }
99
100 let wallet_info: WalletResponse = response.json().await?;
101
102 Pubkey::from_str(&wallet_info.address).map_err(|_| PrivyError::InvalidPublicKey)
104 }
105
106 pub async fn sign_solana(&self, transaction: &[u8]) -> Result<Signature, PrivyError> {
111 let url = format!("{}/wallets/{}/rpc", self.api_base_url, self.wallet_id);
112
113 let request = SignTransactionRequest {
114 method: "signTransaction",
115 params: SignTransactionParams {
116 transaction: STANDARD.encode(transaction),
117 encoding: "base64",
118 },
119 };
120
121 let response = self
122 .client
123 .post(&url)
124 .header("Authorization", self.get_auth_header())
125 .header("privy-app-id", &self.app_id)
126 .header("Content-Type", "application/json")
127 .json(&request)
128 .send()
129 .await?;
130
131 if !response.status().is_success() {
132 let status = response.status().as_u16();
133 let error_text = response.text().await.unwrap_or_default();
134 return Err(PrivyError::ApiError(status));
135 }
136
137 let response_text = response.text().await?;
138
139 let sign_response: SignTransactionResponse = serde_json::from_str(&response_text)?;
140
141 let signed_tx_bytes = STANDARD.decode(&sign_response.data.signed_transaction)?;
143
144 use solana_sdk::transaction::Transaction;
146 let signed_tx: Transaction =
147 bincode::deserialize(&signed_tx_bytes).map_err(|_| PrivyError::InvalidResponse)?;
148
149 if let Some(signature) = signed_tx.signatures.first() {
151 Ok(*signature)
152 } else {
153 Err(PrivyError::InvalidSignature)
154 }
155 }
156
157 pub async fn sign(&self, message: &[u8]) -> Result<Vec<u8>, PrivyError> {
158 let signature = self.sign_solana(message).await?;
159 Ok(signature.as_ref().to_vec())
160 }
161}
162
163#[cfg(test)]
164mod tests {
165 use super::*;
166
167 #[test]
168 fn test_config_from_env() {
169 let saved_app_id = std::env::var("PRIVY_APP_ID").ok();
171 let saved_app_secret = std::env::var("PRIVY_APP_SECRET").ok();
172 let saved_wallet_id = std::env::var("PRIVY_WALLET_ID").ok();
173
174 std::env::set_var("PRIVY_APP_ID", "test_app_id");
176 std::env::set_var("PRIVY_APP_SECRET", "test_secret");
177 std::env::set_var("PRIVY_WALLET_ID", "test_wallet");
178
179 let config = PrivyConfig::from_env();
180 assert_eq!(config.app_id, Some("test_app_id".to_string()));
181 assert_eq!(config.app_secret, Some("test_secret".to_string()));
182 assert_eq!(config.wallet_id, Some("test_wallet".to_string()));
183
184 match saved_app_id {
186 Some(val) => std::env::set_var("PRIVY_APP_ID", val),
187 None => std::env::remove_var("PRIVY_APP_ID"),
188 }
189 match saved_app_secret {
190 Some(val) => std::env::set_var("PRIVY_APP_SECRET", val),
191 None => std::env::remove_var("PRIVY_APP_SECRET"),
192 }
193 match saved_wallet_id {
194 Some(val) => std::env::set_var("PRIVY_WALLET_ID", val),
195 None => std::env::remove_var("PRIVY_WALLET_ID"),
196 }
197 }
198
199 #[test]
200 fn test_config_merge() {
201 let config = PrivyConfig {
202 app_id: Some("env_id".to_string()),
203 app_secret: Some("env_secret".to_string()),
204 wallet_id: None,
205 };
206
207 let merged =
208 config.merge_with_cli(Some("cli_id".to_string()), None, Some("cli_wallet".to_string()));
209
210 assert_eq!(merged.app_id, Some("cli_id".to_string()));
211 assert_eq!(merged.app_secret, Some("env_secret".to_string()));
212 assert_eq!(merged.wallet_id, Some("cli_wallet".to_string()));
213 }
214}