diff --git a/dojo.h b/dojo.h index 42b7282..50ec91d 100644 --- a/dojo.h +++ b/dojo.h @@ -7,10 +7,11 @@ namespace dojo_bindings { #endif // __cplusplus -struct ToriiClient; +struct CallbackState; struct Policy; struct ControllerAccount; struct Call; +struct ToriiClient; struct Controller; struct Entity; struct Query; @@ -64,26 +65,22 @@ typedef struct Error { char *message; } Error; -typedef enum ResultToriiClient_Tag { - OkToriiClient, - ErrToriiClient, -} ResultToriiClient_Tag; +typedef enum Resultbool_Tag { + Okbool, + Errbool, +} Resultbool_Tag; -typedef struct ResultToriiClient { - ResultToriiClient_Tag tag; +typedef struct Resultbool { + Resultbool_Tag tag; union { struct { - struct ToriiClient *ok; + bool ok; }; struct { struct Error err; }; }; -} ResultToriiClient; - -typedef struct FieldElement { - uint8_t data[32]; -} FieldElement; +} Resultbool; typedef enum ResultControllerAccount_Tag { OkControllerAccount, @@ -102,22 +99,9 @@ typedef struct ResultControllerAccount { }; } ResultControllerAccount; -typedef enum Resultbool_Tag { - Okbool, - Errbool, -} Resultbool_Tag; - -typedef struct Resultbool { - Resultbool_Tag tag; - union { - struct { - bool ok; - }; - struct { - struct Error err; - }; - }; -} Resultbool; +typedef struct FieldElement { + uint8_t data[32]; +} FieldElement; typedef enum ResultFieldElement_Tag { OkFieldElement, @@ -136,6 +120,23 @@ typedef struct ResultFieldElement { }; } ResultFieldElement; +typedef enum ResultToriiClient_Tag { + OkToriiClient, + ErrToriiClient, +} ResultToriiClient_Tag; + +typedef struct ResultToriiClient { + ResultToriiClient_Tag tag; + union { + struct { + struct ToriiClient *ok; + }; + struct { + struct Error err; + }; + }; +} ResultToriiClient; + typedef struct CArrayu8 { uint8_t *data; uintptr_t data_len; @@ -785,31 +786,14 @@ typedef struct EnumOption { extern "C" { #endif // __cplusplus -/** - * Creates a new Torii client instance - * - * # Parameters - * * `torii_url` - URL of the Torii server - * * `libp2p_relay_url` - URL of the libp2p relay server - * * `world` - World address as a FieldElement - * - * # Returns - * Result containing pointer to new ToriiClient instance or error - */ -struct ResultToriiClient client_new(const char *torii_url, - const char *libp2p_relay_url, - struct FieldElement world); - /** * Initiates a connection to establish a new session account * * This function: * 1. Generates a new signing key pair - * 2. Starts a local HTTP server to receive the callback - * 3. Opens the keychain session URL in browser - * 4. Waits for callback with session details - * 5. Creates and stores the session - * 6. Calls the provided callback with the new session account + * 2. If redirect_uri is provided: Returns CallbackState for deep link handling + * 3. If redirect_uri is null: Starts a local HTTP server for callback + * 4. Opens the keychain session URL in browser * * # Safety * This function is marked as unsafe because it: @@ -822,25 +806,31 @@ struct ResultToriiClient client_new(const char *torii_url, * * `policies` - Pointer to array of Policy structs defining session permissions * * `policies_len` - Length of the policies array * * `account_callback` - Function pointer called with the new session account when ready + * * `redirect_uri` - Optional pointer to null-terminated string containing the redirect URI. If + * provided, will be used for callback instead of starting a local server. + * + * # Returns + * If redirect_uri is provided, returns pointer to CallbackState that must be used with + * handle_deep_link_callback. If redirect_uri is null, returns null pointer. + */ +struct CallbackState *controller_connect(const char *rpc_url, + const struct Policy *policies, + uintptr_t policies_len, + void (*account_callback)(struct ControllerAccount*), + const char *redirect_uri); + +/** + * Handles the deep link callback when app is reopened * - * # Example - * ```c - * void on_account(SessionAccount* account) { - * // Handle new session account - * } + * # Parameters + * * `callback_data` - Base64 encoded callback data from the deep link + * * `state` - CallbackState pointer returned from controller_connect * - * controller_connect( - * "https://rpc.example.com", - * policies, - * policies_length, - * on_account - * ); - * ``` + * # Returns + * Result containing success boolean or error */ -void controller_connect(const char *rpc_url, - const struct Policy *policies, - uintptr_t policies_len, - void (*account_callback)(struct ControllerAccount*)); +struct Resultbool controller_handle_deep_link_callback(const char *callback_data, + struct CallbackState *state); /** * Retrieves a stored session account if one exists and is valid @@ -955,6 +945,21 @@ struct ResultFieldElement controller_execute_from_outside(struct ControllerAccou */ void client_set_logger(struct ToriiClient *client, void (*logger)(const char*)); +/** + * Creates a new Torii client instance + * + * # Parameters + * * `torii_url` - URL of the Torii server + * * `libp2p_relay_url` - URL of the libp2p relay server + * * `world` - World address as a FieldElement + * + * # Returns + * Result containing pointer to new ToriiClient instance or error + */ +struct ResultToriiClient client_new(const char *torii_url, + const char *libp2p_relay_url, + struct FieldElement world); + /** * Publishes a message to the network * diff --git a/dojo.hpp b/dojo.hpp index 5bfe50a..d498858 100644 --- a/dojo.hpp +++ b/dojo.hpp @@ -6,10 +6,11 @@ namespace dojo_bindings { -struct ToriiClient; +struct CallbackState; struct Policy; struct ControllerAccount; struct Call; +struct ToriiClient; struct Ty; struct Query; struct Subscription; @@ -907,28 +908,13 @@ struct EntityKeysClause { extern "C" { -/// Creates a new Torii client instance -/// -/// # Parameters -/// * `torii_url` - URL of the Torii server -/// * `libp2p_relay_url` - URL of the libp2p relay server -/// * `world` - World address as a FieldElement -/// -/// # Returns -/// Result containing pointer to new ToriiClient instance or error -Result client_new(const char *torii_url, - const char *libp2p_relay_url, - FieldElement world); - /// Initiates a connection to establish a new session account /// /// This function: /// 1. Generates a new signing key pair -/// 2. Starts a local HTTP server to receive the callback -/// 3. Opens the keychain session URL in browser -/// 4. Waits for callback with session details -/// 5. Creates and stores the session -/// 6. Calls the provided callback with the new session account +/// 2. If redirect_uri is provided: Returns CallbackState for deep link handling +/// 3. If redirect_uri is null: Starts a local HTTP server for callback +/// 4. Opens the keychain session URL in browser /// /// # Safety /// This function is marked as unsafe because it: @@ -941,24 +927,27 @@ Result client_new(const char *torii_url, /// * `policies` - Pointer to array of Policy structs defining session permissions /// * `policies_len` - Length of the policies array /// * `account_callback` - Function pointer called with the new session account when ready +/// * `redirect_uri` - Optional pointer to null-terminated string containing the redirect URI. If +/// provided, will be used for callback instead of starting a local server. /// -/// # Example -/// ```c -/// void on_account(SessionAccount* account) { -/// // Handle new session account -/// } -/// -/// controller_connect( -/// "https://rpc.example.com", -/// policies, -/// policies_length, -/// on_account -/// ); -/// ``` -void controller_connect(const char *rpc_url, - const Policy *policies, - uintptr_t policies_len, - void (*account_callback)(ControllerAccount*)); +/// # Returns +/// If redirect_uri is provided, returns pointer to CallbackState that must be used with +/// handle_deep_link_callback. If redirect_uri is null, returns null pointer. +CallbackState *controller_connect(const char *rpc_url, + const Policy *policies, + uintptr_t policies_len, + void (*account_callback)(ControllerAccount*), + const char *redirect_uri); + +/// Handles the deep link callback when app is reopened +/// +/// # Parameters +/// * `callback_data` - Base64 encoded callback data from the deep link +/// * `state` - CallbackState pointer returned from controller_connect +/// +/// # Returns +/// Result containing success boolean or error +Result controller_handle_deep_link_callback(const char *callback_data, CallbackState *state); /// Retrieves a stored session account if one exists and is valid /// @@ -1055,6 +1044,19 @@ Result controller_execute_from_outside(ControllerAccount *controll /// * `logger` - Callback function that takes a C string parameter void client_set_logger(ToriiClient *client, void (*logger)(const char*)); +/// Creates a new Torii client instance +/// +/// # Parameters +/// * `torii_url` - URL of the Torii server +/// * `libp2p_relay_url` - URL of the libp2p relay server +/// * `world` - World address as a FieldElement +/// +/// # Returns +/// Result containing pointer to new ToriiClient instance or error +Result client_new(const char *torii_url, + const char *libp2p_relay_url, + FieldElement world); + /// Publishes a message to the network /// /// # Parameters diff --git a/dojo.pyx b/dojo.pyx index cb6f39f..e595aba 100644 --- a/dojo.pyx +++ b/dojo.pyx @@ -35,6 +35,9 @@ cdef extern from *: cdef struct Account: pass + cdef struct CallbackState: + pass + cdef struct ControllerAccount: pass @@ -50,18 +53,15 @@ cdef extern from *: cdef struct Error: char *message; - cdef enum ResultToriiClient_Tag: - OkToriiClient, - ErrToriiClient, + cdef enum Resultbool_Tag: + Okbool, + Errbool, - cdef struct ResultToriiClient: - ResultToriiClient_Tag tag; - ToriiClient *ok; + cdef struct Resultbool: + Resultbool_Tag tag; + bool ok; Error err; - cdef struct FieldElement: - uint8_t data[32]; - cdef enum ResultControllerAccount_Tag: OkControllerAccount, ErrControllerAccount, @@ -71,14 +71,8 @@ cdef extern from *: ControllerAccount *ok; Error err; - cdef enum Resultbool_Tag: - Okbool, - Errbool, - - cdef struct Resultbool: - Resultbool_Tag tag; - bool ok; - Error err; + cdef struct FieldElement: + uint8_t data[32]; cdef enum ResultFieldElement_Tag: OkFieldElement, @@ -89,6 +83,15 @@ cdef extern from *: FieldElement ok; Error err; + cdef enum ResultToriiClient_Tag: + OkToriiClient, + ErrToriiClient, + + cdef struct ResultToriiClient: + ResultToriiClient_Tag tag; + ToriiClient *ok; + Error err; + cdef struct CArrayu8: uint8_t *data; uintptr_t data_len; @@ -492,28 +495,13 @@ cdef extern from *: const char *name; Ty *ty; - # Creates a new Torii client instance - # - # # Parameters - # * `torii_url` - URL of the Torii server - # * `libp2p_relay_url` - URL of the libp2p relay server - # * `world` - World address as a FieldElement - # - # # Returns - # Result containing pointer to new ToriiClient instance or error - ResultToriiClient client_new(const char *torii_url, - const char *libp2p_relay_url, - FieldElement world); - # Initiates a connection to establish a new session account # # This function: # 1. Generates a new signing key pair - # 2. Starts a local HTTP server to receive the callback - # 3. Opens the keychain session URL in browser - # 4. Waits for callback with session details - # 5. Creates and stores the session - # 6. Calls the provided callback with the new session account + # 2. If redirect_uri is provided: Returns CallbackState for deep link handling + # 3. If redirect_uri is null: Starts a local HTTP server for callback + # 4. Opens the keychain session URL in browser # # # Safety # This function is marked as unsafe because it: @@ -526,24 +514,27 @@ cdef extern from *: # * `policies` - Pointer to array of Policy structs defining session permissions # * `policies_len` - Length of the policies array # * `account_callback` - Function pointer called with the new session account when ready + # * `redirect_uri` - Optional pointer to null-terminated string containing the redirect URI. If + # provided, will be used for callback instead of starting a local server. + # + # # Returns + # If redirect_uri is provided, returns pointer to CallbackState that must be used with + # handle_deep_link_callback. If redirect_uri is null, returns null pointer. + CallbackState *controller_connect(const char *rpc_url, + const Policy *policies, + uintptr_t policies_len, + void (*account_callback)(ControllerAccount*), + const char *redirect_uri); + + # Handles the deep link callback when app is reopened # - # # Example - # ```c - # void on_account(SessionAccount* account) { - # // Handle new session account - # } - # - # controller_connect( - # "https://rpc.example.com", - # policies, - # policies_length, - # on_account - # ); - # ``` - void controller_connect(const char *rpc_url, - const Policy *policies, - uintptr_t policies_len, - void (*account_callback)(ControllerAccount*)); + # # Parameters + # * `callback_data` - Base64 encoded callback data from the deep link + # * `state` - CallbackState pointer returned from controller_connect + # + # # Returns + # Result containing success boolean or error + Resultbool controller_handle_deep_link_callback(const char *callback_data, CallbackState *state); # Retrieves a stored session account if one exists and is valid # @@ -640,6 +631,19 @@ cdef extern from *: # * `logger` - Callback function that takes a C string parameter void client_set_logger(ToriiClient *client, void (*logger)(const char*)); + # Creates a new Torii client instance + # + # # Parameters + # * `torii_url` - URL of the Torii server + # * `libp2p_relay_url` - URL of the libp2p relay server + # * `world` - World address as a FieldElement + # + # # Returns + # Result containing pointer to new ToriiClient instance or error + ResultToriiClient client_new(const char *torii_url, + const char *libp2p_relay_url, + FieldElement world); + # Publishes a message to the network # # # Parameters diff --git a/example/main.c b/example/main.c index 2fcc306..ed619fa 100644 --- a/example/main.c +++ b/example/main.c @@ -1,10 +1,10 @@ -#include "dojo.h" +#include "../dojo.h" #include #include #include // Add this global variable near the top of the file -static struct SessionAccount* g_session_account = NULL; +static struct ControllerAccount* g_session_account = NULL; void on_entity_state_update(FieldElement key, CArrayStruct models) { @@ -52,7 +52,7 @@ void hex_to_bytes(const char *hex, FieldElement *felt) } } -void on_account_connected(struct SessionAccount *account) +void on_account_connected(struct ControllerAccount *account) { // Store the account in our global variable g_session_account = account; @@ -74,7 +74,7 @@ int main() FieldElement katana; hex_to_bytes("0x03dc18a09d1dc893eb1abce2e0d33b8cc285ea430a4bdd30bccf0c8638e59659", &katana); - ResultToriiClient resClient = client_new(torii_url, rpc_url, "/ip4/127.0.0.1/tcp/9090", world); + ResultToriiClient resClient = client_new(torii_url, "/ip4/127.0.0.1/tcp/9090", world); if (resClient.tag == ErrToriiClient) { printf("Failed to create client: %s\n", resClient.err.message); @@ -95,12 +95,15 @@ int main() } struct Provider *controller_provider = resControllerProvider.ok; - ResultController resSessionAccount = controller_account(policies, 2); - if (resSessionAccount.tag == OkController) { + FieldElement chain_id; + hex_to_bytes("0x534e5f474f45524c49", &chain_id); + + ResultControllerAccount resSessionAccount = controller_account(policies, 2, chain_id); + if (resSessionAccount.tag == OkControllerAccount) { printf("Session account already connected\n"); g_session_account = resSessionAccount.ok; } else { - controller_connect("https://api.cartridge.gg/x/knet-controller/katana", policies, 2, on_account_connected); + controller_connect("https://api.cartridge.gg/x/knet-controller/katana", policies, 2, on_account_connected, NULL); } // Wait for the account to be connected @@ -197,7 +200,7 @@ int main() Query query = {}; query.limit = 100; query.clause.tag = NoneClause; - ResultCArrayEntity resEntities = client_entities(client, &query); + ResultCArrayEntity resEntities = client_entities(client, &query, false); if (resEntities.tag == ErrCArrayEntity) { printf("Failed to get entities: %s\n", resEntities.err.message); diff --git a/src/c/mod.rs b/src/c/mod.rs index b0bf3dd..fe714f4 100644 --- a/src/c/mod.rs +++ b/src/c/mod.rs @@ -5,7 +5,6 @@ use std::fs; use std::net::SocketAddr; use std::ops::Deref; use std::os::raw::c_char; -use std::str::FromStr; use std::sync::atomic::{AtomicU64, Ordering}; use std::sync::Arc; use std::time::Duration; @@ -24,7 +23,7 @@ use axum::http::{header, HeaderValue, Method, StatusCode}; use axum::response::IntoResponse; use axum::routing::post; use axum::Router; -use base64::engine::general_purpose::STANDARD as BASE64; +use base64::engine::general_purpose::STANDARD_NO_PAD as BASE64; use base64::Engine as _; use cainome::cairo_serde::{self, ByteArray, CairoSerde}; use crypto_bigint::U256; @@ -69,76 +68,21 @@ lazy_static! { Arc::new(Runtime::new().expect("Failed to create Tokio runtime")); } -/// Creates a new Torii client instance -/// -/// # Parameters -/// * `torii_url` - URL of the Torii server -/// * `libp2p_relay_url` - URL of the libp2p relay server -/// * `world` - World address as a FieldElement -/// -/// # Returns -/// Result containing pointer to new ToriiClient instance or error -#[no_mangle] -pub unsafe extern "C" fn client_new( - torii_url: *const c_char, - libp2p_relay_url: *const c_char, - world: types::FieldElement, -) -> Result<*mut ToriiClient> { - let torii_url = unsafe { CStr::from_ptr(torii_url).to_string_lossy().into_owned() }; - let libp2p_relay_url = - unsafe { CStr::from_ptr(libp2p_relay_url).to_string_lossy().into_owned() }; - - let client_future = TClient::new(torii_url, libp2p_relay_url, (&world).into()); - - let client = match RUNTIME.block_on(client_future) { - Ok(client) => client, - Err(e) => return Result::Err(e.into()), - }; - - let relay_runner = client.relay_runner(); - RUNTIME.spawn(async move { - relay_runner.lock().await.run().await; - }); - - Result::Ok(Box::into_raw(Box::new(ToriiClient { inner: client, logger: None }))) -} - -// State struct to share data with callback handler #[derive(Clone)] -struct CallbackState { +pub(crate) struct CallbackState { shutdown_tx: tokio::sync::mpsc::Sender<()>, - rpc_url: String, + rpc_url: Url, policies: Vec, private_key: SigningKey, public_key: Felt, account_callback: extern "C" fn(*mut ControllerAccount), } -// Modify handle_callback to call the callback -async fn handle_callback(State(state): State, body: String) -> impl IntoResponse { - // Decode base64 payload - let padded = match body.len() % 4 { - 0 => body, - n => body + &"=".repeat(4 - n), - }; - let decoded = match BASE64.decode(padded) { - Ok(d) => d, - Err(e) => { - println!("Failed to decode payload: {}", e); - return StatusCode::BAD_REQUEST; - } - }; - - // Parse JSON from decoded bytes - let payload: RegisterSessionResponse = match serde_json::from_slice(&decoded) { - Ok(p) => p, - Err(e) => { - println!("Failed to deserialize payload: {}", e); - return StatusCode::BAD_REQUEST; - } - }; - - let provider = CartridgeJsonRpcProvider::new(Url::from_str(&state.rpc_url).unwrap()); +async fn process_session_callback( + state: CallbackState, + payload: RegisterSessionResponse, +) -> anyhow::Result<()> { + let provider = CartridgeJsonRpcProvider::new(state.rpc_url.clone()); let chain_id = provider.chain_id().await.unwrap(); let project_dirs = ProjectDirs::from("org", "dojoengine", "dojo"); @@ -198,20 +142,43 @@ async fn handle_callback(State(state): State, body: String) -> im username: payload.username, }))); - // Signal shutdown after handling callback + // Shutdown the server if we're not using a redirect URI state.shutdown_tx.send(()).await.unwrap(); - StatusCode::OK + Ok(()) +} + +// Modify handle_callback to use the shared function +async fn handle_callback(State(state): State, body: String) -> impl IntoResponse { + let decoded = match BASE64.decode(body) { + Ok(d) => d, + Err(e) => { + println!("Failed to decode payload: {}", e); + return StatusCode::BAD_REQUEST; + } + }; + + // Parse JSON from decoded bytes + let payload: RegisterSessionResponse = match serde_json::from_slice(&decoded) { + Ok(p) => p, + Err(e) => { + println!("Failed to deserialize payload: {}", e); + return StatusCode::BAD_REQUEST; + } + }; + + match process_session_callback(state, payload).await { + Ok(_) => StatusCode::OK, + Err(_) => StatusCode::INTERNAL_SERVER_ERROR, + } } /// Initiates a connection to establish a new session account /// /// This function: /// 1. Generates a new signing key pair -/// 2. Starts a local HTTP server to receive the callback -/// 3. Opens the keychain session URL in browser -/// 4. Waits for callback with session details -/// 5. Creates and stores the session -/// 6. Calls the provided callback with the new session account +/// 2. If redirect_uri is provided: Returns CallbackState for deep link handling +/// 3. If redirect_uri is null: Starts a local HTTP server for callback +/// 4. Opens the keychain session URL in browser /// /// # Safety /// This function is marked as unsafe because it: @@ -224,28 +191,22 @@ async fn handle_callback(State(state): State, body: String) -> im /// * `policies` - Pointer to array of Policy structs defining session permissions /// * `policies_len` - Length of the policies array /// * `account_callback` - Function pointer called with the new session account when ready +/// * `redirect_uri` - Optional pointer to null-terminated string containing the redirect URI. If +/// provided, will be used for callback instead of starting a local server. /// -/// # Example -/// ```c -/// void on_account(SessionAccount* account) { -/// // Handle new session account -/// } -/// -/// controller_connect( -/// "https://rpc.example.com", -/// policies, -/// policies_length, -/// on_account -/// ); -/// ``` +/// # Returns +/// If redirect_uri is provided, returns pointer to CallbackState that must be used with +/// handle_deep_link_callback. If redirect_uri is null, returns null pointer. #[no_mangle] pub unsafe extern "C" fn controller_connect( rpc_url: *const c_char, policies: *const Policy, policies_len: usize, account_callback: extern "C" fn(*mut ControllerAccount), -) { - let rpc_url = unsafe { CStr::from_ptr(rpc_url).to_string_lossy().into_owned() }; + redirect_uri: *const c_char, +) -> *mut CallbackState { + let rpc_url = + Url::parse(&unsafe { CStr::from_ptr(rpc_url).to_string_lossy().into_owned() }).unwrap(); let policies = unsafe { std::slice::from_raw_parts(policies, policies_len) }; let account_policies = policies .iter() @@ -261,61 +222,131 @@ pub unsafe extern "C" fn controller_connect( let keyring = Entry::new("dojo-keyring", &format!("{:#x}", verifying_key)).unwrap(); keyring.set_password(&format!("{:#x}", signing_key.secret_scalar())).unwrap(); - // Create shutdown channel - let (shutdown_tx, mut shutdown_rx) = tokio::sync::mpsc::channel(1); - - // Create state with RPC URL and shutdown sender - let state = CallbackState { - shutdown_tx, - rpc_url: rpc_url.clone(), - policies: account_policies, - private_key: signing_key, - public_key: verifying_key, - account_callback, - }; - - // Set up the HTTP callback server with state and CORS - let app = Router::new() - .route("/callback", post(handle_callback)) - .layer( - CorsLayer::new() - .allow_origin(AllowOrigin::exact(HeaderValue::from_static( - "https://x.cartridge.gg", - ))) - .allow_methods([Method::POST]) - .allow_headers([header::CONTENT_TYPE]), - ) - .with_state(state); - - // Find an available port by trying to bind to port 0 - let addr = SocketAddr::from(([127, 0, 0, 1], 0)); - let listener = RUNTIME.block_on(TcpListener::bind(addr)).unwrap(); - let bound_addr = listener.local_addr().unwrap(); + let mut url = url::Url::parse(constants::KEYCHAIN_SESSION_URL).unwrap(); + let callback_state = if !redirect_uri.is_null() { + // Use provided redirect URI + url.query_pairs_mut().append_pair("redirect_uri", &unsafe { + CStr::from_ptr(redirect_uri).to_string_lossy().into_owned() + }); - let server = axum::serve(listener, app); + // Create callback state for deep link handling + let (shutdown_tx, _) = tokio::sync::mpsc::channel(1); + let state = CallbackState { + shutdown_tx, + rpc_url: rpc_url.clone(), + policies: account_policies, + private_key: signing_key, + public_key: verifying_key, + account_callback, + }; + Box::into_raw(Box::new(state)) + } else { + // Create shutdown channel + let (shutdown_tx, mut shutdown_rx) = tokio::sync::mpsc::channel(1); + + // Create state with RPC URL and shutdown sender + let state = CallbackState { + shutdown_tx, + rpc_url: rpc_url.clone(), + policies: account_policies, + private_key: signing_key, + public_key: verifying_key, + account_callback, + }; - // Spawn server with graceful shutdown - RUNTIME.spawn(async move { - server - .with_graceful_shutdown(async move { - shutdown_rx.recv().await; - println!("Shutting down server"); - }) - .await - .unwrap(); - }); + // Set up the HTTP callback server with state and CORS + let app = Router::new() + .route("/callback", post(handle_callback)) + .layer( + CorsLayer::new() + .allow_origin(AllowOrigin::exact(HeaderValue::from_static( + "https://x.cartridge.gg", + ))) + .allow_methods([Method::POST]) + .allow_headers([header::CONTENT_TYPE]), + ) + .with_state(state); + + // Find an available port by trying to bind to port 0 + let addr = SocketAddr::from(([127, 0, 0, 1], 0)); + let listener = RUNTIME.block_on(TcpListener::bind(addr)).unwrap(); + let bound_addr = listener.local_addr().unwrap(); + + let server = axum::serve(listener, app); + + // Spawn server with graceful shutdown + RUNTIME.spawn(async move { + server + .with_graceful_shutdown(async move { + shutdown_rx.recv().await; + println!("Shutting down server"); + }) + .await + .unwrap(); + }); - println!("Listening on {}", bound_addr); + println!("Listening on {}", bound_addr); + let callback_url = + format!("http://{}/callback", bound_addr).replace("127.0.0.1", "localhost"); + url.query_pairs_mut().append_pair("callback_uri", &callback_url); + std::ptr::null_mut() + }; - let callback_url = format!("http://{}/callback", bound_addr).replace("127.0.0.1", "localhost"); - let mut url = url::Url::parse(constants::KEYCHAIN_SESSION_URL).unwrap(); url.query_pairs_mut() - .append_pair("callback_uri", &callback_url) .append_pair("public_key", &format!("{:#x}", verifying_key)) - .append_pair("rpc_url", &rpc_url) + .append_pair("rpc_url", &rpc_url.to_string()) .append_pair("policies", &serde_json::to_string(&policies).unwrap()); open::that(url.as_str()).unwrap(); + callback_state +} + +/// Handles the deep link callback when app is reopened +/// +/// # Parameters +/// * `callback_data` - Base64 encoded callback data from the deep link +/// * `state` - CallbackState pointer returned from controller_connect +/// +/// # Returns +/// Result containing success boolean or error +#[no_mangle] +pub unsafe extern "C" fn controller_handle_deep_link_callback( + callback_data: *const c_char, + state: *mut CallbackState, +) -> Result { + let callback_data = unsafe { CStr::from_ptr(callback_data).to_string_lossy().into_owned() }; + let state = unsafe { Box::from_raw(state) }; + + let decoded = match BASE64.decode(callback_data) { + Ok(d) => d, + Err(e) => { + println!("Failed to decode payload: {}", e); + return Result::Err(Error { + message: CString::new("Failed to decode callback data").unwrap().into_raw(), + }); + } + }; + + // Parse JSON from decoded bytes + let payload: RegisterSessionResponse = match serde_json::from_slice(&decoded) { + Ok(p) => p, + Err(e) => { + println!("Failed to deserialize payload: {}", e); + return Result::Err(Error { + message: CString::new("Failed to parse callback data").unwrap().into_raw(), + }); + } + }; + + // Process the callback using shared function + match RUNTIME.block_on(process_session_callback(*state, payload)) { + Ok(_) => Result::Ok(true), + Err(e) => Result::Err(Error { + message: CString::new(format!("Failed to process session callback: {}", e)) + .unwrap() + .into_raw(), + }), + } } /// Retrieves a stored session account if one exists and is valid @@ -391,7 +422,7 @@ pub unsafe extern "C" fn controller_account( let signing_key_hex = keyring.get_password().ok()?; // Initialize provider and signer - let provider = CartridgeJsonRpcProvider::new(Url::from_str(&account.rpc_url).unwrap()); + let provider = CartridgeJsonRpcProvider::new(account.rpc_url.clone()); let signing_key = SigningKey::from_secret_scalar(Felt::from_hex(&signing_key_hex).unwrap()); let signer = Signer::Starknet(signing_key); @@ -613,10 +644,7 @@ pub unsafe extern "C" fn controller_execute_raw( match RUNTIME.block_on(call.send()) { Ok(result) => Result::Ok((&result.transaction_hash).into()), - Err(e) => { - println!("Error executing call: {:?}", e); - Result::Err(e.into()) - } + Err(e) => Result::Err(e.into()), } } @@ -687,6 +715,40 @@ pub unsafe extern "C" fn client_set_logger( } } +/// Creates a new Torii client instance +/// +/// # Parameters +/// * `torii_url` - URL of the Torii server +/// * `libp2p_relay_url` - URL of the libp2p relay server +/// * `world` - World address as a FieldElement +/// +/// # Returns +/// Result containing pointer to new ToriiClient instance or error +#[no_mangle] +pub unsafe extern "C" fn client_new( + torii_url: *const c_char, + libp2p_relay_url: *const c_char, + world: types::FieldElement, +) -> Result<*mut ToriiClient> { + let torii_url = unsafe { CStr::from_ptr(torii_url).to_string_lossy().into_owned() }; + let libp2p_relay_url = + unsafe { CStr::from_ptr(libp2p_relay_url).to_string_lossy().into_owned() }; + + let client_future = TClient::new(torii_url, libp2p_relay_url, (&world).into()); + + let client = match RUNTIME.block_on(client_future) { + Ok(client) => client, + Err(e) => return Result::Err(e.into()), + }; + + let relay_runner = client.relay_runner(); + RUNTIME.spawn(async move { + relay_runner.lock().await.run().await; + }); + + Result::Ok(Box::into_raw(Box::new(ToriiClient { inner: client, logger: None }))) +} + /// Publishes a message to the network /// /// # Parameters diff --git a/src/types.rs b/src/types.rs index bc49c2f..5c2045d 100644 --- a/src/types.rs +++ b/src/types.rs @@ -14,6 +14,7 @@ use starknet::signers::LocalWallet; use starknet_crypto::Felt; use stream_cancel::Trigger; use torii_client::client::Client; +use url::Url; use wasm_bindgen::prelude::*; #[derive(Debug, Clone, Serialize, Deserialize)] @@ -63,7 +64,7 @@ pub struct RegisteredAccount { pub address: Felt, pub owner_guid: Felt, pub chain_id: Felt, - pub rpc_url: String, + pub rpc_url: Url, } impl SessionsStorage {