diff --git a/.env.example b/.env.example index 5317b07..e2af70c 100644 --- a/.env.example +++ b/.env.example @@ -5,7 +5,7 @@ # CORE CONFIGURATION (Required for all operations) # ============================================================================= -# KEYCLOAK / OIDC AUTHENTICATION +# KEYCLOAK / OIDC AUTHENTICATION (for Keycloak-based examples) # Your Keycloak/OIDC provider host URL KEYCLOAK_HOST=https://keycloak.dev.canton.example.com # The realm name in your Keycloak instance @@ -17,6 +17,16 @@ KEYCLOAK_USERNAME=your-username # Password for password grant authentication KEYCLOAK_PASSWORD=your-password +# AUTH0 AUTHENTICATION (for Auth0-based examples with *_auth0.rs suffix) +# Your Auth0 tenant domain (e.g., https://your-tenant.auth0.com) +AUTH0_DOMAIN=https://your-tenant.auth0.com +# Your Auth0 application's client ID +AUTH0_CLIENT_ID=your-auth0-client-id +# Your Auth0 application's client secret +AUTH0_CLIENT_SECRET=your-auth0-client-secret +# Your Auth0 API audience identifier +AUTH0_AUDIENCE=https://your-api-audience + # CANTON LEDGER # Your Canton participant node's ledger API host LEDGER_HOST=https://participant.example.com diff --git a/crates/cbtc/Cargo.toml b/crates/cbtc/Cargo.toml index 5655cd8..2fccc3e 100644 --- a/crates/cbtc/Cargo.toml +++ b/crates/cbtc/Cargo.toml @@ -19,5 +19,6 @@ futures = "0.3" dotenvy = { workspace = true } base64 = "0.22" log = "0.4" +reqwest = { version = "0.12.24", features = ["json"] } [dev-dependencies] diff --git a/crates/cbtc/src/auth0.rs b/crates/cbtc/src/auth0.rs new file mode 100644 index 0000000..f1ad6c1 --- /dev/null +++ b/crates/cbtc/src/auth0.rs @@ -0,0 +1,224 @@ +//! Auth0 Authentication Module +//! +//! This module provides Auth0 client credentials authentication for Canton network access. +//! It's a local implementation that works alongside the external `keycloak` crate, +//! allowing you to use Auth0 instead of Keycloak for authentication. +//! +//! # Usage +//! +//! ```rust,ignore +//! use cbtc::auth0::{client_credentials, ClientCredentialsParams, auth0_url}; +//! +//! let params = ClientCredentialsParams { +//! url: auth0_url("https://your-tenant.auth0.com"), +//! client_id: "your-client-id".to_string(), +//! client_secret: "your-client-secret".to_string(), +//! audience: "https://your-api-audience".to_string(), +//! }; +//! +//! let auth = client_credentials(params).await?; +//! // Use auth.access_token for API calls +//! ``` + +use base64::Engine; +use serde::Deserialize; + +/// Parameters for Auth0 client credentials authentication +pub struct ClientCredentialsParams { + /// The Auth0 token endpoint URL (use `auth0_url()` to construct) + pub url: String, + /// Your Auth0 application's client ID + pub client_id: String, + /// Your Auth0 application's client secret + pub client_secret: String, + /// The API audience identifier + pub audience: String, +} + +/// Authentication response containing the access token +#[derive(Deserialize, Debug, Clone)] +pub struct Response { + /// The JWT access token to use for API requests + pub access_token: String, + /// Token expiration time in seconds + #[serde(default)] + pub expires_in: u32, + /// Token type (usually "Bearer") + #[serde(default)] + pub token_type: String, +} + +impl Response { + /// Extract the user ID (subject claim) from the access token JWT + /// + /// Returns the 'sub' claim which is typically the Auth0 user/client identifier. + /// For machine-to-machine tokens, this is usually `client_id@clients`. + /// + /// # Errors + /// + /// Returns an error if the JWT is malformed or doesn't contain a 'sub' claim. + pub fn get_user_id(&self) -> Result { + // JWT format: header.payload.signature + let parts: Vec<&str> = self.access_token.split('.').collect(); + if parts.len() != 3 { + return Err("Invalid JWT format".to_string()); + } + + // Decode the payload (second part) + let payload = parts[1]; + + // URL-safe base64 without padding - we need to add padding for the decoder + let padding_needed = (4 - (payload.len() % 4)) % 4; + let padded = if padding_needed > 0 { + format!("{}{}", payload, "=".repeat(padding_needed)) + } else { + payload.to_string() + }; + + // Decode base64 - use URL_SAFE engine first, fall back to STANDARD + let decoded = base64::engine::general_purpose::URL_SAFE_NO_PAD + .decode(payload) + .or_else(|_| { + base64::engine::general_purpose::STANDARD.decode(&padded) + }) + .map_err(|e| format!("Failed to decode JWT payload: {}", e))?; + + // Parse JSON + let json: serde_json::Value = serde_json::from_slice(&decoded) + .map_err(|e| format!("Failed to parse JWT payload JSON: {}", e))?; + + // Extract 'sub' claim + json.get("sub") + .and_then(|v| v.as_str()) + .map(|s| s.to_string()) + .ok_or_else(|| "JWT does not contain 'sub' claim".to_string()) + } + + /// Extract an arbitrary claim from the access token JWT + /// + /// Useful for extracting custom claims like party_id, roles, etc. + pub fn get_claim(&self, claim_name: &str) -> Result { + let parts: Vec<&str> = self.access_token.split('.').collect(); + if parts.len() != 3 { + return Err("Invalid JWT format".to_string()); + } + + let payload = parts[1]; + let padding_needed = (4 - (payload.len() % 4)) % 4; + let padded = if padding_needed > 0 { + format!("{}{}", payload, "=".repeat(padding_needed)) + } else { + payload.to_string() + }; + + let decoded = base64::engine::general_purpose::URL_SAFE_NO_PAD + .decode(payload) + .or_else(|_| { + base64::engine::general_purpose::STANDARD.decode(&padded) + }) + .map_err(|e| format!("Failed to decode JWT payload: {}", e))?; + + let json: serde_json::Value = serde_json::from_slice(&decoded) + .map_err(|e| format!("Failed to parse JWT payload JSON: {}", e))?; + + json.get(claim_name) + .cloned() + .ok_or_else(|| format!("JWT does not contain '{}' claim", claim_name)) + } +} + +/// Perform Auth0 client credentials authentication +/// +/// This function exchanges client credentials for an access token using Auth0's +/// OAuth 2.0 client credentials flow. The returned token can be used to +/// authenticate with Canton APIs. +/// +/// # Arguments +/// +/// * `params` - Authentication parameters including URL, client_id, client_secret, and audience +/// +/// # Errors +/// +/// Returns an error if: +/// - The HTTP request fails +/// - Auth0 returns an error response +/// - The response cannot be parsed +/// +/// # Example +/// +/// ```rust,ignore +/// let auth = client_credentials(ClientCredentialsParams { +/// url: auth0_url("https://your-tenant.auth0.com"), +/// client_id: "abc123".to_string(), +/// client_secret: "secret".to_string(), +/// audience: "https://api.example.com".to_string(), +/// }).await?; +/// ``` +pub async fn client_credentials(params: ClientCredentialsParams) -> Result { + let client = reqwest::Client::new(); + + // Auth0 uses JSON body for client credentials + let json_body = serde_json::json!({ + "grant_type": "client_credentials", + "client_id": params.client_id, + "client_secret": params.client_secret, + "audience": params.audience, + }); + + let res = client + .post(¶ms.url) + .json(&json_body) + .send() + .await + .map_err(|e| format!("Auth0 client_credentials request failed: {}", e))?; + + let status = res.status(); + let body = res + .text() + .await + .map_err(|e| format!("Failed to read Auth0 response: {}", e))?; + + if !status.is_success() { + return Err(format!( + "Auth0 authentication failed [{}]: {}", + status, body + )); + } + + let response: Response = serde_json::from_str(&body) + .map_err(|e| format!("Failed to parse Auth0 response: {} - body: {}", e, body))?; + + Ok(response) +} + +/// Construct Auth0 OAuth token endpoint URL +/// +/// # Arguments +/// +/// * `domain` - Your Auth0 domain (e.g., "https://your-tenant.auth0.com") +/// +/// # Returns +/// +/// The full token endpoint URL (e.g., "https://your-tenant.auth0.com/oauth/token") +pub fn auth0_url(domain: &str) -> String { + let domain = domain.trim_end_matches('/'); + format!("{}/oauth/token", domain) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_auth0_url() { + assert_eq!( + auth0_url("https://example.auth0.com"), + "https://example.auth0.com/oauth/token" + ); + assert_eq!( + auth0_url("https://example.auth0.com/"), + "https://example.auth0.com/oauth/token" + ); + } +} + diff --git a/crates/cbtc/src/lib.rs b/crates/cbtc/src/lib.rs index 474fb74..174f413 100644 --- a/crates/cbtc/src/lib.rs +++ b/crates/cbtc/src/lib.rs @@ -1,5 +1,6 @@ pub mod accept; pub mod active_contracts; +pub mod auth0; pub mod batch; pub mod cancel_offers; pub mod consolidate; diff --git a/crates/examples/Cargo.toml b/crates/examples/Cargo.toml index 76b06a4..0fdb788 100644 --- a/crates/examples/Cargo.toml +++ b/crates/examples/Cargo.toml @@ -61,6 +61,42 @@ path = "src/cancel_offers.rs" name = "check_withdraw_requests" path = "src/check_withdraw_requests.rs" +[[bin]] +name = "accept_transfers_auth0" +path = "src/accept_transfers_auth0.rs" + +[[bin]] +name = "check_balance_auth0" +path = "src/check_balance_auth0.rs" + +[[bin]] +name = "check_withdraw_requests_auth0" +path = "src/check_withdraw_requests_auth0.rs" + +[[bin]] +name = "consolidate_utxos_auth0" +path = "src/consolidate_utxos_auth0.rs" + +[[bin]] +name = "list_deposit_addresses_auth0" +path = "src/list_deposit_addresses_auth0.rs" + +[[bin]] +name = "mint_cbtc_auth0" +path = "src/mint_cbtc_auth0.rs" + +[[bin]] +name = "redeem_cbtc_auth0" +path = "src/redeem_cbtc_auth0.rs" + +[[bin]] +name = "send_cbtc_auth0" +path = "src/send_cbtc_auth0.rs" + +[[bin]] +name = "stream_auth0" +path = "src/stream_auth0.rs" + [dependencies] cbtc = { path = "../cbtc" } common = { path = "../common" } diff --git a/crates/examples/src/accept_transfers_auth0.rs b/crates/examples/src/accept_transfers_auth0.rs new file mode 100644 index 0000000..efb163b --- /dev/null +++ b/crates/examples/src/accept_transfers_auth0.rs @@ -0,0 +1,114 @@ +/// Example: Accept all pending CBTC transfers (Auth0 Version) +/// +/// Run with: cargo run --example accept_transfers_auth0 +/// +/// This example fetches and accepts all pending CBTC TransferInstruction contracts +/// for your party using Auth0 authentication. +/// +/// Make sure to set up your .env file with AUTH0 credentials. +use cbtc::auth0::{auth0_url, client_credentials, ClientCredentialsParams}; +use std::env; + +#[tokio::main] +async fn main() -> Result<(), String> { + // Load environment variables + dotenvy::dotenv().ok(); + env_logger::init(); + + println!("=== Accept All Pending CBTC Transfers (Auth0) ===\n"); + + // Authenticate with Auth0 + println!("Authenticating with Auth0..."); + let auth0_domain = env::var("AUTH0_DOMAIN").map_err(|_| "AUTH0_DOMAIN must be set")?; + let auth0_client_id = env::var("AUTH0_CLIENT_ID").map_err(|_| "AUTH0_CLIENT_ID must be set")?; + let auth0_client_secret = + env::var("AUTH0_CLIENT_SECRET").map_err(|_| "AUTH0_CLIENT_SECRET must be set")?; + let auth0_audience = env::var("AUTH0_AUDIENCE").map_err(|_| "AUTH0_AUDIENCE must be set")?; + + let auth_params = ClientCredentialsParams { + url: auth0_url(&auth0_domain), + client_id: auth0_client_id, + client_secret: auth0_client_secret, + audience: auth0_audience, + }; + + let auth = client_credentials(auth_params) + .await + .map_err(|e| format!("Auth0 authentication failed: {}", e))?; + + println!("✓ Authenticated successfully!\n"); + + let receiver_party = env::var("PARTY_ID").expect("PARTY_ID must be set"); + let ledger_host = env::var("LEDGER_HOST").expect("LEDGER_HOST must be set"); + let registry_url = env::var("REGISTRY_URL").expect("REGISTRY_URL must be set"); + let decentralized_party_id = + env::var("DECENTRALIZED_PARTY_ID").expect("DECENTRALIZED_PARTY_ID must be set"); + + // Fetch pending transfers + println!("Fetching pending incoming transfers..."); + let pending_transfers = cbtc::utils::fetch_incoming_transfers( + ledger_host.clone(), + receiver_party.clone(), + auth.access_token.clone(), + ) + .await?; + + if pending_transfers.is_empty() { + println!("No pending transfers found."); + return Ok(()); + } + + println!("Found {} pending transfer(s)\n", pending_transfers.len()); + + // Accept each transfer + let mut successful = 0; + let mut failed = 0; + + for (idx, transfer) in pending_transfers.iter().enumerate() { + let contract_id = &transfer.created_event.contract_id; + println!( + "[{}/{}] Accepting transfer: {}...", + idx + 1, + pending_transfers.len(), + if contract_id.len() > 16 { + &contract_id[..16] + } else { + contract_id + } + ); + + // Accept the transfer + match cbtc::accept::submit(cbtc::accept::Params { + transfer_offer_contract_id: contract_id.clone(), + receiver_party: receiver_party.clone(), + ledger_host: ledger_host.clone(), + access_token: auth.access_token.clone(), + registry_url: registry_url.clone(), + decentralized_party_id: decentralized_party_id.clone(), + }) + .await + { + Ok(_) => { + println!(" ✓ Accepted successfully"); + successful += 1; + } + Err(e) => { + println!(" ✗ Failed: {}", e); + failed += 1; + } + } + } + + println!("\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"); + println!("Acceptance Complete!"); + println!("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"); + println!("Successful: {}", successful); + println!("Failed: {}", failed); + + if failed > 0 { + return Err(format!("Completed with {} failures", failed)); + } + + Ok(()) +} + diff --git a/crates/examples/src/check_balance_auth0.rs b/crates/examples/src/check_balance_auth0.rs new file mode 100644 index 0000000..45437e2 --- /dev/null +++ b/crates/examples/src/check_balance_auth0.rs @@ -0,0 +1,100 @@ +/// Example: Check CBTC balance and UTXO count (Auth0 Version) +/// +/// This example demonstrates how to: +/// 1. Authenticate with Auth0 (client credentials flow) +/// 2. Query active CBTC holdings (UTXOs) for a party +/// 3. Calculate total balance across all UTXOs +/// 4. Monitor UTXO count and warn about consolidation needs +/// +/// Run with: cargo run --example check_balance_auth0 +/// +/// Required environment variables: +/// - AUTH0_DOMAIN, AUTH0_CLIENT_ID, AUTH0_CLIENT_SECRET, AUTH0_AUDIENCE +/// - LEDGER_HOST, PARTY_ID +use cbtc::auth0::{auth0_url, client_credentials, ClientCredentialsParams}; +use std::env; + +#[tokio::main] +async fn main() -> Result<(), String> { + // Load environment variables + dotenvy::dotenv().ok(); + env_logger::init(); + + // Authenticate with Auth0 + println!("Authenticating with Auth0..."); + let auth0_domain = env::var("AUTH0_DOMAIN").map_err(|_| "AUTH0_DOMAIN must be set")?; + let auth0_client_id = env::var("AUTH0_CLIENT_ID").map_err(|_| "AUTH0_CLIENT_ID must be set")?; + let auth0_client_secret = + env::var("AUTH0_CLIENT_SECRET").map_err(|_| "AUTH0_CLIENT_SECRET must be set")?; + let auth0_audience = env::var("AUTH0_AUDIENCE").map_err(|_| "AUTH0_AUDIENCE must be set")?; + + let auth_params = ClientCredentialsParams { + url: auth0_url(&auth0_domain), + client_id: auth0_client_id, + client_secret: auth0_client_secret, + audience: auth0_audience, + }; + + let auth = client_credentials(auth_params) + .await + .map_err(|e| format!("Auth0 authentication failed: {}", e))?; + + println!("✓ Authenticated successfully!\n"); + + let party = env::var("PARTY_ID").expect("PARTY_ID must be set"); + let ledger_host = env::var("LEDGER_HOST").expect("LEDGER_HOST must be set"); + + println!("\n📊 Checking balance for party: {}", party); + println!("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n"); + + // Get active contracts + let balance_params = cbtc::active_contracts::Params { + ledger_host, + party, + access_token: auth.access_token, + }; + + let holdings = cbtc::active_contracts::get(balance_params).await?; + + // Calculate total balance + let total_balance: f64 = holdings.iter().filter_map(cbtc::utils::extract_amount).sum(); + + // Display results + println!("Total CBTC Balance: {:.8}", total_balance); + println!("Number of UTXOs: {}", holdings.len()); + println!(); + + if holdings.len() >= 10 { + println!("⚠️ Warning: You have {} UTXOs", holdings.len()); + println!(" Canton has a soft limit of 10 UTXOs per party per token."); + println!(" Consider consolidating your holdings."); + } else if holdings.len() >= 7 { + println!("ℹ️ You have {} UTXOs", holdings.len()); + println!(" Consider consolidating soon to stay under the 10 UTXO limit."); + } else { + println!("✅ UTXO count is healthy ({}/10)", holdings.len()); + } + + // Show individual holdings + if !holdings.is_empty() { + println!("\nIndividual Holdings:"); + println!("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"); + for (i, holding) in holdings.iter().enumerate() { + let amount = cbtc::utils::extract_amount(holding).unwrap_or(0.0); + let contract_id = &holding.created_event.contract_id; + let short_id = if contract_id.len() > 12 { + format!( + "{}...{}", + &contract_id[..6], + &contract_id[contract_id.len() - 6..] + ) + } else { + contract_id.clone() + }; + println!(" {}. {:.8} CBTC ({})", i + 1, amount, short_id); + } + } + + Ok(()) +} + diff --git a/crates/examples/src/check_withdraw_requests_auth0.rs b/crates/examples/src/check_withdraw_requests_auth0.rs new file mode 100644 index 0000000..7d1a59d --- /dev/null +++ b/crates/examples/src/check_withdraw_requests_auth0.rs @@ -0,0 +1,127 @@ +/// Check Withdraw Requests Example (Auth0 Version) +/// +/// This example continuously polls for WithdrawRequests that have been created +/// by the attestor network after a user submitted a withdrawal. +/// +/// Flow: +/// 1. User calls submit_withdraw() to burn CBTC (increases pending_balance) +/// 2. Attestor network processes the pending balance and creates a WithdrawRequest +/// 3. This script polls every 5 seconds to see processed withdrawals +/// +/// The WithdrawRequest includes the btc_tx_id which is the Bitcoin transaction +/// that was used to fulfill the withdrawal. +/// +/// To run this example: +/// 1. Make sure you have .env configured with AUTH0 credentials +/// 2. Submit a withdrawal first using redeem_cbtc_auth0 +/// 3. cargo run --example check_withdraw_requests_auth0 +/// 4. Press Ctrl+C to stop +use cbtc::auth0::{auth0_url, client_credentials, ClientCredentialsParams}; +use cbtc::mint_redeem::redeem::{ListWithdrawAccountsParams, ListWithdrawRequestsParams}; +use std::env; +use std::time::Duration; +use tokio::time::sleep; + +#[tokio::main] +async fn main() -> Result<(), String> { + // Load environment variables + dotenvy::dotenv().ok(); + env_logger::init(); + + println!("=== Check Withdraw Requests (Polling Mode) - Auth0 ==="); + println!("Press Ctrl+C to stop\n"); + + // Authenticate with Auth0 + println!("Authenticating with Auth0..."); + let auth0_domain = env::var("AUTH0_DOMAIN").map_err(|_| "AUTH0_DOMAIN must be set")?; + let auth0_client_id = env::var("AUTH0_CLIENT_ID").map_err(|_| "AUTH0_CLIENT_ID must be set")?; + let auth0_client_secret = + env::var("AUTH0_CLIENT_SECRET").map_err(|_| "AUTH0_CLIENT_SECRET must be set")?; + let auth0_audience = env::var("AUTH0_AUDIENCE").map_err(|_| "AUTH0_AUDIENCE must be set")?; + + let auth_params = ClientCredentialsParams { + url: auth0_url(&auth0_domain), + client_id: auth0_client_id, + client_secret: auth0_client_secret, + audience: auth0_audience, + }; + + let login_response = client_credentials(auth_params) + .await + .map_err(|e| format!("Auth0 authentication failed: {}", e))?; + + println!("✓ Authenticated successfully\n"); + + // Common parameters + let ledger_host = env::var("LEDGER_HOST").expect("LEDGER_HOST must be set"); + let party_id = env::var("PARTY_ID").expect("PARTY_ID must be set"); + let access_token = login_response.access_token.clone(); + + let mut poll_count = 0u64; + + loop { + poll_count += 1; + let timestamp = chrono::Local::now().format("%H:%M:%S"); + println!("─────────────────────────────────────────────────────"); + println!("[{}] Poll #{}", timestamp, poll_count); + println!("─────────────────────────────────────────────────────"); + + // Check withdraw accounts for pending balances + match cbtc::mint_redeem::redeem::list_withdraw_accounts(ListWithdrawAccountsParams { + ledger_host: ledger_host.clone(), + party: party_id.clone(), + access_token: access_token.clone(), + }) + .await + { + Ok(accounts) => { + if accounts.is_empty() { + println!("No withdraw accounts found."); + } else { + println!("Withdraw Accounts ({}):", accounts.len()); + for account in &accounts { + let pending: f64 = account.pending_balance.parse().unwrap_or(0.0); + let status = if pending > 0.0 { "PENDING" } else { "ready" }; + println!( + " [{:>7}] {} BTC -> {}", + status, account.pending_balance, &account.destination_btc_address + ); + } + } + } + Err(e) => { + println!("Error fetching accounts: {}", e); + } + } + + // Check for withdraw requests + match cbtc::mint_redeem::redeem::list_withdraw_requests(ListWithdrawRequestsParams { + ledger_host: ledger_host.clone(), + party: party_id.clone(), + access_token: access_token.clone(), + }) + .await + { + Ok(requests) => { + if requests.is_empty() { + println!("No withdraw requests yet."); + } else { + println!("\nWithdraw Requests ({}):", requests.len()); + for request in &requests { + println!( + " {} BTC -> {} (tx: {})", + request.amount, &request.destination_btc_address, &request.btc_tx_id + ); + } + } + } + Err(e) => { + println!("Error fetching requests: {}", e); + } + } + + println!("\nNext poll in 5 seconds...\n"); + sleep(Duration::from_secs(5)).await; + } +} + diff --git a/crates/examples/src/consolidate_utxos_auth0.rs b/crates/examples/src/consolidate_utxos_auth0.rs new file mode 100644 index 0000000..4a540ca --- /dev/null +++ b/crates/examples/src/consolidate_utxos_auth0.rs @@ -0,0 +1,83 @@ +/// Example: Check and consolidate UTXOs if needed (Auth0 Version) +/// +/// Run with: cargo run --example consolidate_utxos_auth0 +/// +/// Make sure to set up your .env file with the required configuration. +use cbtc::auth0::{auth0_url, client_credentials, ClientCredentialsParams}; +use std::env; + +#[tokio::main] +async fn main() -> Result<(), String> { + // Load environment variables + dotenvy::dotenv().ok(); + env_logger::init(); + + // Authenticate with Auth0 + println!("Authenticating with Auth0..."); + let auth0_domain = env::var("AUTH0_DOMAIN").map_err(|_| "AUTH0_DOMAIN must be set")?; + let auth0_client_id = env::var("AUTH0_CLIENT_ID").map_err(|_| "AUTH0_CLIENT_ID must be set")?; + let auth0_client_secret = + env::var("AUTH0_CLIENT_SECRET").map_err(|_| "AUTH0_CLIENT_SECRET must be set")?; + let auth0_audience = env::var("AUTH0_AUDIENCE").map_err(|_| "AUTH0_AUDIENCE must be set")?; + + let auth_params = ClientCredentialsParams { + url: auth0_url(&auth0_domain), + client_id: auth0_client_id, + client_secret: auth0_client_secret, + audience: auth0_audience, + }; + + let auth = client_credentials(auth_params) + .await + .map_err(|e| format!("Auth0 authentication failed: {}", e))?; + + println!("✓ Authenticated successfully!\n"); + + let party = env::var("PARTY_ID").expect("PARTY_ID must be set"); + + // You can customize the threshold (default is 10) + let threshold: usize = env::var("CONSOLIDATION_THRESHOLD") + .unwrap_or_else(|_| "10".to_string()) + .parse() + .expect("CONSOLIDATION_THRESHOLD must be a valid number"); + + println!("\n🔄 Checking UTXO consolidation for party:"); + println!(" Party: {}", party); + println!(" Threshold: {} UTXOs\n", threshold); + + let consolidate_params = cbtc::consolidate::CheckConsolidateParams { + party, + threshold, + ledger_host: env::var("LEDGER_HOST").expect("LEDGER_HOST must be set"), + access_token: auth.access_token, + registry_url: env::var("REGISTRY_URL").expect("REGISTRY_URL must be set"), + decentralized_party_id: env::var("DECENTRALIZED_PARTY_ID") + .expect("DECENTRALIZED_PARTY_ID must be set"), + }; + + let result = cbtc::consolidate::check_and_consolidate(consolidate_params).await?; + + println!(); + if result.consolidated { + println!("✅ Consolidation complete!"); + println!(" Before: {} UTXOs", result.utxos_before); + println!(" After: {} UTXO(s)", result.utxos_after); + println!(); + println!(" Resulting holding CIDs:"); + for cid in &result.holding_cids { + let short_id = if cid.len() > 16 { + format!("{}...{}", &cid[..8], &cid[cid.len() - 8..]) + } else { + cid.clone() + }; + println!(" - {}", short_id); + } + } else { + println!("✅ No consolidation needed"); + println!(" Current UTXO count: {}", result.utxos_before); + println!(" Threshold: {}", threshold); + } + + Ok(()) +} + diff --git a/crates/examples/src/list_deposit_addresses_auth0.rs b/crates/examples/src/list_deposit_addresses_auth0.rs new file mode 100644 index 0000000..11fc72b --- /dev/null +++ b/crates/examples/src/list_deposit_addresses_auth0.rs @@ -0,0 +1,104 @@ +/// Example: List Deposit Accounts and Bitcoin Addresses (Auth0 Version) +/// +/// This example demonstrates how to: +/// 1. Authenticate with Auth0 (client credentials flow) +/// 2. List all deposit accounts for your party +/// 3. Fetch the Bitcoin address for each account from the attestor +/// +/// Run with: cargo run --example list_deposit_addresses_auth0 +/// +/// Required environment variables: +/// - AUTH0_DOMAIN, AUTH0_CLIENT_ID, AUTH0_CLIENT_SECRET, AUTH0_AUDIENCE +/// - LEDGER_HOST, PARTY_ID +/// - ATTESTOR_URL, CANTON_NETWORK +/// +/// Note on account IDs: +/// The attestor uses the account's `id` field (a UUID in the createArgument) to +/// look up Bitcoin addresses. For older accounts where this field is null, the +/// `contract_id` is used instead. The `account_id()` method handles this automatically. +use cbtc::auth0::{auth0_url, client_credentials, ClientCredentialsParams}; +use cbtc::mint_redeem::mint::{GetBitcoinAddressParams, ListDepositAccountsParams}; +use std::env; + +#[tokio::main] +async fn main() -> Result<(), String> { + // Load environment variables + dotenvy::dotenv().ok(); + env_logger::init(); + + // Authenticate with Auth0 + println!("Authenticating with Auth0..."); + let auth0_domain = env::var("AUTH0_DOMAIN").map_err(|_| "AUTH0_DOMAIN must be set")?; + let auth0_client_id = env::var("AUTH0_CLIENT_ID").map_err(|_| "AUTH0_CLIENT_ID must be set")?; + let auth0_client_secret = + env::var("AUTH0_CLIENT_SECRET").map_err(|_| "AUTH0_CLIENT_SECRET must be set")?; + let auth0_audience = env::var("AUTH0_AUDIENCE").map_err(|_| "AUTH0_AUDIENCE must be set")?; + + let auth_params = ClientCredentialsParams { + url: auth0_url(&auth0_domain), + client_id: auth0_client_id, + client_secret: auth0_client_secret, + audience: auth0_audience, + }; + + let auth = client_credentials(auth_params) + .await + .map_err(|e| format!("Auth0 authentication failed: {}", e))?; + + println!("✓ Authenticated successfully!\n"); + + let party = env::var("PARTY_ID").expect("PARTY_ID must be set"); + let ledger_host = env::var("LEDGER_HOST").expect("LEDGER_HOST must be set"); + let attestor_url = env::var("ATTESTOR_URL").expect("ATTESTOR_URL must be set"); + let chain = env::var("CANTON_NETWORK").expect("CANTON_NETWORK must be set"); + + println!("Listing deposit accounts for party: {}", party); + println!("{}\n", "=".repeat(60)); + + // List all deposit accounts + let accounts = cbtc::mint_redeem::mint::list_deposit_accounts(ListDepositAccountsParams { + ledger_host, + party, + access_token: auth.access_token, + }) + .await?; + + if accounts.is_empty() { + println!("No deposit accounts found."); + return Ok(()); + } + + println!("Found {} deposit account(s)\n", accounts.len()); + + // Fetch Bitcoin address for each account + for (i, account) in accounts.iter().enumerate() { + println!("Account #{}", i + 1); + println!(" Contract ID: {}", account.contract_id); + if let Some(ref id) = account.id { + println!(" Account ID: {}", id); + } else { + println!(" Account ID: (none - using contract_id for lookups)"); + } + println!(" Owner: {}", account.owner); + + // Fetch the Bitcoin address using account_id() which handles the id/contract_id fallback + match cbtc::mint_redeem::mint::get_bitcoin_address(GetBitcoinAddressParams { + attestor_url: attestor_url.clone(), + account_id: account.account_id().to_string(), + chain: chain.clone(), + }) + .await + { + Ok(bitcoin_address) => { + println!(" BTC Address: {}", bitcoin_address); + } + Err(e) => { + println!(" BTC Address: (error: {})", e); + } + } + println!(); + } + + Ok(()) +} + diff --git a/crates/examples/src/mint_cbtc_auth0.rs b/crates/examples/src/mint_cbtc_auth0.rs new file mode 100644 index 0000000..24c3c9e --- /dev/null +++ b/crates/examples/src/mint_cbtc_auth0.rs @@ -0,0 +1,169 @@ +/// CBTC Minting Flow Example (Auth0 Version) +/// +/// This example demonstrates the complete flow of minting CBTC from BTC using Auth0: +/// +/// 1. Authenticate with Auth0 (client_credentials flow) +/// 2. Get account rules from the attestor network +/// 3. Create a deposit account on Canton +/// 4. Get the Bitcoin address for the account +/// 5. (User sends BTC to that address - external step) +/// 6. Monitor for deposit requests +/// 7. Check account status +/// +/// To run this example: +/// 1. Make sure .env has AUTH0_DOMAIN, AUTH0_CLIENT_ID, AUTH0_CLIENT_SECRET, AUTH0_AUDIENCE +/// 2. cargo run --example mint_cbtc_auth0 +use cbtc::auth0::{auth0_url, client_credentials, ClientCredentialsParams}; +use cbtc::mint_redeem::attestor; +use cbtc::mint_redeem::mint::{ + CreateDepositAccountParams, GetBitcoinAddressParams, GetDepositAccountStatusParams, + ListDepositAccountsParams, +}; +use std::env; + +#[tokio::main] +async fn main() -> Result<(), String> { + // Load environment variables + dotenvy::dotenv().ok(); + env_logger::init(); + + println!("=== CBTC Minting Flow Example (Auth0) ===\n"); + + // Step 1: Authenticate with Auth0 + println!("Step 1: Authenticating with Auth0..."); + let auth0_domain = env::var("AUTH0_DOMAIN").map_err(|_| "AUTH0_DOMAIN must be set")?; + let auth0_client_id = env::var("AUTH0_CLIENT_ID").map_err(|_| "AUTH0_CLIENT_ID must be set")?; + let auth0_client_secret = + env::var("AUTH0_CLIENT_SECRET").map_err(|_| "AUTH0_CLIENT_SECRET must be set")?; + let auth0_audience = env::var("AUTH0_AUDIENCE").map_err(|_| "AUTH0_AUDIENCE must be set")?; + + let auth_params = ClientCredentialsParams { + url: auth0_url(&auth0_domain), + client_id: auth0_client_id, + client_secret: auth0_client_secret, + audience: auth0_audience, + }; + + let login_response = client_credentials(auth_params) + .await + .map_err(|e| format!("Auth0 authentication failed: {}", e))?; + + println!("✓ Authenticated successfully!"); + println!(" Token expires in: {} seconds", login_response.expires_in); + + // Extract user identifier from token for account creation + let user_name = login_response + .get_user_id() + .unwrap_or_else(|_| "auth0-user".to_string()); + println!(" User ID: {}\n", user_name); + + // Common parameters + let ledger_host = env::var("LEDGER_HOST").expect("LEDGER_HOST must be set"); + let party_id = env::var("PARTY_ID").expect("PARTY_ID must be set"); + let access_token = login_response.access_token.clone(); + let attestor_url = env::var("ATTESTOR_URL").expect("ATTESTOR_URL must be set"); + let chain = env::var("CANTON_NETWORK").expect("CANTON_NETWORK must be set"); + + // Step 2: List existing deposit accounts + println!("Step 2: Listing existing deposit accounts..."); + let accounts = cbtc::mint_redeem::mint::list_deposit_accounts(ListDepositAccountsParams { + ledger_host: ledger_host.clone(), + party: party_id.clone(), + access_token: access_token.clone(), + }) + .await?; + + println!("✓ Found {} existing deposit account(s)", accounts.len()); + for account in &accounts { + println!(" - Contract ID: {}", account.contract_id); + println!(" Owner: {}", account.owner); + } + println!(); + + // Step 3: Get account rules from attestor + println!("Step 3: Getting account contract rules from attestor..."); + let account_rules = attestor::get_account_contract_rules(&attestor_url, &chain).await?; + println!("✓ Retrieved account rules:"); + println!( + " - DepositAccountRules CID: {}", + account_rules.da_rules.contract_id + ); + println!( + " - WithdrawAccountRules CID: {}", + account_rules.wa_rules.contract_id + ); + println!(); + + // Step 4: Create a new deposit account + println!("Step 4: Creating a new deposit account..."); + let deposit_account = + cbtc::mint_redeem::mint::create_deposit_account(CreateDepositAccountParams { + ledger_host: ledger_host.clone(), + party: party_id.clone(), + user_name: user_name.clone(), + access_token: access_token.clone(), + account_rules: account_rules.clone(), + }) + .await?; + + println!("✓ Deposit account created successfully!"); + println!(" - Contract ID: {}", deposit_account.contract_id); + println!(" - Owner: {}", deposit_account.owner); + println!(); + + // Step 5: Get the Bitcoin address for this account + println!("Step 5: Getting Bitcoin address for the deposit account..."); + let bitcoin_address = + cbtc::mint_redeem::mint::get_bitcoin_address(GetBitcoinAddressParams { + attestor_url: attestor_url.clone(), + account_id: deposit_account.account_id().to_string(), + chain: chain.clone(), + }) + .await?; + + println!("✓ Bitcoin address retrieved:"); + println!(" {}", bitcoin_address); + println!(); + println!("📝 To mint CBTC, send BTC to this address."); + println!(" Once confirmed, CBTC will be automatically minted to your Canton party."); + println!(); + + // Step 6: Get full account status + println!("Step 6: Getting full account status..."); + let status = + cbtc::mint_redeem::mint::get_deposit_account_status(GetDepositAccountStatusParams { + ledger_host: ledger_host.clone(), + party: party_id.clone(), + access_token: access_token.clone(), + attestor_url: attestor_url.clone(), + chain: chain.clone(), + account_contract_id: deposit_account.contract_id.clone(), + }) + .await?; + + println!("✓ Account status:"); + println!(" - Bitcoin Address: {}", status.bitcoin_address); + println!(" - Owner: {}", status.owner); + println!( + " - Last Processed BTC Block: {}", + status.last_processed_bitcoin_block + ); + println!(); + + println!("=== Example Complete ==="); + println!(); + println!("Summary:"); + println!( + " • Your deposit account contract ID: {}", + deposit_account.contract_id + ); + println!(" • Send BTC to: {}", bitcoin_address); + println!(" • The attestor network will monitor this address"); + println!(" • Once BTC is confirmed, CBTC will be minted to your party"); + println!(); + println!("To monitor for deposits, you can periodically call:"); + println!(" - get_deposit_account_status() to check account status"); + + Ok(()) +} + diff --git a/crates/examples/src/redeem_cbtc_auth0.rs b/crates/examples/src/redeem_cbtc_auth0.rs new file mode 100644 index 0000000..ca768ae --- /dev/null +++ b/crates/examples/src/redeem_cbtc_auth0.rs @@ -0,0 +1,272 @@ +/// CBTC Redeeming (Withdrawal) Flow Example (Auth0 Version) +/// +/// This example demonstrates the complete flow of submitting a CBTC withdrawal using Auth0: +/// +/// 1. Authenticate with Auth0 (client_credentials flow) +/// 2. Get account rules from the attestor network +/// 3. Create a withdraw account on Canton with destination BTC address +/// 4. List existing CBTC holdings +/// 5. Submit withdrawal (burn CBTC and increase pending balance) +/// 6. Verify the withdrawal was submitted successfully +/// +/// Note: WithdrawRequests are NOT created atomically with the withdrawal submission. +/// The attestor network will create WithdrawRequests later. Use the separate +/// `check_withdraw_requests` example to monitor for processed withdrawals. +/// +/// To run this example: +/// 1. Make sure .env has AUTH0_DOMAIN, AUTH0_CLIENT_ID, AUTH0_CLIENT_SECRET, AUTH0_AUDIENCE +/// 2. Make sure you have CBTC holdings (run mint_cbtc_auth0 first) +/// 3. cargo run --example redeem_cbtc_auth0 +use cbtc::auth0::{auth0_url, client_credentials, ClientCredentialsParams}; +use cbtc::mint_redeem::attestor; +use cbtc::mint_redeem::redeem::{ + CreateWithdrawAccountParams, ListHoldingsParams, ListWithdrawAccountsParams, + SubmitWithdrawParams, +}; +use std::env; + +#[tokio::main] +async fn main() -> Result<(), String> { + // Load environment variables + dotenvy::dotenv().ok(); + env_logger::init(); + + println!("=== CBTC Redeeming (Withdrawal) Flow Example (Auth0) ===\n"); + + // Step 1: Authenticate with Auth0 + println!("Step 1: Authenticating with Auth0..."); + let auth0_domain = env::var("AUTH0_DOMAIN").map_err(|_| "AUTH0_DOMAIN must be set")?; + let auth0_client_id = env::var("AUTH0_CLIENT_ID").map_err(|_| "AUTH0_CLIENT_ID must be set")?; + let auth0_client_secret = + env::var("AUTH0_CLIENT_SECRET").map_err(|_| "AUTH0_CLIENT_SECRET must be set")?; + let auth0_audience = env::var("AUTH0_AUDIENCE").map_err(|_| "AUTH0_AUDIENCE must be set")?; + + let auth_params = ClientCredentialsParams { + url: auth0_url(&auth0_domain), + client_id: auth0_client_id, + client_secret: auth0_client_secret, + audience: auth0_audience, + }; + + let login_response = client_credentials(auth_params) + .await + .map_err(|e| format!("Auth0 authentication failed: {}", e))?; + + println!("✓ Authenticated successfully!"); + println!(" Token expires in: {} seconds", login_response.expires_in); + + // Extract user identifier from token for account creation + let user_name = login_response + .get_user_id() + .unwrap_or_else(|_| "auth0-user".to_string()); + println!(" User ID: {}\n", user_name); + + // Common parameters + let ledger_host = env::var("LEDGER_HOST").expect("LEDGER_HOST must be set"); + let party_id = env::var("PARTY_ID").expect("PARTY_ID must be set"); + let access_token = login_response.access_token.clone(); + let attestor_url = env::var("ATTESTOR_URL").expect("ATTESTOR_URL must be set"); + let chain = env::var("CANTON_NETWORK").expect("CANTON_NETWORK must be set"); + + // Step 2: List existing withdraw accounts + println!("Step 2: Listing existing withdraw accounts..."); + let accounts = + cbtc::mint_redeem::redeem::list_withdraw_accounts(ListWithdrawAccountsParams { + ledger_host: ledger_host.clone(), + party: party_id.clone(), + access_token: access_token.clone(), + }) + .await?; + + println!("✓ Found {} existing withdraw account(s)", accounts.len()); + for account in &accounts { + println!(" - Contract ID: {}", account.contract_id); + println!(" Owner: {}", account.owner); + println!( + " Destination BTC Address: {}", + account.destination_btc_address + ); + } + println!(); + + // Step 3: Check CBTC holdings + println!("Step 3: Checking CBTC holdings..."); + let holdings = cbtc::mint_redeem::redeem::list_holdings(ListHoldingsParams { + ledger_host: ledger_host.clone(), + party: party_id.clone(), + access_token: access_token.clone(), + }) + .await?; + + let cbtc_holdings: Vec<_> = holdings + .iter() + .filter(|h| h.instrument_id == "CBTC") + .collect(); + + let total_cbtc: f64 = cbtc_holdings + .iter() + .map(|h| h.amount.parse::().unwrap_or(0.0)) + .sum(); + + println!("✓ Found {} CBTC holding(s)", cbtc_holdings.len()); + println!(" Total CBTC balance: {} BTC", total_cbtc); + for holding in &cbtc_holdings { + println!( + " - {} BTC (CID: {})", + holding.amount, holding.contract_id + ); + } + println!(); + + if cbtc_holdings.is_empty() { + println!("⚠ You don't have any CBTC holdings to redeem."); + println!(" Run 'cargo run --example mint_cbtc_auth0' first to mint some CBTC."); + return Ok(()); + } + + // Step 4: Get account rules from attestor + println!("Step 4: Getting account contract rules from attestor..."); + let account_rules = attestor::get_account_contract_rules(&attestor_url, &chain).await?; + println!("✓ Retrieved account rules:"); + println!( + " - WithdrawAccountRules CID: {}", + account_rules.wa_rules.contract_id + ); + println!(); + + // Step 5: Create a new withdraw account (or skip if one already exists) + if !accounts.is_empty() { + println!("Step 5: Withdraw account already exists, skipping creation..."); + println!(" Using existing account: {}", accounts[0].contract_id); + println!(" Destination: {}\n", accounts[0].destination_btc_address); + } else { + let destination_btc_address = env::var("DESTINATION_BTC_ADDRESS") + .unwrap_or_else(|_| "bcrt1qexamplewithdrawaddressfortestingonly00000000".to_string()); + + println!("Step 5: Creating a new withdraw account..."); + println!(" Destination BTC address: {}", destination_btc_address); + + let withdraw_account = + cbtc::mint_redeem::redeem::create_withdraw_account(CreateWithdrawAccountParams { + ledger_host: ledger_host.clone(), + party: party_id.clone(), + user_name: user_name.clone(), + access_token: access_token.clone(), + account_rules_contract_id: account_rules.wa_rules.contract_id.clone(), + account_rules_template_id: account_rules.wa_rules.template_id.clone(), + account_rules_created_event_blob: account_rules.wa_rules.created_event_blob.clone(), + destination_btc_address: destination_btc_address.clone(), + }) + .await?; + + println!("✓ Withdraw account created successfully!"); + println!(" - Contract ID: {}", withdraw_account.contract_id); + println!(" - Owner: {}", withdraw_account.owner); + println!( + " - Destination BTC Address: {}", + withdraw_account.destination_btc_address + ); + println!(); + } + + // Use the first account (either existing or newly created) + let withdraw_account = if accounts.is_empty() { + // Fetch the newly created account + let updated_accounts = + cbtc::mint_redeem::redeem::list_withdraw_accounts(ListWithdrawAccountsParams { + ledger_host: ledger_host.clone(), + party: party_id.clone(), + access_token: access_token.clone(), + }) + .await?; + updated_accounts + .into_iter() + .next() + .ok_or("Failed to find newly created withdraw account")? + } else { + accounts[0].clone() + }; + + // Step 6: Submit withdrawal (burn CBTC) + let withdraw_amount = "0.00003218"; + let withdraw_amount_f64: f64 = withdraw_amount.parse().unwrap(); + + if total_cbtc < withdraw_amount_f64 { + println!( + "⚠ Insufficient CBTC balance. You have {} but trying to withdraw {}", + total_cbtc, withdraw_amount + ); + return Ok(()); + } + + println!("Step 6: Submitting withdrawal (burning CBTC)..."); + println!(" Amount to withdraw: {} BTC", withdraw_amount); + + // Select holdings to burn + let mut selected_holdings = Vec::new(); + let mut selected_total = 0.0; + + for holding in &cbtc_holdings { + let amount = holding.amount.parse::().unwrap_or(0.0); + selected_holdings.push(holding.contract_id.clone()); + selected_total += amount; + + if selected_total >= withdraw_amount_f64 { + break; + } + } + + println!( + " Using {} holding(s) totaling {} BTC", + selected_holdings.len(), + selected_total + ); + + let updated_account = cbtc::mint_redeem::redeem::submit_withdraw(SubmitWithdrawParams { + ledger_host: ledger_host.clone(), + party: party_id.clone(), + user_name: user_name.clone(), + access_token: access_token.clone(), + attestor_url: attestor_url.clone(), + chain: chain.clone(), + withdraw_account_contract_id: withdraw_account.contract_id.clone(), + withdraw_account_template_id: withdraw_account.template_id.clone(), + withdraw_account_created_event_blob: withdraw_account.created_event_blob.clone(), + amount: withdraw_amount.to_string(), + holding_contract_ids: selected_holdings, + }) + .await?; + + println!("✓ Withdrawal submitted successfully!"); + println!( + " - Updated Account Contract ID: {}", + updated_account.contract_id + ); + println!(" - Pending Balance: {} BTC", updated_account.pending_balance); + println!( + " - Destination: {}", + updated_account.destination_btc_address + ); + println!(); + + println!("=== Example Complete ==="); + println!(); + println!("Summary:"); + println!( + " • Your withdraw account contract ID: {}", + updated_account.contract_id + ); + println!(" • Pending balance: {} BTC", updated_account.pending_balance); + println!( + " • BTC will be sent to: {}", + updated_account.destination_btc_address + ); + println!(); + println!("Important: WithdrawRequests are NOT created atomically with this call."); + println!("The attestor network will process your pending balance and create a"); + println!("WithdrawRequest later. Use 'check_withdraw_requests' to monitor:"); + println!(" cargo run --example check_withdraw_requests_auth0"); + + Ok(()) +} + diff --git a/crates/examples/src/send_cbtc_auth0.rs b/crates/examples/src/send_cbtc_auth0.rs new file mode 100644 index 0000000..bc203cd --- /dev/null +++ b/crates/examples/src/send_cbtc_auth0.rs @@ -0,0 +1,137 @@ +/// Example: Send CBTC to another party (Auth0 Version) +/// +/// Run with: cargo run --example send_cbtc_auth0 +/// +/// Make sure to set up your .env file with: +/// - AUTH0_DOMAIN, AUTH0_CLIENT_ID, AUTH0_CLIENT_SECRET, AUTH0_AUDIENCE +/// - LEDGER_HOST, PARTY_ID, REGISTRY_URL, DECENTRALIZED_PARTY_ID +/// - LIB_TEST_RECEIVER_PARTY_ID (the party to send CBTC to) +/// - TRANSFER_AMOUNT (optional, default: 0.00001) +use cbtc::auth0::{auth0_url, client_credentials, ClientCredentialsParams}; +use std::env; + +#[tokio::main] +async fn main() -> Result<(), String> { + // Load environment variables + dotenvy::dotenv().ok(); + env_logger::init(); + + println!("=== Send CBTC Example (Auth0) ===\n"); + + // Authenticate with Auth0 + println!("Authenticating with Auth0..."); + let auth0_domain = env::var("AUTH0_DOMAIN").map_err(|_| "AUTH0_DOMAIN must be set")?; + let auth0_client_id = env::var("AUTH0_CLIENT_ID").map_err(|_| "AUTH0_CLIENT_ID must be set")?; + let auth0_client_secret = + env::var("AUTH0_CLIENT_SECRET").map_err(|_| "AUTH0_CLIENT_SECRET must be set")?; + let auth0_audience = env::var("AUTH0_AUDIENCE").map_err(|_| "AUTH0_AUDIENCE must be set")?; + + let auth_params = ClientCredentialsParams { + url: auth0_url(&auth0_domain), + client_id: auth0_client_id, + client_secret: auth0_client_secret, + audience: auth0_audience, + }; + + let auth = client_credentials(auth_params) + .await + .map_err(|e| format!("Auth0 authentication failed: {}", e))?; + + println!("✓ Authenticated successfully!"); + println!(" Token expires in: {} seconds\n", auth.expires_in); + + // Set up transfer parameters + let sender_party = env::var("PARTY_ID").expect("PARTY_ID must be set"); + let receiver_party = env::var("LIB_TEST_RECEIVER_PARTY_ID") + .expect("LIB_TEST_RECEIVER_PARTY_ID must be set (the party to send CBTC to)"); + let amount = env::var("TRANSFER_AMOUNT").unwrap_or_else(|_| "0.00001".to_string()); + + println!("Transfer Details:"); + println!(" Amount: {} CBTC", amount); + println!(" From: {}", sender_party); + println!(" To: {}\n", receiver_party); + + // Check if receiver looks like a party ID (should contain ::) + if !receiver_party.contains("::") { + return Err(format!( + "ERROR: Receiver '{}' does not look like a Canton party ID.\n\ + Party IDs should be in format: party-name::1220...\n\ + You provided what looks like a Bitcoin address. For sending CBTC,\n\ + you need a Canton party ID, not a Bitcoin address.", + receiver_party + )); + } + + // Check balance before attempting transfer + println!("Checking CBTC balance..."); + let holdings = cbtc::active_contracts::get(cbtc::active_contracts::Params { + ledger_host: env::var("LEDGER_HOST").expect("LEDGER_HOST must be set"), + party: sender_party.clone(), + access_token: auth.access_token.clone(), + }) + .await?; + + let total_balance: f64 = holdings.iter().filter_map(cbtc::utils::extract_amount).sum(); + + println!(" Current balance: {:.8} CBTC", total_balance); + println!(" UTXO count: {}\n", holdings.len()); + + if holdings.is_empty() { + return Err(format!( + "ERROR: No CBTC holdings found for party {}.\n\ + You need to have CBTC tokens before you can send them.\n\ + \n\ + To get CBTC:\n\ + 1. Run 'cargo run --example mint_cbtc_auth0' to create a deposit account\n\ + 2. Send BTC to the Bitcoin address provided\n\ + 3. Wait for CBTC to be minted (check balance again)", + sender_party + )); + } + + let amount_f64: f64 = amount.parse().map_err(|e| format!("Invalid amount: {}", e))?; + if total_balance < amount_f64 { + return Err(format!( + "ERROR: Insufficient balance.\n\ + You have {:.8} CBTC but trying to send {} CBTC", + total_balance, amount + )); + } + + // Create transfer + let decentralized_party = + env::var("DECENTRALIZED_PARTY_ID").expect("DECENTRALIZED_PARTY_ID must be set"); + + let transfer_params = cbtc::transfer::Params { + transfer: common::transfer::Transfer { + sender: sender_party, + receiver: receiver_party, + amount, + instrument_id: common::transfer::InstrumentId { + admin: decentralized_party.clone(), + id: "CBTC".to_string(), + }, + requested_at: chrono::Utc::now().to_rfc3339(), + execute_before: chrono::Utc::now() + .checked_add_signed(chrono::Duration::hours(168)) + .unwrap() + .to_rfc3339(), + input_holding_cids: None, // Library will auto-select UTXOs + meta: None, + }, + ledger_host: env::var("LEDGER_HOST").expect("LEDGER_HOST must be set"), + access_token: auth.access_token, + registry_url: env::var("REGISTRY_URL").expect("REGISTRY_URL must be set"), + decentralized_party_id: decentralized_party, + }; + + // Submit transfer + println!("Submitting transfer..."); + cbtc::transfer::submit(transfer_params).await?; + + println!("✅ Transfer submitted successfully!"); + println!("\nNote: The receiver must accept the transfer for it to complete."); + + Ok(()) +} + diff --git a/crates/examples/src/stream_auth0.rs b/crates/examples/src/stream_auth0.rs new file mode 100644 index 0000000..3a62a60 --- /dev/null +++ b/crates/examples/src/stream_auth0.rs @@ -0,0 +1,133 @@ +/// Example: Stream CBTC to a Single Receiver (Auth0 Version) +/// +/// This script distributes CBTC multiple times to the same receiver. +/// Useful for streaming payments or testing repeated transfers. +/// +/// Configuration: +/// - RECEIVER_PARTY: The party ID to receive all transfers +/// - TRANSFER_COUNT: Number of transfers to send +/// - TRANSFER_AMOUNT: Amount per transfer +/// +/// Run with: cargo run --example stream_auth0 +use cbtc::auth0::{auth0_url, client_credentials, ClientCredentialsParams}; +use std::env; + +#[tokio::main] +async fn main() -> Result<(), String> { + dotenvy::dotenv().ok(); + env_logger::init(); + + // Load configuration + let sender = env::var("PARTY_ID").expect("PARTY_ID must be set"); + let receiver_party = env::var("RECEIVER_PARTY").expect("RECEIVER_PARTY must be set"); + let transfer_count: usize = env::var("TRANSFER_COUNT") + .expect("TRANSFER_COUNT must be set") + .parse() + .expect("TRANSFER_COUNT must be a valid number"); + let transfer_amount = env::var("TRANSFER_AMOUNT").expect("TRANSFER_AMOUNT must be set"); + + let ledger_host = env::var("LEDGER_HOST").expect("LEDGER_HOST must be set"); + let registry_url = env::var("REGISTRY_URL").expect("REGISTRY_URL must be set"); + let decentralized_party_id = + env::var("DECENTRALIZED_PARTY_ID").expect("DECENTRALIZED_PARTY_ID must be set"); + + // Authenticate with Auth0 + println!("Authenticating with Auth0..."); + let auth0_domain = env::var("AUTH0_DOMAIN").map_err(|_| "AUTH0_DOMAIN must be set")?; + let auth0_client_id = env::var("AUTH0_CLIENT_ID").map_err(|_| "AUTH0_CLIENT_ID must be set")?; + let auth0_client_secret = + env::var("AUTH0_CLIENT_SECRET").map_err(|_| "AUTH0_CLIENT_SECRET must be set")?; + let auth0_audience = env::var("AUTH0_AUDIENCE").map_err(|_| "AUTH0_AUDIENCE must be set")?; + + let auth_params = ClientCredentialsParams { + url: auth0_url(&auth0_domain), + client_id: auth0_client_id, + client_secret: auth0_client_secret, + audience: auth0_audience, + }; + + let auth = client_credentials(auth_params) + .await + .map_err(|e| format!("Auth0 authentication failed: {}", e))?; + + println!("✓ Authenticated successfully!\n"); + + println!("\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"); + println!("Stream CBTC Configuration"); + println!("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"); + println!("Sender: {}", sender); + println!("Receiver: {}", receiver_party); + println!("Transfer count: {}", transfer_count); + println!("Amount per transfer: {}", transfer_amount); + println!( + "Total amount: {} CBTC", + transfer_count as f64 * transfer_amount.parse::().unwrap_or(0.0) + ); + println!("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n"); + + println!("Starting stream of {} transfers...\n", transfer_count); + + let mut successful = 0; + let mut failed = 0; + + for i in 0..transfer_count { + println!( + "[{}/{}] Sending {} CBTC to {}...", + i + 1, + transfer_count, + transfer_amount, + if receiver_party.len() > 30 { + &receiver_party[..30] + } else { + &receiver_party + } + ); + + let transfer_params = cbtc::transfer::Params { + transfer: common::transfer::Transfer { + sender: sender.clone(), + receiver: receiver_party.clone(), + amount: transfer_amount.clone(), + instrument_id: common::transfer::InstrumentId { + admin: decentralized_party_id.clone(), + id: "CBTC".to_string(), + }, + requested_at: chrono::Utc::now().to_rfc3339(), + execute_before: chrono::Utc::now() + .checked_add_signed(chrono::Duration::hours(168)) + .unwrap() + .to_rfc3339(), + input_holding_cids: None, + meta: None, + }, + ledger_host: ledger_host.clone(), + access_token: auth.access_token.clone(), + registry_url: registry_url.clone(), + decentralized_party_id: decentralized_party_id.clone(), + }; + + match cbtc::transfer::submit(transfer_params).await { + Ok(_) => { + println!(" ✓ Transfer submitted successfully"); + successful += 1; + } + Err(e) => { + println!(" ✗ Failed: {}", e); + failed += 1; + } + } + } + + println!("\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"); + println!("Stream Complete!"); + println!("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"); + println!("Successful: {}", successful); + println!("Failed: {}", failed); + + if failed > 0 { + return Err(format!("Stream completed with {} failures", failed)); + } + + Ok(()) +} +