Native Integration
Controller was initially developed as a web wallet, designed for use in browser-based applications. As the ecosystem has grown more ambitious, there has been growing demand for integrating Controller into native and mobile applications directly.
This guide will explain how to integrate Controller into native and mobile applications.
Controller.c
The Controller source code is written in Rust -- chosen for its developer ergonomics, type and memory safety, portability, and performance. Supporting native integrations of Controller meant generating FFI (Foreign Function Interfaces) definitions for C, a foundational language which can be easily imported into many programming environments -- including C++, Python, Kotlin, Objective-C, and more.
With Controller.c, application developers can integrate Controller into native applications directly.
Headless Controller
Controller.c includes support for "headless" Controllers, which takes user-supplied signing keys and gives greater control to the application developer.
Here is an illustrative example of how to set up a new headless Controller with Controller.c:
#include "bindings/c/Controller.h"
#include "bindings/c/DiplomatOwner.h"
// Create owner from locally-generated private key
// IMPORTANT: never commit private keys in source code
DiplomatStringView private_key = {"0x1234...", 66};
DiplomatOwner *owner = DiplomatOwner_new_from_starknet_signer(private_key).ok;
// Create a new headless Controller
Controller *controller = Controller_new_headless(
app_id, username, class_hash, rpc_url, owner, chain_id).ok;
// Register a new Controller account onchain
Controller_signup_result signup_result = Controller_signup(
controller, SignerType_Starknet, session_expiration, cartridge_api_url);
Native Sessions
With the regular Controller, signing keys are created by the Cartridge keychain and stored in a secure enclave, inaccessible to application developers. With Controller.c, session signing keys are generated by the application itself and linked to the user's account through a browser-based login flow.
The following is a high-level summary of the session-linking flow:
Native application generates a new keypair
The native application generates a new keypair using a local utility like ssh-keygen
.
The application should securely store the private key and use it to sign transactions for the user.
Native application initiates the Controller login flow
In order to link the keypair to the user's Controller account, the application will initiate the Controller connect flow, passing in the keypair's public key and a Merkle root of the policy object for session creation.
This flow will open a browser window inside the application interface, with the key and policy root as URL paramters, and prompt the user to log in with Controller.
Keychain authorizes and records session onchain
Inside of the local browser, the user will complete the Controller WebAuthn login flow, at which point the keychain will sign the public key and the policy root using the user's credentials. This signed payload will then be recorded onchain at the Controller contract and used to validate future transactions.
As a final step, the native application will receive a GraphQL push with the user's Controller address, which the native application should use when preparing transactions.
Native application sends transactions
Once the signed session has been recorded onchain, the native application can then sign transactions using the session signer. These signed transactions will be validated against the recorded session payload and executed.
Here is an illustrative example of how to set up a native session for an existing Controller with Controller.c:
#include "../bindings/c/ContractPolicy.h"
#include "../bindings/c/DiplomatFelt.h"
#include "../bindings/c/DiplomatOwner.h"
#include "../bindings/c/DiplomatSessionPolicies.h"
#include "../bindings/c/DiplomatSigner.h"
#include "../bindings/c/Method.h"
#include "../bindings/c/SessionAccount.h"
#include "../bindings/c/Utils.h"
// Test private key - DO NOT USE IN PRODUCTION
DiplomatStringView private_key_view = toStringView(
"0x1234567890123456789012345678901234567890123456789012345678901234");
DiplomatFelt_new_from_hex_result pk =
DiplomatFelt_new_from_hex(private_key_view);
// Generate public key and starknet signer from private key
DiplomatFelt *public_key = Utils_get_public_key(pk.ok);
DiplomatSigner *starknet_signer = DiplomatSigner_new_starknet_signer(pk.ok);
// Build policies
DiplomatSessionPolicies *session_policies = DiplomatSessionPolicies_new();
ContractPolicy *policy =
ContractPolicy_new(noneStringViewOption(), noneStringViewOption());
Method *method =
Method_new(toStringView("transfer"),
toStringView("transfer funds from one account to the other"),
toStringView("transfer"), true, true, true);
ContractPolicy_push_method(policy, method);
DiplomatSessionPolicies_add_contract_policy(
session_policies,
toStringView(
"0x049d36570d4e46f48e99674bd3fcc84644ddd6b96f7c741b1562b82f9e004dc7"),
policy);
// Link the session to the Controller account via the browser
printf("\nPlease open a browser to this URL and create a session:\n"
"%s/session"
"?public_key=%.*s"
"&policies=%s"
"&rpc_url=%s"
"&redirect_uri=http://example.com"
"&redirect_query_name=startapp\n",
KEYCHAIN_URL, (int)writeable.len, buffer, policies, RPC_URL);
// Create session account
SessionAccount_create_from_subscribe_create_session_result ret =
SessionAccount_create_from_subscribe_create_session(
starknet_signer, session_policies,
toStringView(RPC_URL), toStringView(CARTRIDGE_API_URL));