use base64::{engine::general_purpose::STANDARD, Engine};
use solana_sdk::{pubkey::Pubkey, signature::Signature};
use std::str::FromStr;
mod types;
mod utils;
pub use types::*;
pub use utils::*;
impl PrivyConfig {
pub fn from_env() -> Self {
Self {
app_id: std::env::var("PRIVY_APP_ID").ok(),
app_secret: std::env::var("PRIVY_APP_SECRET").ok(),
wallet_id: std::env::var("PRIVY_WALLET_ID").ok(),
}
}
pub fn merge_with_cli(
mut self,
app_id: Option<String>,
app_secret: Option<String>,
wallet_id: Option<String>,
) -> Self {
if app_id.is_some() {
self.app_id = app_id;
}
if app_secret.is_some() {
self.app_secret = app_secret;
}
if wallet_id.is_some() {
self.wallet_id = wallet_id;
}
self
}
pub fn build(self) -> Result<PrivySigner, PrivyError> {
Ok(PrivySigner {
app_id: self.app_id.ok_or(PrivyError::MissingConfig("app_id"))?,
app_secret: self.app_secret.ok_or(PrivyError::MissingConfig("app_secret"))?,
wallet_id: self.wallet_id.ok_or(PrivyError::MissingConfig("wallet_id"))?,
api_base_url: "https://api.privy.io/v1".to_string(),
client: reqwest::Client::new(),
public_key: None, })
}
}
impl PrivySigner {
pub fn new(app_id: String, app_secret: String, wallet_id: String) -> Self {
Self {
app_id,
app_secret,
wallet_id,
api_base_url: "https://api.privy.io/v1".to_string(),
client: reqwest::Client::new(),
public_key: None,
}
}
pub async fn init(&mut self) -> Result<(), PrivyError> {
let pubkey = self.get_public_key().await?;
self.public_key = Some(pubkey);
Ok(())
}
pub fn solana_pubkey(&self) -> Pubkey {
self.public_key.expect("PrivySigner not initialized. Call init() first.")
}
fn get_auth_header(&self) -> String {
let credentials = format!("{}:{}", self.app_id, self.app_secret);
format!("Basic {}", STANDARD.encode(credentials))
}
pub async fn get_public_key(&self) -> Result<Pubkey, PrivyError> {
let url = format!("{}/wallets/{}", self.api_base_url, self.wallet_id);
let response = self
.client
.get(&url)
.header("Authorization", self.get_auth_header())
.header("privy-app-id", &self.app_id)
.send()
.await?;
if !response.status().is_success() {
return Err(PrivyError::ApiError(response.status().as_u16()));
}
let wallet_info: WalletResponse = response.json().await?;
Pubkey::from_str(&wallet_info.address).map_err(|_| PrivyError::InvalidPublicKey)
}
pub async fn sign_solana(&self, transaction: &[u8]) -> Result<Signature, PrivyError> {
let url = format!("{}/wallets/{}/rpc", self.api_base_url, self.wallet_id);
let request = SignTransactionRequest {
method: "signTransaction",
params: SignTransactionParams {
transaction: STANDARD.encode(transaction),
encoding: "base64",
},
};
let response = self
.client
.post(&url)
.header("Authorization", self.get_auth_header())
.header("privy-app-id", &self.app_id)
.header("Content-Type", "application/json")
.json(&request)
.send()
.await?;
if !response.status().is_success() {
let status = response.status().as_u16();
let error_text = response.text().await.unwrap_or_default();
return Err(PrivyError::ApiError(status));
}
let response_text = response.text().await?;
let sign_response: SignTransactionResponse = serde_json::from_str(&response_text)?;
let signed_tx_bytes = STANDARD.decode(&sign_response.data.signed_transaction)?;
use solana_sdk::transaction::Transaction;
let signed_tx: Transaction =
bincode::deserialize(&signed_tx_bytes).map_err(|_| PrivyError::InvalidResponse)?;
if let Some(signature) = signed_tx.signatures.first() {
Ok(*signature)
} else {
Err(PrivyError::InvalidSignature)
}
}
pub async fn sign(&self, message: &[u8]) -> Result<Vec<u8>, PrivyError> {
let signature = self.sign_solana(message).await?;
Ok(signature.as_ref().to_vec())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_config_from_env() {
let saved_app_id = std::env::var("PRIVY_APP_ID").ok();
let saved_app_secret = std::env::var("PRIVY_APP_SECRET").ok();
let saved_wallet_id = std::env::var("PRIVY_WALLET_ID").ok();
std::env::set_var("PRIVY_APP_ID", "test_app_id");
std::env::set_var("PRIVY_APP_SECRET", "test_secret");
std::env::set_var("PRIVY_WALLET_ID", "test_wallet");
let config = PrivyConfig::from_env();
assert_eq!(config.app_id, Some("test_app_id".to_string()));
assert_eq!(config.app_secret, Some("test_secret".to_string()));
assert_eq!(config.wallet_id, Some("test_wallet".to_string()));
match saved_app_id {
Some(val) => std::env::set_var("PRIVY_APP_ID", val),
None => std::env::remove_var("PRIVY_APP_ID"),
}
match saved_app_secret {
Some(val) => std::env::set_var("PRIVY_APP_SECRET", val),
None => std::env::remove_var("PRIVY_APP_SECRET"),
}
match saved_wallet_id {
Some(val) => std::env::set_var("PRIVY_WALLET_ID", val),
None => std::env::remove_var("PRIVY_WALLET_ID"),
}
}
#[test]
fn test_config_merge() {
let config = PrivyConfig {
app_id: Some("env_id".to_string()),
app_secret: Some("env_secret".to_string()),
wallet_id: None,
};
let merged =
config.merge_with_cli(Some("cli_id".to_string()), None, Some("cli_wallet".to_string()));
assert_eq!(merged.app_id, Some("cli_id".to_string()));
assert_eq!(merged.app_secret, Some("env_secret".to_string()));
assert_eq!(merged.wallet_id, Some("cli_wallet".to_string()));
}
}