quorum_wallet/
quorum_wallet.rs

1//! Quorum Wallet Example
2//!
3//! This example demonstrates the complete quorum workflow:
4//! - Generate P-256 key pairs for quorum members
5//! - Create a 2-of-3 key quorum
6//! - Create a wallet owned by the quorum
7//! - Test signing with insufficient keys (should fail)
8//! - Test signing with sufficient keys (should succeed)
9//!
10//! This shows how to use Privy's key-based quorum system for multi-signature
11//! wallet operations, where multiple cryptographic keys are required to authorize
12//! wallet operations like signing transactions or exporting private keys.
13//!
14//! ## Required Environment Variables
15//! - `PRIVY_APP_ID`: Your Privy app ID
16//! - `PRIVY_APP_SECRET`: Your Privy app secret
17//!
18//! ## Usage
19//! ```bash
20//! cargo run --example quorum_wallet
21//! ```
22
23use anyhow::Result;
24use p256::elliptic_curve::SecretKey;
25use privy_rs::{
26    AuthorizationContext, PrivateKey, PrivyClient,
27    generated::types::{
28        CreateKeyQuorumBody, CreateKeyQuorumBodyDisplayName, CreateWalletBody, WalletChainType,
29    },
30};
31use tracing_subscriber::EnvFilter;
32use uuid::Uuid;
33
34#[tokio::main]
35async fn main() -> Result<()> {
36    tracing_subscriber::fmt()
37        .with_env_filter(
38            EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new("info")),
39        )
40        .init();
41
42    // Initialize client from environment variables
43    let client = PrivyClient::new_from_env()?;
44
45    tracing::info!("initialized privy client from environment");
46
47    // Generate unique identifiers for this example run
48    let timestamp = std::time::SystemTime::now()
49        .duration_since(std::time::SystemTime::UNIX_EPOCH)?
50        .as_secs();
51    let wallet_idempotency_key = format!("quorum-wallet-{}", Uuid::new_v4());
52
53    tracing::info!("Starting quorum wallet example");
54
55    // Step 1: Generate three P-256 private keys for quorum
56    tracing::info!("Generating three P-256 private keys for 2-of-3 quorum");
57    let mut rng = rand::thread_rng();
58    let key1 = SecretKey::<p256::NistP256>::random(&mut rng);
59    let key2 = SecretKey::<p256::NistP256>::random(&mut rng);
60    let key3 = SecretKey::<p256::NistP256>::random(&mut rng);
61
62    let pubkey1 = key1.public_key().to_string();
63    let pubkey2 = key2.public_key().to_string();
64    let pubkey3 = key3.public_key().to_string();
65
66    tracing::info!("Generated public keys:");
67    tracing::info!("Key 1: {}", pubkey1);
68    tracing::info!("Key 2: {}", pubkey2);
69    tracing::info!("Key 3: {}", pubkey3);
70
71    // Step 2: Create a 2-of-3 key quorum
72    tracing::info!("Creating 2-of-3 key quorum");
73
74    let quorum_display_name =
75        CreateKeyQuorumBodyDisplayName::try_from(format!("Quorum Example {timestamp}").as_str())?;
76
77    let quorum_body = CreateKeyQuorumBody {
78        authorization_threshold: Some(2.0), // 2-of-3 threshold
79        display_name: Some(quorum_display_name),
80        public_keys: vec![pubkey1, pubkey2, pubkey3],
81        user_ids: vec![],
82    };
83
84    let key_quorum = client.key_quorums().create(&quorum_body).await?;
85
86    tracing::info!("Created key quorum with ID: {}", key_quorum.id);
87    tracing::info!("Quorum threshold: {:?}", key_quorum.authorization_threshold);
88
89    // Step 3: Create a new Ethereum wallet owned by the quorum
90    tracing::info!(
91        "Creating new Ethereum wallet owned by quorum with idempotency key: {}",
92        wallet_idempotency_key
93    );
94
95    let create_body = CreateWalletBody {
96        chain_type: WalletChainType::Ethereum,
97        additional_signers: None,
98        owner: None,
99        owner_id: Some(key_quorum.id.parse().unwrap()),
100        policy_ids: vec![],
101    };
102
103    let wallet = client
104        .wallets()
105        .create(Some(&wallet_idempotency_key), &create_body)
106        .await?;
107
108    tracing::info!("Created wallet with ID: {}", wallet.id);
109    tracing::info!("Wallet address: {}", wallet.address);
110    tracing::info!("Wallet owner ID: {:?}", wallet.owner_id);
111
112    // Step 4: Test signing with only one key (should fail)
113    tracing::info!("Testing wallet export with only one key (should fail due to quorum threshold)");
114
115    let single_key_ctx = AuthorizationContext::new().push(PrivateKey(
116        key1.to_sec1_pem(der::pem::LineEnding::LF)
117            .unwrap()
118            .as_str()
119            .to_owned(),
120    ));
121
122    let single_key_result = client.wallets().export(&wallet.id, &single_key_ctx).await;
123
124    match single_key_result {
125        Err(err) => {
126            tracing::info!(
127                "✓ Single key authorization correctly failed as expected: {:?}",
128                err
129            );
130        }
131        Ok(_) => {
132            tracing::error!("✗ Single key authorization should have failed but succeeded!");
133            return Err(anyhow::anyhow!(
134                "Single key authorization should have failed due to quorum threshold"
135            ));
136        }
137    }
138
139    // Step 5: Test signing with two keys (should succeed)
140    tracing::info!("Testing wallet export with two keys (should succeed)");
141
142    let two_key_ctx = single_key_ctx.push(PrivateKey(
143        key2.to_sec1_pem(der::pem::LineEnding::LF)
144            .unwrap()
145            .as_str()
146            .to_owned(),
147    ));
148
149    let two_key_result = client.wallets().export(&wallet.id, &two_key_ctx).await;
150
151    match two_key_result {
152        Ok(export_result) => {
153            tracing::info!("✓ Two key authorization succeeded as expected");
154            tracing::info!("Exported private key length: {} bytes", export_result.len());
155        }
156        Err(err) => {
157            tracing::error!("✗ Two key authorization failed unexpectedly: {:?}", err);
158            return Err(anyhow::anyhow!(
159                "Two key authorization should have succeeded"
160            ));
161        }
162    }
163
164    Ok(())
165}