Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
65 changes: 29 additions & 36 deletions examples/tap_battle/src/auth.rs
Original file line number Diff line number Diff line change
@@ -1,14 +1,16 @@
//! Authentication and session management for Tap Battle.
//!
//! This module wraps cougr-core's secp256r1 authentication and SessionBuilder
//! This module wraps cougr-core's secp256r1 authentication and SessionManager
//! to provide the mobile-first auth flow:
//! 1. Player registers a passkey (secp256r1 public key)
//! 2. Player authenticates with biometrics → signature verified on-chain
//! 3. SessionBuilder creates a scoped gameplay session (no per-tx wallet prompts)
//! 3. SessionManager creates a scoped gameplay session (no per-tx wallet prompts)

use soroban_sdk::{symbol_short, Address, Bytes, BytesN, Env, Symbol};

use cougr_core::auth::{verify_secp256r1, Secp256r1Key, Secp256r1Storage, SessionBuilder};
use cougr_core::auth::{verify_secp256r1, Secp256r1Key, Secp256r1Storage};
use cougr_core::accounts::{GameAction, SessionBuilder, SessionStorage};
use cougr_core::session::SessionManager;

use crate::types::*;

Expand Down Expand Up @@ -45,7 +47,7 @@ pub fn register_passkey(env: &Env, player: &Address, pubkey: &BytesN<65>) {
/// Authenticate a player via passkey and create a gameplay session.
///
/// Verifies the secp256r1 signature against the stored public key, then
/// uses `SessionBuilder` to create a session scoped to `tap` and `use_power`
/// uses `SessionManager` to create a session scoped to `tap` and `use_power_up`
/// actions. The session key allows gasless gameplay.
///
/// # Returns
Expand Down Expand Up @@ -76,18 +78,11 @@ pub fn authenticate_and_create_session(
.allow_action(symbol_short!("tap"))
.allow_action(Symbol::new(env, "use_power_up"))
.max_operations(DEFAULT_MAX_OPS)
.expires_at(env.ledger().sequence() as u64 + duration)
.expires_in(duration)
.build_scope();

// Store session state on-chain
let session = SessionState {
player: player.clone(),
expires_at: session_scope.expires_at,
ops_remaining: session_scope.max_operations,
};
env.storage()
.persistent()
.set(&DataKey::Session(player.clone()), &session);
// Store session state via SessionManager::approve
SessionManager::approve(env, player, session_scope).expect("session approval failed");

// Initialize tap counter for this player
env.storage()
Expand All @@ -107,34 +102,32 @@ pub fn authenticate_and_create_session(
// SessionSystem — Session validation
// ============================================================================

/// Validate that a session is still active and decrement operations.
/// Validate that a session is still active and decrement operations via SessionManager.
///
/// Checks expiration and remaining operations. Returns the player address
/// associated with the session.
///
/// # Panics
/// Panics if the session has expired or has no remaining operations.
pub fn validate_session(env: &Env, session_key: &Address) -> Address {
let mut session: SessionState = env
.storage()
.persistent()
.get(&DataKey::Session(session_key.clone()))
.expect("no active session");
pub fn validate_session(env: &Env, session_key: &Address, action_name: Symbol) -> Address {
let keys = SessionStorage::load_all(env, session_key);
let session = keys.last().expect("no active session");

// Check expiration (ledger-based)
assert!(
(env.ledger().sequence() as u64) < session.expires_at,
"session expired"
);

// Check operations remaining
assert!(session.ops_remaining > 0, "session operations exhausted");

// Decrement operations
session.ops_remaining -= 1;
env.storage()
.persistent()
.set(&DataKey::Session(session_key.clone()), &session);
let action = GameAction {
system_name: action_name,
data: Bytes::new(env),
};

session.player.clone()
match SessionManager::execute_action(
env,
session_key,
&session,
action,
env.ledger().timestamp().saturating_add(60),
) {
Ok(_) => session_key.clone(),
Err(cougr_core::accounts::AccountError::SessionExpired) => panic!("session expired"),
Err(cougr_core::accounts::AccountError::SessionBudgetExceeded) => panic!("session operations exhausted"),
Err(e) => panic!("session validation failed: {:?}", e),
}
}
70 changes: 63 additions & 7 deletions examples/tap_battle/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,14 @@
//! → No seed phrases, no mnemonics — just Face ID / Touch ID
//! 2. **Authentication + Session**: Player authenticates via passkey
//! → `verify_secp256r1()` validates the biometric signature
//! → `SessionBuilder` creates a gameplay session scoped to `tap` + `use_power`
//! → `SessionManager` creates a gameplay session scoped to `tap` + `use_power_up`
//! 3. **Gameplay (gasless via session key)**: Rapid tapping is processed
//! through the session key with no per-transaction wallet prompts
//! 4. **Result**: Scores compared, winner declared, stats recorded on-chain
//!
//! # Cougr-Core Integration
//! - `secp256r1_auth`: Passkey registration and signature verification
//! - `SessionBuilder`: Scoped session creation for gasless gameplay
//! - `SessionManager`: Scoped session creation, execution, renewal, and status queries
//! - ECS Components: `PasskeyIdentity`, `TapCounter`, `PowerUp`, `RoundState`
//! - ECS World: Entity management for game objects

Expand All @@ -30,7 +30,7 @@ mod types;
mod test;

use cougr_core::SimpleWorld;
use soroban_sdk::{contract, contractimpl, Address, Bytes, BytesN, Env};
use soroban_sdk::{contract, contractimpl, symbol_short, Address, Bytes, BytesN, Env, Symbol};

// Re-export types for external use
pub use types::*;
Expand Down Expand Up @@ -65,15 +65,15 @@ impl TapBattleContract {
/// Authenticate via passkey and create a gameplay session.
///
/// Verifies the secp256r1 signature (biometric challenge), then creates
/// a `SessionBuilder` session scoped to `tap` and `use_power` actions.
/// a `SessionManager` session scoped to `tap` and `use_power_up` actions.
/// After this call, the player can submit rapid tap actions without
/// per-transaction wallet prompts.
///
/// # Arguments
/// * `player` - The player's Stellar address
/// * `signature` - secp256r1 signature (64 bytes) of the challenge
/// * `challenge` - Random challenge that was signed (32 bytes)
/// * `duration` - Session duration in ledger sequences
/// * `duration` - Session duration in ledger sequences/seconds
///
/// # Returns
/// The session key address for subsequent gameplay calls
Expand All @@ -100,7 +100,7 @@ impl TapBattleContract {
/// # Returns
/// `TapResult` with current count, combo, multiplier, and score earned
pub fn tap(env: Env, session_key: Address) -> TapResult {
let player = auth::validate_session(&env, &session_key);
let player = auth::validate_session(&env, &session_key, symbol_short!("tap"));
game::process_tap(&env, &player)
}

Expand All @@ -116,10 +116,66 @@ impl TapBattleContract {
/// * `session_key` - The player's session address
/// * `power_up` - Power-up kind (0 = DoubleTap, 1 = Shield, 2 = Burst)
pub fn use_power_up(env: Env, session_key: Address, power_up: u32) {
let player = auth::validate_session(&env, &session_key);
let player = auth::validate_session(&env, &session_key, Symbol::new(&env, "use_power_up"));
game::activate_power_up(&env, &player, power_up);
}

/// Extend session lifetime (owner must re-approve via wallet).
pub fn renew_session(
env: Env,
owner: Address,
key_id: BytesN<32>,
expires_in: u64,
) -> cougr_core::session::ActiveSession {
owner.require_auth();
let new_expires = env.ledger().timestamp().saturating_add(expires_in);
let key = cougr_core::session::SessionManager::renew(&env, &owner, &key_id, new_expires).expect("renewed");
let status = cougr_core::session::SessionManager::status(&env, &owner, &key.key_id).expect("session status");
cougr_core::session::ActiveSession::from_status(&status, key.scope.expires_at)
}

/// Read session health for UI renewal prompts.
pub fn session_status(
env: Env,
owner: Address,
key_id: BytesN<32>,
) -> cougr_core::session::SessionStatus {
cougr_core::session::SessionManager::status(&env, &owner, &key_id).expect("session status")
}

/// Tap using session first, falling back to direct owner auth when expired.
pub fn fallback_tap(env: Env, owner: Address, key_id: BytesN<32>) -> TapResult {
let session = cougr_core::accounts::SessionStorage::load(&env, &owner, &key_id).expect("session missing");
let action = cougr_core::accounts::GameAction {
system_name: symbol_short!("tap"),
data: Bytes::new(&env),
};
let session_intent = cougr_core::accounts::SignedIntent::session(
&env,
owner.clone(),
&key_id,
action.clone(),
session.next_nonce,
env.ledger().timestamp().saturating_add(60),
);
let direct_intent = cougr_core::accounts::SignedIntent::direct(
&env,
owner.clone(),
action,
cougr_core::accounts::ReplayProtection::next_account_nonce(&env, &owner),
env.ledger().timestamp().saturating_add(60),
);
cougr_core::session::SessionManager::fallback_execute(&env, &session_intent, &direct_intent).expect("fallback tap");

game::process_tap(&env, &owner)
}

/// Get the latest session key ID for a player.
pub fn get_latest_session_key(env: Env, player: Address) -> BytesN<32> {
let keys = cougr_core::accounts::SessionStorage::load_all(&env, &player);
keys.last().expect("no active session").key_id
}

/// Start a competitive round between two players.
///
/// Both players must have registered passkeys. Round duration is measured
Expand Down
Loading
Loading