pyra-privy 0.9.3

Privy authentication client core: P-256 signing, wallet lookup, transaction signing
Documentation
use serde::{Deserialize, Serialize};

// ─── Wallet types ─────────────────────────────────────────────────────

/// A Privy-managed wallet.
#[derive(Debug, Clone, Deserialize)]
pub struct Wallet {
    pub id: String,
    pub address: String,
    pub chain_type: String,
    #[serde(default)]
    pub policy_ids: Vec<String>,
    #[serde(default)]
    pub additional_signers: Vec<WalletSigner>,
}

/// A signer attached to a wallet.
#[derive(Debug, Clone, Deserialize)]
pub struct WalletSigner {
    pub signer_id: String,
    #[serde(default)]
    pub override_policy_ids: Vec<String>,
}

/// Response from GET /v1/wallets.
#[derive(Debug, Deserialize)]
pub struct WalletsResponse {
    pub data: Vec<Wallet>,
}

// ─── Sign transaction types ───────────────────────────────────────────

/// Response from POST /v1/wallets/{id}/rpc (signTransaction).
#[derive(Debug, Deserialize)]
pub struct SignTransactionResponse {
    #[serde(default)]
    pub method: String,
    pub data: SignTransactionData,
}

/// Data field from the signTransaction response.
#[derive(Debug, Deserialize)]
pub struct SignTransactionData {
    pub signed_transaction: String,
    #[serde(default)]
    pub encoding: String,
}

// ─── Policy types ─────────────────────────────────────────────────────

/// Request body for POST /v1/policies.
#[derive(Debug, Serialize)]
pub struct CreatePolicyRequest {
    pub version: String,
    pub name: String,
    pub chain_type: String,
    pub rules: Vec<PolicyRule>,
}

/// A single rule within a delegation policy.
#[derive(Debug, Clone, Serialize)]
pub struct PolicyRule {
    pub name: String,
    pub method: String,
    pub conditions: Vec<PolicyCondition>,
    pub action: String,
}

/// A condition within a policy rule.
#[derive(Debug, Clone, Serialize)]
pub struct PolicyCondition {
    pub field_source: String,
    pub field: String,
    pub operator: String,
    pub value: serde_json::Value,
}

/// Response from POST /v1/policies.
#[derive(Debug, Deserialize)]
pub struct CreatePolicyResponse {
    pub id: String,
}

/// Response from GET /v1/policies/{id}.
#[derive(Debug, Deserialize, Default)]
#[serde(default)]
pub struct GetPolicyResponse {
    pub id: String,
    pub name: String,
    pub rules: Vec<PolicyRuleResponse>,
}

/// A rule as returned by GET /v1/policies/{id}.
#[derive(Debug, Deserialize, Default)]
#[serde(default)]
pub struct PolicyRuleResponse {
    pub id: String,
    pub name: String,
    pub method: String,
    pub conditions: Vec<PolicyConditionResponse>,
    pub action: String,
}

/// A condition as returned by GET /v1/policies/{id}.
#[derive(Debug, Deserialize, Default)]
#[serde(default)]
pub struct PolicyConditionResponse {
    pub field_source: String,
    pub field: String,
    pub operator: String,
    pub value: serde_json::Value,
}

// ─── JWT types ────────────────────────────────────────────────────────

/// Privy Identity Token claims.
#[derive(Debug, Serialize, Deserialize)]
pub struct PrivyClaims {
    pub aud: String,
    pub exp: u64,
    pub iss: String,
    /// User ID (Privy DID, e.g. "did:privy:abc123").
    pub sub: String,
    pub sid: String,
    pub iat: u64,
    #[serde(default)]
    pub linked_accounts: Option<String>,
    #[serde(default)]
    pub custom_metadata: Option<String>,
}

/// Parsed linked account from identity token.
#[derive(Debug, Clone, Deserialize)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum LinkedAccount {
    Email {
        address: String,
    },
    Wallet {
        address: String,
        #[serde(default)]
        chain_type: Option<String>,
    },
    Phone {
        #[serde(rename = "phoneNumber")]
        phone_number: String,
    },
    Google {
        email: String,
        #[serde(default)]
        name: Option<String>,
    },
    Apple {
        email: String,
    },
    #[serde(other)]
    Unknown,
}

impl PrivyClaims {
    /// Extract the primary email from linked_accounts.
    pub fn get_email(&self) -> Option<String> {
        let accounts_json = self.linked_accounts.as_ref()?;
        let accounts: Vec<LinkedAccount> = serde_json::from_str(accounts_json).ok()?;
        accounts.into_iter().find_map(|acc| match acc {
            LinkedAccount::Email { address } => Some(address),
            LinkedAccount::Google { email, .. } => Some(email),
            LinkedAccount::Apple { email } => Some(email),
            _ => None,
        })
    }

    /// Extract all emails from linked_accounts.
    pub fn get_all_emails(&self) -> Vec<String> {
        let Some(accounts_json) = self.linked_accounts.as_ref() else {
            return Vec::new();
        };
        let Ok(accounts) = serde_json::from_str::<Vec<LinkedAccount>>(accounts_json) else {
            return Vec::new();
        };
        accounts
            .into_iter()
            .filter_map(|acc| match acc {
                LinkedAccount::Email { address } => Some(address),
                LinkedAccount::Google { email, .. } => Some(email),
                LinkedAccount::Apple { email } => Some(email),
                _ => None,
            })
            .collect()
    }

    /// Extract wallet addresses from linked_accounts.
    pub fn get_wallet_addresses(&self) -> Vec<String> {
        let Some(accounts_json) = self.linked_accounts.as_ref() else {
            return Vec::new();
        };
        let Ok(accounts) = serde_json::from_str::<Vec<LinkedAccount>>(accounts_json) else {
            return Vec::new();
        };
        accounts
            .into_iter()
            .filter_map(|acc| match acc {
                LinkedAccount::Wallet { address, .. } => Some(address),
                _ => None,
            })
            .collect()
    }
}