monaco-sdk 0.8.1

Typed Rust client for the Monaco REST API — generated from the OpenAPI specification
Documentation
mod common;

use common::{authed_client, authenticate, client};

#[tokio::test]
#[ignore = "requires running API server"]
async fn auth_flow_and_profile() {
    let (token, address, _signer) = authenticate().await;

    let authed = authed_client(&token);
    let profile = authed.get_user_profile().await.unwrap().into_inner();

    let profile_addr = profile.address.unwrap();
    assert_eq!(profile_addr.to_lowercase(), address.to_lowercase());
}

/// After authenticating, the refresh exchange must return a fresh access
/// token that the holder can use in place of the original. This is the
/// only way long-lived sessions stay usable without re-signing, and a
/// same-token response would be a security-relevant regression.
#[tokio::test]
#[ignore = "requires running API server"]
async fn refresh_token_rotates_access_token() {
    let signer = alloy::signers::local::PrivateKeySigner::random();
    let address = format!("{:?}", signer.address());

    let c = client();

    let challenge = c
        .create_challenge(&monaco_sdk::types::ChallengeRequest {
            address: address.parse().unwrap(),
            chain_id: None,
            client_id: None,
        })
        .await
        .unwrap()
        .into_inner();

    let message = challenge.message.unwrap();
    let nonce = challenge.nonce.unwrap();
    let signature = alloy::signers::Signer::sign_message(&signer, message.as_bytes())
        .await
        .unwrap();
    let sig_hex = format!("0x{}", alloy::hex::encode(signature.as_bytes()));

    let verify = c
        .verify_signature(&monaco_sdk::types::VerifyRequest {
            address: address.parse().unwrap(),
            chain_id: None,
            client_id: None,
            nonce: nonce.parse().unwrap(),
            signature: sig_hex.parse().unwrap(),
        })
        .await
        .unwrap()
        .into_inner();

    let refresh_token = verify
        .refresh_token
        .expect("verify should return a refresh_token");
    let original_access = verify.access_token.clone().unwrap();

    let refreshed = c
        .refresh_token(&monaco_sdk::types::RefreshRequest { refresh_token })
        .await
        .unwrap()
        .into_inner();

    let new_access = refreshed
        .access_token
        .expect("refresh should return a new access_token");
    assert!(
        !new_access.is_empty(),
        "refresh returned empty access_token"
    );
    assert_ne!(
        new_access, original_access,
        "refresh must rotate the access token — returning the same JWT on refresh would be a security regression"
    );

    // The new token must actually authenticate a real call; an identity
    // pass that returns a fresh string but is rejected by the server is
    // indistinguishable from a broken refresh at the client.
    let authed = authed_client(&new_access);
    let profile = authed.get_user_profile().await.unwrap().into_inner();
    assert_eq!(
        profile.address.unwrap().to_lowercase(),
        address.to_lowercase(),
        "refreshed access_token should authenticate as the original wallet"
    );
}

/// `get_user_profile` must reject calls without a valid token.
#[tokio::test]
#[ignore = "requires running API server"]
async fn unauthenticated_profile_call_is_rejected() {
    let resp = client().get_user_profile().await;
    let Err(err) = resp else {
        panic!("expected 401 from get_user_profile without auth, got Ok");
    };
    match err {
        monaco_sdk::Error::ErrorResponse(r) => {
            assert_eq!(r.status().as_u16(), 401, "expected HTTP 401, got {r:?}");
        }
        other => panic!("expected ErrorResponse(401), got {other:?}"),
    }
}