Skip to content

feat(controller): start generalizing to support redirects for mobile #104

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 7 commits into
base: main
Choose a base branch
from
Open
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
135 changes: 70 additions & 65 deletions dojo.h
Original file line number Diff line number Diff line change
@@ -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
*
74 changes: 38 additions & 36 deletions dojo.hpp
Original file line number Diff line number Diff line change
@@ -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<ToriiClient*> 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<ToriiClient*> 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<bool> 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<FieldElement> 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<ToriiClient*> client_new(const char *torii_url,
const char *libp2p_relay_url,
FieldElement world);

/// Publishes a message to the network
///
/// # Parameters
108 changes: 56 additions & 52 deletions dojo.pyx
Original file line number Diff line number Diff line change
@@ -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
19 changes: 11 additions & 8 deletions example/main.c
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
#include "dojo.h"
#include "../dojo.h"
#include <unistd.h>
#include <stdio.h>
#include <string.h>

// 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);
336 changes: 199 additions & 137 deletions src/c/mod.rs
Original file line number Diff line number Diff line change
@@ -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<account_sdk::account::session::policy::Policy>,
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<CallbackState>, 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<CallbackState>, 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<CallbackState>, 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<CallbackState>, 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<bool> {
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
3 changes: 2 additions & 1 deletion src/types.rs
Original file line number Diff line number Diff line change
@@ -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 {