# Cartridge Documentation > High Performance Infrastructure for Provable Games and Applications ## Marketplace The Cartridge Marketplace brings onchain assets directly to your players, enabling richer and more complex game experiences. ### Adding Assets Adding assets to your game's marketplace is simple. First, configure your game's Torii instances to index the asset, and then configure your Controller with Torii. ::::steps ##### Configure your Torii Every digital asset on the Marketplace must be indexed by Torii. The [Arcade Setup](./setup#torii-configuration) page provides detailed information about configuring Torii for various use cases including marketplace assets. For basic marketplace asset indexing, add the token address to the configuration file: ```toml # torii.toml [indexing] contracts = [ "erc20:", "erc721:" ] ``` :::info See the [Torii docs](https://book.dojoengine.org/toolchain/torii/configuration#indexing-configuration) for more information about indexing token contracts. ::: ##### Add Torii to Controller Once Torii is indexing your asset, pass the URL to your game's Controller. The URL of your Torii instance should be passed as the `slot` option: ```typescript import { Controller } from '@cartridge/controller' controller = new Controller({ // other options slot: "https://api.cartridge.gg/x/my-game/torii" }) ``` :::info See the [Torii documentation](https://book.dojoengine.org/toolchain/torii) for more information about running your own Torii instance. ::: :::warning Controller instances can only be configured with a single Torii instance, so ensure that all your Marketplace assets are indexed by the same Torii. ::: :::: ## Overview #### TL;DR: Arcade is: * A unified hub for all your favorite onchain games * A frictionless bridge between developers and players * A permissionless platform for game registration and publishing * Designed to enhance discoverability, engagement, and player experience * Leverage NFTs for ownership and control of your games and editions ![Arcade Overview](/arcade-inventory.png) ### Key Features #### 🎮 Register a Game Game studios can register a game freely, without needing permission. When creating a game for the first time, you'll be asked to provide its metadata and define at least one Game Edition. :::info A Game Edition is a version of your game --- it can be deployed on mainnet, testnet, or represent a season or special mode (e.g., Season 1, Mainnet, Playtest, etc.). ::: When you register a game or an edition, an NFT representing its ownership is minted. This NFT grants you admin rights over the game or edition. You can update the metadata of your game or any edition at any time --- including name, description, icon, image gallery, video, and more. For technical setup and configuration details, see [Arcade Setup](./setup). ![Register Game](/arcade-register-game.png) #### 🚀 Publish a Game Once your game is registered, you can publish it to request a review. After approval, the game will be whitelisted and made publicly visible to all users on the platform. ![Publish Game](/arcade-publish-game.png) #### 🧩 Create and update Game Editions You can create new editions of your game at any time --- each edition gets its own ownership NFT. As the game owner, you control which editions are visible or hidden. As the edition owner, you can choose to publish or hide your edition from public view. :::info No permission is required to register an edition in an existin game, meaning anyone can register a new edition to your game such as registering a Game onto Arcade, however you get the control on their visibility. ::: ![Update Edition](/arcade-update-edition.png) #### 🚀 Publish and whitelist an Edition As the edition owner you have the ability to publish your edition once created. Once published, the game owner has the ability to whitelist you Edition to make it public. :::info Any update within the Edition will turn off both the publish and the whitelist, the process should be repeat to make it public again ::: ![Publish Edition](/arcade-publish-edition.png) ## Setup ### Torii Configuration To provide a rich user experience in Arcade, we recommend enhancing your Torii configuration to enable live activity feeds, asset indexing, and leaderboards. This configuration also supports marketplace functionality for asset display. #### ⚡️ Activity Feed Display live player activity on your Arcade edition page. ```toml [indexing] transactions = true ``` #### 💄 Player Asset Indexing Index and display custom in-game assets tied to your players. This enables asset display in both the Arcade edition page and marketplace functionality. ```toml [indexing] contracts = [ "ERC20:0x1234...5678", "ERC721:0x1234...5678", ] ``` #### ✨ Leaderboard Integration Enable live leaderboards based on progression events or achievements (if implemented). ```toml [sql] historical = ["-TrophyProgression"] ``` Only add TrophyProgression if your game emits progression events through achievements. ## Starter Packs Starter packs let you bundle game assets and distribute them to players through a purchase flow integrated with [Cartridge Controller](/controller/starter-packs). The Arcade starter pack registry is a permissionless onchain system — anyone can register a starter pack by deploying an implementation contract and calling `register`. ### How It Works The registry follows a two-contract pattern: 1. **Implementation contract** — a contract you deploy that implements the `IStarterpackImplementation` interface. When a player purchases your starter pack, the registry calls your contract's `on_issue` function to distribute assets. 2. **Registry contract** — the Arcade registry where you register your implementation, set pricing, and configure options. ``` Player purchases → Registry collects payment → Registry calls on_issue → Your contract distributes assets ``` ### Implementation Contract Your implementation contract must expose two functions: ```cairo #[starknet::interface] trait IStarterpackImplementation { /// Called by the registry when a starter pack is issued. /// Distribute assets to the recipient here. fn on_issue( ref self: TContractState, recipient: ContractAddress, starterpack_id: u32, quantity: u32, ); /// Return the supply limit, or None for unlimited. fn supply(self: @TContractState, starterpack_id: u32) -> Option; } ``` `on_issue` is where you mint tokens, transfer NFTs, or perform any onchain action to fulfill the starter pack. The registry will only call `on_issue` after payment has been collected. :::warning `on_issue` is a public function which should typically be callable only by the registry contract. ::: #### Example: ERC721 Starter Pack A minimal implementation that mints an NFT to the recipient: ```cairo #[abi(embed_v0)] impl StarterpackImpl of IStarterpackImplementation { fn on_issue( ref self: ContractState, recipient: ContractAddress, starterpack_id: u32, quantity: u32, ) { assert(self.starterpacks.read(starterpack_id), 'Invalid starterpack'); self.mint(recipient); } fn supply(self: @ContractState, starterpack_id: u32) -> Option { Option::None // Unlimited supply } } ``` See the full example at [cartridge-gg/activations](https://github.com/cartridge-gg/activations/tree/main/erc721). ### Registering a Starter Pack Once your implementation contract is deployed, register it with the Arcade registry by calling `register`: ```cairo fn register( ref self: TContractState, implementation: ContractAddress, // Your deployed implementation contract referral_percentage: u8, // Percentage of base price paid to referrers (0-50) reissuable: bool, // Whether a player can purchase more than once price: u256, // Price per unit in payment token payment_token: ContractAddress, // ERC20 token used for payment payment_receiver: Option, // Payment destination (defaults to caller) metadata: ByteArray, // JSON metadata (see below) conditional: bool, // Whether purchases require a voucher ) -> u32; // Returns the starter pack ID ``` The returned ID is what players use to purchase the starter pack via [`controller.openStarterPack(id)`](/controller/starter-packs). ### Parameters #### `reissuable` Controls whether the same player can purchase the starter pack more than once. * `false` — each player can only purchase once, and `quantity` is forced to 1 * `true` — players can purchase multiple times with any quantity #### `referral_percentage` Percentage of the base price paid to a referrer when one is provided during purchase. Maximum is 50. Self-referrals (referrer == payer) are ignored. #### `payment_receiver` Where the base price (minus any referral fee) is sent. If `None`, payment goes to the starter pack owner (the address that called `register`). If `Some(address)`, payment goes to that address instead — useful for treasury contracts or revenue sharing. #### `conditional` When `true`, purchases require a voucher. Vouchers are granted by an admin via the `allow` function, which authorizes a specific recipient address. The player must provide the matching `voucher_key` when purchasing. #### Metadata The `metadata` parameter is a JSON string describing the starter pack for display in the UI. Use the `MetadataTrait` helper from the starterpack package to construct it: ```cairo use starterpack::types::item::ItemTrait; use starterpack::types::metadata::MetadataTrait; let sword = ItemTrait::new("Sword", "A mighty sword", "https://example.com/sword.png"); let metadata = MetadataTrait::new( name: "My Starter Pack", description: "A pack of game assets", image_uri: "https://example.com/image.png", items: [sword].span(), tokens: [].span(), // Additional payment token hints for the UI conditions: ["Must be level 5"].span(), // Display-only condition strings ).jsonify(); ``` ### Managing Starter Packs After registration, the owner can manage the starter pack: * **`update`** — change implementation, pricing, referral percentage, or other parameters * **`update_metadata`** — update the display metadata * **`pause`** / **`resume`** — temporarily disable or re-enable purchases ### Payment Flow When a player purchases a starter pack, the registry handles payment distribution: 1. **Referral fee** — if a referrer is provided, their percentage is deducted from the base price and sent to them 2. **Protocol fee** — a fee is added on top of the base price and sent to the Arcade fee receiver 3. **Owner payment** — the remaining base price (after referral fee) is sent to the `payment_receiver` or owner 4. **Asset distribution** — the registry calls `on_issue` on the implementation contract If `price` is zero, all payment steps are skipped and `on_issue` is called directly. ## Achievements The Cartridge Achievements system enables games to reward players for completing achievements with built-in progress tracking and Cartridge points. ### Key Features * **Packages**: Games can define achievements using the provided Cairo packages * **Rewards**: Games can reward players with Cartridge points for completing achievements * **Profile**: Players can view their achievements and scores without leaving the game ### Benefits for Game Developers * **Simplicity**: Easy integration with existing Starknet smart contracts and Dojo * **Cost-effectiveness**: Achievements are event-based, no additional storage is required * **Performance** (coming soon): Plugin attached to Torii to improve achievement computation performance ### How It Works Achievements consist of: * **Achievement Definition**: A unique `identifier`, `title`, `description`, and set of `tasks` * **Tasks**: Each task has an `identifier`, `total` target, and `description` * **Completion**: A task completes when enough progression has been made; an achievement completes when all its tasks are completed For the complete implementation, see the [GitHub repository](https://github.com/cartridge-gg/arcade). ### Setup #### Dependencies Add the Cartridge `achievement` package as a dependency in your Scarb.toml: ```rust [dependencies] starknet = "2.8.4" dojo = { git = "https://github.com/dojoengine/dojo", tag = "v1.5.1" } achievement = { git = "https://github.com/cartridge-gg/arcade", tag = "v1.5.1" } // [!code focus] [[target.starknet-contract]] build-external-contracts = [ "dojo::world::world_contract::world", "achievement::events::index::e_TrophyCreation", // [!code focus] "achievement::events::index::e_TrophyProgression", // [!code focus] ] ``` :::info Don't forget to add the corresponding writes while deploying your contract if not globally declared: ```toml [writers] "-TrophyCreation" = ["-Actions"] "-TrophyProgression" = ["-Actions"] ``` ::: #### Torii Configuration The progression events require historical event management by Torii, meaning every event will remain available in the `event_messages_historical` table: ```toml rpc = world_address = [indexing] ... [sql] // [!code focus] historical = ["-TrophyProgression"] // [!code focus] ``` :::info The `TrophyCreation` event doesn't need to be historical since it should only be emitted once at trophy creation. If a new `TrophyCreation` event is emitted with the same keys as an existing one, it will replace it---useful for updating trophy metadata. ::: ### Creating Achievements Emit events to define your achievements using the provided Starknet components: ```rust #[dojo::contract] pub mod Actions { use achievement::components::achievable::AchievableComponent; // [!code focus] use achievement::types::task::{Task, TaskTrait}; // [!code focus] component!(path: AchievableComponent, storage: achievable, event: AchievableEvent); // [!code focus] impl AchievableInternalImpl = AchievableComponent::InternalImpl; // [!code focus] #[storage] struct Storage { #[substorage(v0)] achievable: AchievableComponent::Storage, // [!code focus] } #[event] #[derive(Drop, starknet::Event)] enum Event { #[flat] AchievableEvent: AchievableComponent::Event, // [!code focus] } fn dojo_init(self: @ContractState) { // [Event] Emit all Achievement creation events let world = self.world(""); let task_id = 'TASK_IDENTIFIER'; let task_target = 100; let task = TaskTrait::new(task_id, task_target, "Do something 100 times"); let tasks: Span = array![task].span(); self.achievable // [!code focus] .create( // [!code focus] world, // [!code focus] id: 'ACHIEVEMENT_IDENTIFIER', // [!code focus] hidden: false, // [!code focus] index: 0, // [!code focus] points: 10, // [!code focus] start: 0, // [!code focus] end: 0, // [!code focus] group: 'Group', // [!code focus] title: "Achievement title", // [!code focus] description: "The achievement description", // [!code focus] tasks: tasks, // [!code focus] data: "", // [!code focus] icon: 'fa-trophy', // [!code focus] ); // [!code focus] } // [!code focus] } } ``` #### AchievableComponent.create Parameters ```rust AchievableComponent.create( self: @ComponentState, world: WorldStorage, id: felt252, hidden: bool, index: u8, points: u16, start: u64, end: u64, group: felt252, icon: felt252, title: felt252, description: ByteArray, tasks: Span, data: ByteArray, ) ``` | Parameter | Description | | ------------- | -------------------------------------------------------------------------- | | `id` | Unique achievement identifier | | `hidden` | Whether to hide the achievement in the controller UI | | `index` | Page index within the group for display ordering | | `points` | Cartridge points to reward the player | | `start` | Start timestamp for ephemeral achievements (`0` for everlasting) | | `end` | End timestamp for ephemeral achievements (`0` for everlasting) | | `group` | Achievement group for organizing achievements together | | `icon` | [FontAwesome](https://fontawesome.com/icons) icon name (e.g., `fa-trophy`) | | `title` | Achievement title | | `description` | Achievement description | | `tasks` | Achievement tasks (see Task type below) | | `data` | Reserved for future use | See also [AchievableComponent](https://github.com/cartridge-gg/arcade/blob/main/packages/achievement/src/components/achievable.cairo) #### Task Type ```rust pub struct Task { id: felt252, total: u32, description: ByteArray, } ``` | Parameter | Description | | ------------- | --------------------------------------------------- | | `id` | Task identifier (can be shared across achievements) | | `total` | Target count for task completion | | `description` | Task description | See also [Task](https://github.com/cartridge-gg/arcade/blob/main/packages/trophy/src/types/task.cairo) ### Tracking Progression Emit events to track player progress on tasks: ```rust #[dojo::contract] pub mod Actions { use achievement::store::{Store, StoreTrait}; // ... #[abi(embed_v0)] impl ActionsImpl of IActions { fn play(ref self: ContractState, do: felt252) { let world = self.world(@"") // If the player meets the task requirement, emit an event to track the progress if do === 'something' { let store = StoreTrait::new(world); let player_id = starknet::get_caller_address(); let task_id = 'TASK_IDENTIFIER'; let count = 1; let time = starknet::get_block_timestamp(); store.progress(player_id.into(), task_id, count, time); } } } } ``` :::info You can also use the component directly: `self.achievable.progress(world, player_id, task_id, count)` ::: #### AchievableComponent.progress Parameters ```rust AchievableComponent.progress( self: @ComponentState, world: WorldStorage, player_id: felt252, task_id: felt252, count: u32, ) ``` | Parameter | Description | | ----------- | ------------------------ | | `player_id` | The player identifier | | `task_id` | The task identifier | | `count` | Progression count to add | ### Client Integration #### Controller Configuration For policy configuration details, see [Sessions](./sessions). ```typescript new ControllerConnector({ url, rpc, profileUrl, namespace: "dopewars", // [!code focus] slot: "ryomainnet", // [!code focus] theme, colorMode, policies, }); ``` #### Opening the Achievements Page Add a button to open the achievements page in your game client: ```typescript const { connector } = useAccount(); const handleClick = useCallback(() => { if (!connector?.controller) { console.error("Connector not initialized"); return; } connector.controller.openProfile("achievements"); }, [connector]); ``` ### Testing Add the corresponding events to your namespace definition in tests: ```rust fn namespace_def() -> NamespaceDef { NamespaceDef { namespace: "namespace", resources: [ // ... TestResource::Event(achievement::events::index::e_TrophyCreation::TEST_CLASS_HASH), TestResource::Event(achievement::events::index::e_TrophyProgression::TEST_CLASS_HASH), TestResource::Contract(Actions::TEST_CLASS_HASH), ].span() }; } ``` ### Examples * [DopeWars Scarb.toml](https://github.com/cartridge-gg/dopewars/blob/mainnet/Scarb.toml) * [DopeWars Systems](https://github.com/cartridge-gg/dopewars/blob/mainnet/src/systems/ryo.cairo) * [DopeWars Progression](https://github.com/cartridge-gg/dopewars/blob/mainnet/src/systems/helpers/shopping.cairo) * [DopeWars Connect Button](https://github.com/cartridge-gg/dopewars/blob/mainnet/web/src/components/wallet/ConnectButton.tsx) ## Architecture This page provides a technical overview of the Controller smart contract for developers who need to understand the on-chain mechanisms. For user-facing documentation, see [Sessions](/controller/sessions) and [Signer Management](/controller/signer-management). ### Account Model The Controller is a smart contract wallet with support for multiple owners and flexible signer types. #### Components The account is built from modular components: | Component | Purpose | | ---------------------- | ------------------------------------------- | | **multiple\_owners** | Manages account owners (add/remove signers) | | **session** | Session-based transaction authorization | | **outside\_execution** | Meta-transactions via SNIP-9 | | **external\_owners** | External contract-based ownership | | **delegate\_account** | Account delegation support | #### Owner Management Owners are stored by their GUID (a hash identifying the signer). Adding a new owner requires a signature from the new signer to prevent accidental misconfiguration. ```cairo fn add_owner(owner: Signer, signature: SignerSignature) fn remove_owner(owner: Signer) fn is_owner(owner_guid: felt252) -> bool ``` ### Signer Types The Controller supports six cryptographic signature schemes: | Type | Description | GUID Calculation | | ------------- | ------------------------------------------ | -------------------------------------------------------------------------- | | **Starknet** | Native Starknet curve (most gas-efficient) | `poseidon('Starknet Signer', pubkey)` | | **Secp256k1** | Ethereum-compatible curve | `poseidon('Secp256k1 Signer', pubkey_hash)` | | **Secp256r1** | Hardware security module support | `poseidon('Secp256r1 Signer', pubkey.low, pubkey.high)` | | **Eip191** | Ethereum personal signatures | `poseidon('Eip191 Signer', eth_address)` | | **Webauthn** | passkey support for browsers/OS | `poseidon('Webauthn Signer', origin.len(), ...origin, rp_id_hash, pubkey)` | | **SIWS** | Sign-In With Solana (Ed25519) | `poseidon('SIWS Signer', pubkey)` | Each signer is uniquely identified by a GUID (hash of the signer data). When a signer is added, a `SignerLinked` event is emitted with the GUID and full signer data. ### Sessions (On-Chain) Sessions allow dapps to submit transactions on behalf of users without per-transaction approval. #### Session Structure ```cairo struct Session { expires_at: u64, // Expiration timestamp allowed_policies_root: felt252, // Merkle root of allowed methods metadata_hash: felt252, // Hash of session metadata JSON session_key_guid: felt252, // GUID of the session key guardian_key_guid: felt252, // GUID of the guardian key (optional) } ``` #### How It Works 1. Dapp generates a session key pair 2. User signs an off-chain message with session parameters 3. Dapp submits transactions using a `SessionToken` containing: * The session data * Session key signature over `poseidon(tx_hash, session_hash)` * Guardian signature (if guardian key is set) * Merkle proofs for each call #### Session Token Format Transactions using sessions must have signatures starting with the magic value `'session-token'`. #### Verification **On-chain checks:** * Session expiration (`expires_at > block_timestamp`) * Session not revoked * Session key signature validity * Guardian signature validity (if `guardian_key_guid != 0`) * Merkle proofs for each call against `allowed_policies_root` #### Session Management ```cairo fn revoke_session(session_hash: felt252) fn register_session(session: Session, guid_or_address: felt252) fn is_session_revoked(session_hash: felt252) -> bool fn is_session_registered(session_hash: felt252, guid_or_address: felt252) -> bool ``` #### Session Caching Sessions can cache the authorization signature to reduce transaction costs. Set `cache_authorization: true` in the session token to enable this. Subsequent transactions can then bypass authorization signature verification. #### Wildcard Policies Sessions can use `'wildcard-policy'` as the `allowed_policies_root` to allow any method call, bypassing policy checks. ### Auth Flows The following diagrams show how each provider authenticates users and signs transactions. #### ControllerProvider ![ControllerProvider flow](/controller-provider-flow.svg) #### SessionProvider ![SessionProvider flow](/session-provider-flow.svg) #### Headless ![Headless flow](/headless-flow.svg) ### Transaction Execution Flow When you call `account.execute()` in your application, the Controller SDK determines the best execution path based on your session configuration and fee source settings. #### Execution Paths The SDK supports two primary execution methods: | Method | Description | Use Case | | ------------------------ | --------------------------------------------------- | ------------------------------------------ | | **Regular Execute** | Standard Starknet transaction signed by the account | User pays gas fees | | **Execute From Outside** | Meta-transaction via SNIP-9 | paymaster-sponsored (gasless) transactions | #### How Paymastered Transactions Work When using the Cartridge paymaster (the default for session-based transactions), your `account.execute()` call is automatically converted into a meta-transaction: ``` account.execute(calls) └── trySessionExecute(calls, feeSource) └── executeFromOutsideV3(calls, feeSource) └── cartridge_addExecuteOutsideTransaction (RPC) └── paymaster submits transaction on-chain ``` The SDK: 1. Validates the session is active and session policies match 2. Constructs an `OutsideExecution` message with a 10-minute validity window 3. Signs the message with the session key 4. Sends the signed payload to Cartridge's paymaster service 5. The paymaster submits the transaction onchain and pays gas fees If the paymaster is unavailable (e.g., on local Katana), the SDK falls back to regular execution where the user pays fees. #### Developer Experience Game developers don't need to call `executeFromOutside` directly. The abstraction is handled entirely by the SDK: ```typescript // This is all you need - the SDK handles the rest const result = await account.execute([ { contractAddress: GAME_CONTRACT, entrypoint: "play_card", calldata: [cardId], }, ]); ``` The SDK automatically: * Uses `executeFromOutside` when paymaster is configured * Falls back to regular execute when paymaster isn't available * Opens the approval modal if the session is expired or session policies don't match #### Outside Execution (SNIP-9) The Controller implements [SNIP-9](https://github.com/starknet-io/SNIPs/blob/main/SNIPS/snip-9.md) for meta-transactions via `execute_from_outside_v3`. This allows external contracts or relayers to submit transactions on behalf of the account by providing valid signatures. ```cairo fn execute_from_outside_v3( outside_execution: OutsideExecution, signature: Span ) -> Array> ``` The `OutsideExecution` struct specifies: * `caller`: Who can submit (or `'ANY_CALLER'`) * `nonce`: Channel-based nonce for replay protection * `execute_after` / `execute_before`: Time window for validity * `calls`: The calls to execute For more details on outside execution, see the [Starknet.js documentation](https://starknetjs.com/docs/guides/account/outsideExecution/). ### Recovery (Threshold-Based) Recovery is implemented as a separate component (`threshold_recovery_component`) for accounts with multiple signers. When enabled, `threshold - 1` signers can initiate recovery to replace a signer. #### Escape Flow 1. **Trigger**: `threshold - 1` signers call `trigger_escape` with target and new signer 2. **Wait**: Security period must elapse (configurable) 3. **Execute**: `threshold - 1` signers call `execute_escape` to complete 4. **Expiry**: If not executed within expiry period, escape expires #### Configuration ```cairo fn toggle_escape(is_enabled: bool, security_period: u64, expiry_period: u64) fn get_escape_enabled() -> EscapeEnabled fn get_escape() -> (Escape, EscapeStatus) ``` #### Escape States | Status | Description | | ---------- | -------------------------------------------------- | | `None` | No escape triggered | | `NotReady` | Escape triggered, waiting for security period | | `Ready` | Security period elapsed, can be executed | | `Expired` | Execution window passed, must cancel or re-trigger | #### Override Rules A new escape can override an existing one only if it targets a signer with lower priority in the signer list. ### Upgrades The account can be upgraded via the standard OpenZeppelin `upgrade` function, requiring owner authorization. ```cairo fn upgrade(new_class_hash: ClassHash) ``` ### Source Code For full implementation details, see the [controller-cairo repository](https://github.com/cartridge-gg/controller/tree/main/packages/contracts). ## Booster Packs Booster packs are a reward distribution system that allows eligible users to claim various game assets, credits, and exclusive game passes. Unlike starter packs which are purchased, booster packs are claimed for free by users who meet specific eligibility criteria, often based on holding certain NFTs or participating in events. ### Overview Booster packs enable: * **Free Asset Claims**: Eligible users can claim rewards without payment * **Cross-Chain Eligibility**: Check eligibility using Ethereum addresses from various events or NFT holdings * **Multiple Reward Types**: Claim fungible tokens (LORDS, NUMS, PAPER), game credits, or exclusive game passes * **Merkle Drop Technology**: Secure, verifiable claiming using cryptographic proofs * **Game Integration**: Special game passes for supported games like Loot Survivor 2 and NUMS * **Animated Reveals**: Interactive UI with card reveals for mystery asset types ### How Booster Packs Work #### Eligibility Verification Booster packs use an eligibility system where users must meet specific criteria to claim rewards: ```typescript // Check if an Ethereum address is eligible for booster pack rewards const eligibilityResponse = await checkAssetEligibility(ethereumAddress); // Returns: { value: number, type: string } (e.g., { value: 150, type: "credits" }) ``` #### Supported Reward Types Booster packs can contain various types of rewards: **Fungible Tokens:** * `CREDITS`: Platform credits for gasless transactions (typically 150) * `LORDS`: Realms ecosystem token (75 tokens) * `NUMS`: NUMS game token (2000 tokens) * `PAPER`: Dope Wars token (3000 tokens) * `SURVIVOR`: Loot Survivor token (10 tokens) **Game Passes:** * `LS2_GAME`: Loot Survivor 2 exclusive game pass * `NUMS_GAME`: NUMS game pass **Special Types:** * `MYSTERY_ASSET`: Contains multiple random game passes with reveal animation #### Claim Process The claiming process involves several steps: 1. **Eligibility Check**: System verifies if the user's Ethereum address qualifies for rewards 2. **Authentication**: User must be connected to their Cartridge account 3. **Merkle Proof Validation**: Claims are validated using merkle tree proofs 4. **Asset Distribution**: Eligible rewards are distributed to the user's Cartridge account 5. **Animation**: For mystery assets, an interactive reveal animation shows claimed game passes ### Technical Implementation #### Credit Claims API For credit-type booster packs, a special API call is made to grant credits: ```typescript interface ClaimCreditsMessage { account_username: string; amount: string; // hex format (e.g., "0x96" for 150) } interface ClaimCreditsRequest { account_username: string; message: ClaimCreditsMessage; signature: string; // EIP-191 signature from private key } // Example claim flow const message: ClaimCreditsMessage = { account_username: "player123", amount: "0x96", // 150 credits in hex }; const signature = await signClaimMessage(privateKey, message); const response = await claimBoosterCredits({ account_username: "player123", message, signature, }); ``` #### Merkle Drop Integration Booster packs use the same Merkle Drop technology as claimable starter packs: * **Cryptographic Proofs**: Claims are validated using merkle proofs * **Cross-Chain Support**: Eligibility can originate from multiple blockchain networks * **Signature Verification**: EIP-191 signatures verify ownership of claiming addresses * **Forwarder Contracts**: Assets are distributed through verified smart contracts ### User Experience #### Booster Pack Interface When users access a booster pack: 1. **Loading State**: System checks asset eligibility for the provided address 2. **Asset Preview**: Displays the eligible reward type with appropriate imagery 3. **Connection Flow**: If not authenticated, redirects to connect their Cartridge account 4. **Claim Button**: Single-click claiming once eligibility and authentication are confirmed 5. **Success State**: Shows claimed status with options to use rewards #### Mystery Asset Reveals For mystery asset booster packs, users experience: * **Suspenseful Animation**: 2-second delay before reveal begins * **Sequential Card Reveals**: Multiple game passes revealed one by one * **Confetti Effects**: Celebratory visual feedback during reveals * **Interactive Cards**: Claimed game passes become clickable to launch games #### Game Integration Claimed game passes integrate directly with supported games: * **Loot Survivor 2**: Game passes provide tournament entry tokens * **NUMS**: Access to specific game modes or levels * **Automatic Launch**: Claimed passes can directly open the associated game ### Error Handling Common error scenarios and their handling: ```typescript try { await claimBoosterCredits(request); } catch (error) { // Handle specific error cases if (error.message.includes("already claimed")) { // User has already claimed this booster pack } else if (error.message.includes("not eligible")) { // Address doesn't meet eligibility criteria } else if (error.message.includes("Account not found")) { // User's Cartridge account couldn't be found } } ``` ### Integration Notes #### Asset Eligibility Asset eligibility is typically determined by: * **NFT Holdings**: Owning specific NFTs or collections * **Event Participation**: Participating in airdrops, campaigns, or events * **Whitelist Inclusion**: Being included in predetermined distribution lists * **Time-Based Claims**: Meeting criteria during specific time windows #### Security Considerations * **Private Key Signing**: Claims require cryptographic signatures proving address ownership * **One-Time Claims**: Most booster packs can only be claimed once per eligible address * **Server Validation**: Backend services verify merkle proofs and prevent double-spending * **Rate Limiting**: API endpoints include protection against abuse ### Differences from Starter Packs While both use Merkle Drop technology, booster packs differ from starter packs in key ways: | Feature | Booster Packs | Starter Packs | | ----------------- | -------------------------- | -------------------- | | **Cost** | Free (for eligible users) | Paid or free | | **Eligibility** | Based on external criteria | Open to all users | | **Content** | Predetermined rewards | Customizable bundles | | **UI Experience** | Claim-focused with reveals | Purchase-focused | | **Integration** | Event/campaign-based | Game monetization | ### Related Documentation * [Starter Packs](./starter-packs) - For purchasable asset bundles * [Sessions](./sessions) - For gasless gaming experiences using claimed credits * [Achievements](./achievements) - For other reward and progression systems ### Getting Help If you encounter issues with booster pack integration: * Verify eligibility criteria are met for the claiming address * Check that the user's Cartridge account is properly authenticated * Ensure merkle proofs are valid and haven't expired * Review browser console for detailed error messages * Confirm API endpoints are accessible and responding correctly ## Coinbase Onramp Integration Controller v0.12.0 introduces integrated Coinbase onramp functionality, enabling users to purchase cryptocurrency directly within the keychain interface using fiat payment methods. This streamlines the user experience by eliminating the need to exit your application to acquire crypto for gaming transactions. ### Overview The Coinbase onramp integration provides: * **Direct Fiat-to-Crypto**: Users can buy cryptocurrency using Apple Pay (for eligible US users) and other Coinbase-supported fiat payment methods * **Automatic IP Detection**: Client IP detection for compliance with regional restrictions and optimal user experience * **Order Management**: Complete order lifecycle tracking from creation to completion * **Transaction Queries**: Real-time status updates and transaction monitoring * **Seamless Integration**: Built into the existing purchase flows for starter packs and credit purchases ### useCoinbase Hook The keychain provides a `useCoinbase` hook for managing Coinbase onramp operations. This hook is used internally by the purchase flows and provides comprehensive order management capabilities. #### Hook Features The `useCoinbase` hook includes functionality for: * **Order Creation**: Initialize new fiat-to-crypto purchase orders * **Real-time Quote Fetching**: Get up-to-date pricing and fee breakdowns via `getQuote` * **Cost Transparency**: Detailed fee breakdown showing Coinbase fees and cross-chain bridging costs * **Transaction Monitoring**: Query transaction status and completion * **Sandbox Mode**: Automatic toggling between production and sandbox environments based on network * **Requirement Checks**: Verify user eligibility and regional compliance * **IP Detection**: Automatic client IP detection for regulatory compliance * **Order Management**: Enhanced order fetching by IDs for improved order tracking and management #### Integration Points The Coinbase onramp is integrated into the existing purchase flows: 1. **Starter Pack Purchases**: Available as a payment option in the wallet selection drawer; Apple Pay is shown by default for eligible US users 2. **Credit Purchases**: Integrated into the credit purchase interface 3. **Automatic Flow Management**: Seamlessly handles the transition from fiat payment to crypto receipt ### Enhanced Cost Transparency Starting with the latest updates, Coinbase onramp provides detailed cost breakdowns for enhanced transparency: * **Real-time Quote Fetching**: Automatic retrieval of current pricing and fees when Apple Pay is selected * **Detailed Fee Breakdown**: Separate display of Coinbase service fees and cross-chain bridging costs * **Dynamic Updates**: Quotes automatically refresh when purchase quantity changes * **Total Cost Display**: Clear presentation of the final amount to be charged #### Fee Structure Visibility The enhanced cost breakdown shows: * **Base Price**: The core cost of the items being purchased * **Protocol Fee**: Platform service fees * **Coinbase Fee**: Service fees charged by Coinbase for fiat-to-crypto conversion * **Bridge Fee**: Layerswap fees for cross-chain bridging to Starknet * **Final Total**: The complete amount charged to the user's payment method ### User Experience Flow When users select Coinbase onramp as their payment method: 1. **Selection**: User chooses Coinbase onramp from available payment options 2. **Compliance Check**: Automatic verification of regional availability and user eligibility 3. **Cost Breakdown**: Comprehensive cost breakdown display showing detailed fee structure including Coinbase fees and bridge fees with real-time pricing from Coinbase Onramp API 4. **Apple Pay Integration**: Streamlined starter pack Apple Pay checkout functionality for mobile payments (iOS) 5. **Limit Verification** (if needed): Users exceeding spending limits can verify their identity in-place to upgrade limits 6. **Order Creation**: Enhanced Coinbase order creation with `createCoinbaseLayerswapOrder` for improved processing 7. **Payment Processing**: User completes fiat payment through Coinbase's secure interface 8. **Transaction Monitoring**: Real-time tracking of crypto purchase and delivery with order fetching by IDs 9. **Completion**: Cryptocurrency is delivered to user's wallet for use in game purchases ### Regional Availability Apple Pay uses the same regional eligibility checks as Coinbase onramp and is shown by default in the onchain wallet selection drawer for eligible US users. Users outside supported regions will not see fiat payment options. Coinbase onramp availability varies by region based on: * **Regulatory Requirements**: Compliance with local financial regulations * **Supported Payment Methods**: Available banking and payment options in user's region * **Service Coverage**: Coinbase's operational coverage areas The integration automatically detects user location via IP geolocation to determine service availability and present appropriate options. ### Benefits for Game Developers Integrating Coinbase onramp provides several advantages: * **Reduced Friction**: Users can acquire crypto without leaving your application * **Enhanced UX**: Verification autofill functionality improves user experience during authentication flows * **Mobile Optimization**: Apple Pay checkout functionality provides streamlined mobile payment experience * **Higher Conversion**: Simplified path from fiat to game purchases * **Enhanced Transparency**: Real-time fee breakdowns build user trust and reduce abandonment * **Broader Audience**: Serves users who don't already own cryptocurrency * **Seamless Experience**: Integrated directly into existing purchase flows * **Compliance Handled**: Automatic regional restriction management * **Transparent Pricing**: Real-time cost breakdown with detailed fee structures ### Security and Compliance The Coinbase onramp integration maintains strict security standards: * **KYC/AML Compliance**: Handled entirely by Coinbase's regulated infrastructure * **Secure Transactions**: All fiat payments processed through Coinbase's secure payment systems * **Regional Compliance**: Automatic compliance with local financial regulations * **IP Detection**: Client IP validation for geographical restrictions ### Error Handling The integration includes comprehensive error handling for: * **Service Unavailability**: Graceful degradation when Coinbase onramp is not available in user's region * **Order Failures**: Clear messaging and alternative payment options when orders cannot be completed * **Network Issues**: Retry logic and fallback options for connectivity problems * **Compliance Blocks**: Appropriate messaging when regulatory restrictions apply ### Development Testing When testing Coinbase onramp integration: * **Sandbox Environment**: Use Coinbase's sandbox environment for development testing (automatically enabled on testnets) * **Quote Testing**: Verify quote fetching functionality and fee breakdown display * **Regional Testing**: Test from different IP locations to verify regional behavior * **Error Scenarios**: Test error conditions and fallback flows * **Mobile Testing**: Verify mobile experience and payment flows * **Cost Breakdown UI**: Test fee transparency components with various purchase amounts :::note Coinbase onramp integration is automatically included in Controller v0.12.0+ and does not require additional configuration for basic usage. Enhanced features including Apple Pay checkout, comprehensive cost breakdown, and improved order management are available in v0.12.2+. The latest version v0.12.3 includes additional sandbox configuration improvements and bug fixes for enhanced reliability. ::: ### Next Steps * Learn about [Starter Pack Integration](/controller/starter-packs) for complete purchase flows * Review [Configuration Options](/controller/configuration) for customization * Explore [Credit Purchases](/controller/starter-packs#credit-purchases) for account top-ups * See [Cross-Chain Payments](/controller/starter-packs#cross-chain-bridging-with-layerswap) for alternative payment methods ## Configuration Controller provides several configuration options related to chains, sessions, and theming. ### ControllerOptions ```typescript export type Chain = { rpcUrl: string; }; export type ControllerOptions = { // Chain configuration chains?: Chain[]; // Custom RPC endpoints (takes precedence over default chains) defaultChainId?: string; // Default chain to use (hex encoded). If using Starknet React, this gets overridden by the same param in StarknetConfig // Session options policies?: SessionPolicies; // Optional: Session policies for pre-approved transactions propagateSessionErrors?: boolean; // Propagate transaction errors back to caller instead of showing keychain UI errorDisplayMode?: "modal" | "notification" | "silent"; // How to display transaction/execution errors. Defaults to "modal" // Performance options lazyload?: boolean; // When true, defer iframe mounting until connect() is called. Reduces initial load time and resource fetching // Keychain options url?: string; // The URL of keychain origin?: string; // The origin of keychain starterPackId?: string; // The ID of the starter pack to use feeSource?: FeeSource; // The fee source to use for execute from outside signupOptions?: AuthOptions; // Signup options (order reflects UI. Group socials and wallets together). With one option configured, submit buttons show branded styling shouldOverridePresetPolicies?: boolean; // When true, manually provided policies override preset policies. Default is false namespace?: string; // The namespace to use to fetch trophies data from indexer tokens?: Tokens; // The tokens to be listed on Inventory modal // Customization options preset?: string; // Preset name for custom themes and verified policies slot?: string; // Torii instance URL for custom asset indexing }; ``` :::warning **Policy Precedence Behavior:** When both `preset` and `policies` are provided: * If `shouldOverridePresetPolicies: true` → uses manual policies * If preset has policies for the current chain → uses preset policies (ignores manual policies) * If preset has no policies for the current chain → falls back to manual policies To guarantee manual policies take precedence, set `shouldOverridePresetPolicies: true`. ::: ### Chain Configuration Controller provides default Cartridge RPC endpoints for Starknet mainnet and sepolia networks: * `https://api.cartridge.gg/x/starknet/mainnet` * `https://api.cartridge.gg/x/starknet/sepolia` When you provide custom chains via the `chains` option, they take precedence over the default Cartridge chains if they specify the same network. This allows you to: * Use custom RPC endpoints for mainnet or sepolia * Add support for additional networks (like local Katana instances) * Override default chain configurations * Programmatically switch chains for connected external wallets (MetaMask, Rabby, WalletConnect) #### Network Switching Controller supports multiple methods for switching between different blockchain networks: ##### External Wallet Chain Switching For external wallets, Controller supports programmatic chain switching through the `externalSwitchChain` method. This allows applications to request connected external wallets to switch to different blockchain networks seamlessly. **Supported Wallets**: MetaMask, Rabby, Base, WalletConnect (desktop only) **Not Supported**: Braavos (does not support the `wallet_switchStarknetChain` API) > **Note**: Ethereum-based external wallets are only available on desktop browsers. Mobile devices automatically disable these wallets to provide better mobile user experience. ##### Dynamic RPC URL Override The keychain interface supports dynamic RPC URL switching via URL parameters, allowing users to connect to different networks without page reloads: ``` https://x.cartridge.gg/?rpc_url=https://custom-rpc-endpoint.com ``` When an RPC URL parameter is provided, the keychain will: * Detect the chain ID from the new RPC endpoint * Recreate the controller instance with the new network configuration * Update all network-related state without requiring a page reload This feature is particularly useful for testing against development and staging environments and connecting to custom Starknet deployments. **Example:** ```typescript const controller = new Controller({ chains: [ { rpcUrl: "https://api.cartridge.gg/x/my-game/sepolia" }, // Overrides default sepolia { rpcUrl: "http://localhost:5050" }, // Adds local development chain ], chainId: constants.StarknetChainId.SN_SEPOLIA, }); ``` #### Using Katana for Local Development When developing locally with [Katana](https://book.dojoengine.org/toolchain/katana), you need to define a custom chain and configure the RPC provider to point to your local Katana. :::warning If the chain configuration does not match your running Katana instance (wrong chain ID, wrong RPC URL, or mismatched network settings), Controller will not be able to execute transactions. ::: Here is an example using `starknet-react` with a local Katana chain: ```typescript import { Chain } from "@starknet-react/chains"; import { jsonRpcProvider } from "@starknet-react/core"; const KATANA_CHAIN_ID = "0x4b4154414e41"; // "KATANA" hex-encoded ASCII const KATANA_RPC_URL = "http://localhost:5050" // Define the Katana chain const katana: Chain = { id: BigInt(KATANA_CHAIN_ID), name: "Katana", network: "katana", testnet: true, nativeCurrency: { address: "0x04718f5a0fc34cc1af16a1cdee98ffb20c31f5cd61d6ab07201858f4287c938d", // Katana default name: "Stark", symbol: "STRK", decimals: 18, }, rpcUrls: { default: { http: [KATANA_URL] }, public: { http: [KATANA_URL] }, }, }; // Configure the RPC provider const provider = jsonRpcProvider({ rpc: () => ({ nodeUrl: KATANA_URL }), }); // Set up StarknetConfig with Katana ``` :::warning Controller must be instantiated with [session policies](/controller/sessions) to work correctly on Katana. Without policies, transactions will fail because Katana does not support the paymaster flow used for manual approvals. ::: Common issues to check: * **Missing policies**: Controller requires session policies when used with Katana. See [session configuration](/controller/sessions) for details. * **RPC URL**: Katana defaults to `http://localhost:5050`. If you've configured a different port, update accordingly. * **Native currency address**: The STRK token address must match the token deployed on your Katana instance. ### Performance Optimization #### Lazy Loading The `lazyload` option allows you to defer iframe mounting until `connect()` is called. This can significantly reduce initial load time and prevent unnecessary resource fetching when the controller is instantiated but not immediately used. **Example:** ```typescript const controller = new Controller({ lazyload: true, // Iframe is created only when connect() is called // ... other options }); // No iframe is created yet - faster initial load await controller.connect(); // Iframe is created and mounted now ``` **When to use lazy loading:** * Applications where the controller might not be used immediately * Performance-critical scenarios where reducing initial bundle execution time matters * Mobile applications where resource conservation is important **When not to use lazy loading:** * Applications that need immediate controller availability * When the slight delay during first connect() is unacceptable ### Error Handling #### Propagate Session Errors The `propagateSessionErrors` option controls how contract execution errors are handled when using session-based transactions. When enabled, errors are returned directly to your application instead of showing the manual approval modal in the keychain. **Example:** ```typescript const controller = new Controller({ policies: { // ... your session policies }, propagateSessionErrors: true, // Enable error propagation }); ``` ##### How Error Propagation Works **With `propagateSessionErrors: false` (default behavior):** * Contract execution errors trigger the keychain's manual approval UI * Users see an error screen with retry/cancel options * Application receives `USER_INTERACTION_REQUIRED` response * Transaction flow continues through the keychain interface **With `propagateSessionErrors: true`:** * Contract execution errors are returned directly to your application * No keychain UI interruption for certain error types * Application receives detailed error information for handling * Users stay in your application's error handling flow ##### Error Types and Behavior | Error Type | Propagated? | Behavior | | ------------------------- | ----------- | --------------------------------------------- | | Contract revert/failure | ✅ Yes | Returned as `ERROR` response with details | | Insufficient balance | ✅ Yes | Returned as `ERROR` response with details | | General execution errors | ✅ Yes | Returned as `ERROR` response with details | | Session refresh required | ❌ No | Still shows keychain UI for re-authentication | | Manual execution required | ❌ No | Still shows keychain UI for user approval | ##### Implementation Example ```typescript import { Controller, ResponseCodes } from '@cartridge/controller'; const controller = new Controller({ policies: { // ... your policies }, propagateSessionErrors: true, }); const account = await controller.connect(); try { const result = await account.execute([ { contractAddress: "0x123...", entrypoint: "transfer", calldata: ["0x456...", "1000000000000000000"] // 1 ETH } ]); if (result.code === ResponseCodes.SUCCESS) { console.log('Transaction successful:', result.transaction_hash); } else if (result.code === ResponseCodes.ERROR) { // Handle the error in your application console.error('Transaction failed:', result.message); console.error('Error details:', result.error); // Show custom error UI to user showCustomErrorMessage(result.message); } else if (result.code === ResponseCodes.USER_INTERACTION_REQUIRED) { // User interaction still required (session refresh, manual approval, etc.) console.log('Redirecting to keychain for user action'); } } catch (error) { console.error('Unexpected error:', error); } ``` ##### When to Use Error Propagation **Use `propagateSessionErrors: true` when:** * You want to handle transaction errors with custom UI * Building games where keychain UI interruptions break immersion * You need programmatic access to detailed error information * Your application has sophisticated error handling and retry logic **Use default behavior (`false`) when:** * You're okay with keychain handling errors * You prefer built-in error UI and retry mechanisms * Building simple applications without custom error flows * You want users to have consistent error experiences across all apps ##### Error Response Format When `propagateSessionErrors` is enabled, error responses include: ```typescript { code: ResponseCodes.ERROR, message: string, // Human-readable error message error: { code: ErrorCode, // Specific error code from controller message: string, // Detailed error message data?: any, // Additional error context (e.g., execution details) } } ``` ### Configuration Categories The configuration options are organized into several categories: * **Chain Options**: Core network configuration and chain settings * [**Session Options**](/controller/sessions): Session policies, transaction-related settings, and error handling * **Performance Options**: Lazy loading and other performance optimizations * **Keychain Options**: Authentication, signup flow, and keychain-specific settings * **Customization Options**: [Presets](/controller/presets) for themes and verified policies, [Torii indexing](/controller/inventory) for custom assets ### Error Display Modes Controller provides configurable error handling through the `errorDisplayMode` option, allowing you to control how transaction and execution errors are presented to users. This gives you fine-grained control over the user experience during error scenarios. #### Available Modes ##### Modal (Default) The default error handling behavior that displays errors in a modal dialog: ```typescript const controller = new Controller({ errorDisplayMode: "modal", // Can be omitted since this is the default }); ``` **Behavior:** * Displays transaction errors in a modal overlay * Blocks user interaction until dismissed * Provides detailed error information * Best for applications where users need to understand and resolve errors ##### Notification Displays errors as clickable toast notifications: ```typescript const controller = new Controller({ errorDisplayMode: "notification", }); ``` **Behavior:** * Shows errors as [toast notifications](/controller/toast-notifications) * Non-blocking - users can continue interacting with the application * Auto-dismisses after a few seconds * Clickable for more details * Best for applications where errors shouldn't interrupt gameplay flow ##### Silent Suppresses error UI and only logs errors to the console: ```typescript const controller = new Controller({ errorDisplayMode: "silent", }); ``` **Behavior:** * No visual error display to users * Errors are logged to the browser console * Application must handle error feedback through other means * Best for applications with custom error handling or where errors are handled programmatically #### Special Cases ##### USER\_INTERACTION\_REQUIRED Errors Certain errors that require user interaction (such as session refresh or manual execution approval) **always show modal UI** regardless of the `errorDisplayMode` setting. This ensures critical authentication and approval flows are not bypassed. Examples of errors that always show modals: * Session refresh required * Manual execution approval needed * Authentication failures #### Error Handling Examples **Gaming Application with Non-Blocking Errors:** ```typescript const gameController = new Controller({ errorDisplayMode: "notification", // Don't interrupt gameplay policies: { // Game-specific policies }, }); // Transaction errors appear as toast notifications // Players can continue playing while being aware of issues ``` **Financial Application with Detailed Error Handling:** ```typescript const financeController = new Controller({ errorDisplayMode: "modal", // Show detailed errors }); // Critical transaction errors require user acknowledgment // Users must understand what went wrong before proceeding ``` **Custom Error Handling Application:** ```typescript const customController = new Controller({ errorDisplayMode: "silent", propagateSessionErrors: true, // Enable error propagation }); // Handle errors programmatically try { await customController.account.execute(calls); } catch (error) { // Custom error handling logic showCustomErrorUI(error); } ``` #### Integration with Toast Notifications When using `errorDisplayMode: "notification"`, errors are displayed using Controller's built-in [toast notification system](/controller/toast-notifications). This provides: * Consistent styling with your controller preset * Cross-iframe compatibility * Automatic positioning and duration management * Integration with other game notifications #### Best Practices **Choose the Right Mode:** * **Modal**: Financial apps, critical operations, or when users need detailed error information * **Notification**: Games, real-time applications, or when errors shouldn't interrupt user flow * **Silent**: Applications with custom error handling or sophisticated error management systems **Error Handling Strategy:** ```typescript // Recommended: Combine error modes with propagation for flexibility const controller = new Controller({ errorDisplayMode: "notification", // User-friendly notifications propagateSessionErrors: true, // Also handle programmatically }); // Handle both automatic notifications and custom logic try { await controller.account.execute(calls); // Success handling } catch (error) { // Additional custom error handling if needed if (error.code === 'CRITICAL_ERROR') { showCriticalErrorModal(error); } } ``` ### When to Use Policies **Policies are optional** in Cartridge Controller. Choose based on your application's needs: #### Use Policies When: * Building games that need frequent, seamless transactions * You want gasless transactions via Cartridge Paymaster * Users should not be interrupted with approval prompts during gameplay * You need session-based authorization for better UX #### Skip Policies When: * Building simple applications with occasional transactions * Manual approval for each transaction is acceptable * You don't need gasless transaction capabilities * You want minimal setup complexity ```typescript // Without policies - simple setup, manual approvals const simpleController = new Controller(); // With policies - session-based, gasless transactions const sessionController = new Controller({ policies: { // ... policy definitions } }); ``` ### Dynamic Authentication Options Controller supports dynamic authentication configuration on a per-connection basis. This enables multiple branded authentication flows while using a single Controller instance. #### Per-Connection signupOptions Override The `connect()` method now accepts an optional `signupOptions` parameter that overrides the constructor defaults: ```typescript import Controller from "@cartridge/controller"; const controller = new Controller({ signupOptions: ["webauthn", "google", "discord"] // Default options }); // Use default signupOptions const account1 = await controller.connect(); // Override with specific options for branded flows const account2 = await controller.connect(["phantom-evm"]); const account3 = await controller.connect(["google"]); const account4 = await controller.connect(["discord"]); ``` #### ControllerConnector Dynamic Options The `ControllerConnector` also supports dynamic authentication options: ```typescript import { ControllerConnector } from "@cartridge/connector"; const connector = new ControllerConnector({ signupOptions: ["webauthn", "google", "discord"] // Default options }); // Use default options await connector.connect(); // Override with specific options and chain hint await connector.connect({ signupOptions: ["phantom-evm"], chainIdHint: BigInt(constants.StarknetChainId.SN_MAIN) }); ``` #### Use Cases Dynamic authentication options enable several powerful patterns: **Branded Authentication Buttons** ```typescript // Create multiple specific authentication flows ``` **Platform-Specific Authentication** ```typescript // Mobile-optimized authentication (remove external wallets) const isMobile = /iPhone|iPad|iPod|Android/i.test(navigator.userAgent); const authOptions = isMobile ? ["webauthn", "google", "discord"] : ["webauthn", "google", "discord", "metamask", "walletconnect"]; await controller.connect(authOptions); ``` **Conditional Authentication Flows** ```typescript // Different options based on user preferences or game context const isNewUser = !localStorage.getItem("returning_user"); const authOptions = isNewUser ? ["google", "discord"] // Simplified for first-time users : ["webauthn", "google", "discord", "metamask"]; // All options for experienced users await controller.connect(authOptions); ``` #### Benefits * **Single Instance**: Use one Controller/Connector for multiple authentication methods * **Branded UI**: Create specific authentication buttons for different providers * **Flexible UX**: Adapt authentication options based on context or user preferences * **Override Capability**: Per-connection options override constructor defaults ### Purchase Methods Controller includes built-in methods for opening purchase interfaces: #### openPurchaseCredits() Opens the credit purchase flow, allowing users to buy credits for gasless transactions and platform services. ```typescript controller.openPurchaseCredits(); ``` #### openStarterPack(starterpackId: string) Opens the starterpack purchase interface for a specific bundle. ```typescript controller.openStarterPack("starterpack-id-123"); ``` Both methods support: * Cryptocurrency payments across multiple networks (Starknet, Base, Arbitrum, Optimism) * Integration with popular wallets (Argent, Braavos, MetaMask, Rabby) * Coinbase onramp for fiat-to-crypto purchases For detailed integration guidance, see the [Starter Packs](/controller/starter-packs) guide. #### openLocationPrompt(options?: LocationPromptOptions) Opens the location verification interface, requesting user consent to access their geolocation. Returns coordinates for location-based verification. ```typescript type LocationPromptOptions = { returnTo?: string; // Optional path to navigate to after completion (standalone mode) }; type LocationPromptReply = { code: ResponseCodes.SUCCESS; location: { latitude: number; longitude: number; accuracy: number; altitude?: number | null; altitudeAccuracy?: number | null; heading?: number | null; speed?: number | null; timestamp: number; }; }; const response = await controller.openLocationPrompt(); if (response?.code === ResponseCodes.SUCCESS) { console.log('Location:', response.location.latitude, response.location.longitude); } ``` This method is useful for geo-gating or location-based verification in applications. ### Standalone Authentication Controller supports **standalone authentication flows** that establish first-party storage access for seamless cross-domain gameplay. This is particularly useful for games that redirect users between different domains (e.g., from game launcher to game client). #### controller.open() The `open()` method redirects users to the keychain in standalone mode, establishing first-party storage access that enables seamless iframe authentication across all game domains. ```typescript type OpenOptions = { redirectUrl?: string; // URL to redirect to after authentication (defaults to current page) }; controller.open(options?: OpenOptions); ``` **Example Usage:** ```typescript // Redirect to keychain for authentication, then return to current page controller.open(); // Redirect to keychain, then redirect to a specific game URL controller.open({ redirectUrl: "https://my-game.com/play" }); ``` #### How Standalone Authentication Works The standalone authentication flow follows this pattern: 1. **Application calls `controller.open()`** - User is redirected to the keychain in first-party context 2. **User authenticates** - Keychain establishes first-party storage and session state 3. **Keychain redirects back** - User returns to the application with `controller_standalone=1` parameter 4. **Controller detects return** - Automatically requests Storage Access API permissions for iframe 5. **Seamless iframe access** - All subsequent controller operations work seamlessly across domains #### Storage Access Management Controller automatically manages the Storage Access API to enable cross-domain iframe functionality: ##### Automatic Storage Access Detection ```typescript // Check if keychain iframe has first-party storage access const hasAccess = await controller.hasFirstPartyAccess(); if (!hasAccess) { // Redirect to standalone auth flow controller.open(); } ``` ##### Manual Storage Access Control The keychain iframe also exposes a `requestStorageAccess()` method for manual control: ```typescript // Request storage access from within the keychain iframe context await keychain.requestStorageAccess(); ``` #### URL Parameter Handling Controller automatically handles several URL parameters related to authentication flows: * **`controller_standalone=1`** - Indicates successful completion of standalone auth flow * **`controller_redirect`** - Triggers automatic redirect to keychain for authentication * **`lastUsedConnector=controller`** - Backwards compatibility parameter for framework detection These parameters are automatically cleaned from the URL after processing to maintain clean application URLs. #### Cross-Domain Game Integration The standalone authentication pattern is particularly powerful for games that operate across multiple domains: **Example: Game Launcher → Game Client Flow** ```typescript // In game launcher (launcher.example.com) const controller = new Controller(); // Check if user needs authentication const account = await controller.probe(); if (!account) { // Redirect to keychain, then to game client controller.open({ redirectUrl: "https://game.example.com/play" }); return; } // User is authenticated, can redirect directly to game window.location.href = "https://game.example.com/play"; ``` ```typescript // In game client (game.example.com) const controller = new Controller(); // Controller automatically detects return from standalone auth // and requests storage access for seamless iframe operations const account = await controller.probe(); // Works seamlessly ``` #### Security Considerations The standalone authentication flow includes several security measures: * **URL validation** - Redirect URLs are validated to prevent open redirect attacks * **Protocol restrictions** - Only `http:` and `https:` protocols are allowed * **Localhost restrictions** - Localhost redirects are blocked in production environments * **Domain validation** - Redirect URLs must have valid hostnames ### Dynamic Authentication Options #### Dynamic `signupOptions` Override The `signupOptions` can be dynamically overridden on a per-connection basis, enabling developers to create multiple branded authentication flows using a single Controller instance: ```typescript // Constructor configuration sets the default options const controller = new Controller({ signupOptions: ["webauthn", "google", "metamask"], // Default options }); // Override options per connection for branded flows await controller.connect({ signupOptions: ["phantom-evm"] // Only Phantom for this specific connection }); // Different branded flow for the same controller instance await controller.connect({ signupOptions: ["google"] // Only Google for this connection }); ``` This pattern enables applications to create branded authentication experiences like "Login with Phantom" and "Login with Google" using the same Controller instance, perfect for supporting multiple wallet types or creating game-specific authentication flows. ### Branded Submit Buttons When `signupOptions` contains only a single authentication method (either in constructor or dynamically via `connect()`), Controller automatically displays branded submit buttons with: * **Signer icon**: Visual representation of the authentication method (e.g., Phantom icon, Google icon) * **Brand background color**: Themed background matching the signer's brand colors * **Branded text**: Context-aware text like "log in with Phantom" or "sign up with Google" #### Single Signer Configuration ```typescript // Single signer configuration enables branded buttons const controller = new Controller({ signupOptions: ["phantom-evm"], // Only Phantom EVM authentication }); // Users will see "sign up with Phantom" button with Phantom icon and branding ``` #### Dynamic Single Signer Configuration ```typescript // Dynamic override also enables branded buttons const controller = new Controller({ signupOptions: ["webauthn", "google", "metamask"], // Multiple default options }); // This connection will show branded Phantom button await controller.connect({ signupOptions: ["phantom-evm"] // Single option override }); ``` #### Multiple Signer Configuration ```typescript // Multiple signers show generic buttons const controller = new Controller({ signupOptions: ["webauthn", "google", "metamask"], // Multiple options }); // Users will see generic "log in" or "sign up" buttons ``` #### Supported Branded Signers The following authentication methods support branded styling: * **webauthn**: Passkey icon with default styling * **google**: Google icon with white background * **discord**: Discord icon with Discord purple background * **twitter**: Twitter/X icon with themed background * **metamask**: MetaMask icon with orange background * **phantom**: Phantom icon with purple background * **phantom-evm**: Phantom icon with purple background * **password**: Lock icon with gray background * **sms**: Mobile icon with SMS label * **walletconnect**: WalletConnect icon with blue background * **rabby**: Rabby icon with themed background #### Button Behavior The branded submit button adapts its text based on the user's state: * **New users**: Displays "sign up with \[Signer]" when entering a new username * **Existing users**: Displays "log in with \[Signer]" when entering an existing username * **Generic state**: Shows "log in" or "sign up" when username field is empty #### Extension Validation For extension-based signers (MetaMask, Phantom, Rabby), the branded button automatically: * Detects if the required browser extension is installed * Disables the button if the extension is missing * Shows appropriate error messaging to guide users to install the extension ### Error Display Configuration Controller provides flexible error display options to customize how transaction errors are presented to users: #### errorDisplayMode The `errorDisplayMode` option controls how transaction errors are displayed to users. It works independently from `propagateSessionErrors` and provides three display modes: ```typescript const controller = new Controller({ errorDisplayMode: "modal", // "modal" | "notification" | "silent" // other options... }); ``` ##### Display Modes **`modal` (default)** * Opens the controller modal when transaction errors occur * Preserves existing behavior for backward compatibility * Provides detailed error information in a focused interface * Recommended for applications that prefer modal-based error handling **`notification`** * Shows a clickable toast notification when errors occur * Users can click the toast to open the modal for manual retry * Provides a less intrusive error experience * Ideal for gaming applications where modal interruptions are disruptive **`silent`** * No UI is displayed for transaction errors * Errors are logged to console for programmatic handling * Applications must handle error states programmatically * Best for applications that implement custom error handling ##### Error Display Behavior The error display behavior depends on the combination of `propagateSessionErrors` and `errorDisplayMode`: | `propagateSessionErrors` | `errorDisplayMode` | Behavior | | ------------------------ | ------------------ | --------------------------------------------------- | | `true` | Any | Errors are always rejected immediately, no UI shown | | `false` (default) | `modal` | Opens controller modal for error handling | | `false` | `notification` | Shows clickable toast notification | | `false` | `silent` | No UI, errors logged to console | ##### Special Cases Certain error types always display UI regardless of the `errorDisplayMode` setting: * **SessionRefreshRequired**: Always opens modal to refresh user session * **ManualExecutionRequired**: Always opens modal for manual transaction approval These exceptions ensure users can complete required authentication or approval flows. ##### Usage Examples **Gaming Application (Minimal Interruption)** ```typescript const controller = new Controller({ errorDisplayMode: "notification", policies: gameSessionPolicies, }); // Transaction errors show as clickable toast notifications // Users can continue gameplay and address errors when convenient ``` **Financial Application (Detailed Error Handling)** ```typescript const controller = new Controller({ errorDisplayMode: "modal", }); // Transaction errors open detailed modal interface // Users get comprehensive error information and retry options ``` **Custom Error Handling** ```typescript const controller = new Controller({ errorDisplayMode: "silent", }); try { await account.execute(calls); } catch (error) { // Application handles error display and retry logic handleTransactionError(error); } ``` ### Browser Compatibility Storage Access API support varies by browser: * **Safari** - Full support, required for cross-domain iframe access * **Chrome/Edge** - Full support when third-party cookies are blocked * **Firefox** - Full support in private browsing and with strict privacy settings * **Legacy browsers** - Graceful degradation, assumes storage access is available ## Getting Started Cartridge Controller implements a standard StarkNet account interface and can be seamlessly integrated into your application like any other wallet. ### Quick Start The fastest way to get started is to install the Controller SDK and connect to Cartridge: ```typescript twoslash import Controller from "@cartridge/controller"; const controller = new Controller({}); const account = await controller.connect(); // You're ready to execute transactions! ``` For more advanced use cases, you can also pass dynamic authentication options: ```typescript import Controller from "@cartridge/controller"; const controller = new Controller({}); // Use default signupOptions from constructor const account = await controller.connect(); // Or override with specific options for this connection const phantomAccount = await controller.connect(["phantom-evm"]); ``` #### Headless Authentication For programmatic authentication without opening any UI, you can use [headless mode](/controller/headless-authentication) by providing a `username` and `signer`: ```typescript import Controller from "@cartridge/controller"; const controller = new Controller({}); // Headless authentication with WebAuthn/Passkey const account = await controller.connect({ username: "alice", signer: "webauthn", }); // Headless authentication with password const account = await controller.connect({ username: "alice", signer: "password", password: "your-secure-password", }); // Headless authentication with OAuth providers await controller.connect({ username: "alice", signer: "google" }); await controller.connect({ username: "alice", signer: "discord" }); // Headless authentication with EVM wallets await controller.connect({ username: "alice", signer: "metamask" }); await controller.connect({ username: "alice", signer: "phantom-evm" }); ``` :::info Headless mode performs authentication in a hidden iframe without displaying any UI. However, if session policies need approval or verification, the UI will open automatically to request user consent. ::: When `connect()` is called, users will see an improved Controller creation interface with username autocomplete functionality. As users type their username, they'll see matching existing accounts with user profiles, making it easier to connect to existing controllers or choose unique usernames for new accounts. After creating a new Controller, users see a welcome screen and can continue from there to close the modal. For session-based applications, users will see permissions organized into clear sections: an expandable "Authorize \[game]" card containing contract methods, followed by dedicated spending limit cards for token approvals, making it easy to understand what they're authorizing. :::info Controller will set **essential cookies** as part of the initialization. These are necessary for the Controller to function properly. ::: ### Installation ```bash npm install @cartridge/controller starknet ``` ### Basic Usage Here's a simple example of how to initialize and use the Controller: #### Without session policies ```typescript import Controller from "@cartridge/controller"; // All transactions will require manual approval const controller = new Controller(); ``` :::note When no session policies are provided, each transaction requires manual user approval through the Cartridge interface. This is suitable for simple applications or testing, but games typically benefit from using [session policies](/controller/sessions) for a smoother experience. ::: #### With session policies Pass session policies to the Controller constructor to enable gasless transactions and pre-approved transactions. For detailed information about configuring session policies, see [Sessions](/controller/sessions). ```typescript import Controller from "@cartridge/controller"; import { SessionPolicies } from "@cartridge/controller"; const policies: SessionPolicies = { contracts: { // Your game contract "0x1234...": { name: "My Game Contract", methods: [ { name: "move_player", entrypoint: "move_player" }, { name: "attack", entrypoint: "attack" }, ], }, }, }; // `move_player` and `attack` txs will not require approval const controller = new Controller({ policies }); ``` #### With custom configuration The Controller ships with sensible defaults for chain RPCs, but you can override them if needed. ```typescript import { constants } from "starknet"; import Controller from "@cartridge/controller"; const controller = new Controller({ // Optional chain configuration chains: [ { rpcUrl: "https://api.cartridge.gg/x/starknet/sepolia" }, { rpcUrl: "https://api.cartridge.gg/x/starknet/mainnet" }, ], defaultChainId: constants.StarknetChainId.SN_MAIN, }); ``` #### Standalone Authentication The Controller provides an `open()` method for standalone authentication, which opens the keychain in first-party context. This is useful for establishing first-party storage and enabling seamless iframe access across all games. ```typescript import Controller from "@cartridge/controller"; const controller = new Controller(); // Basic usage - redirect to current page after authentication controller.open(); // With custom redirect URL controller.open({ redirectUrl: "https://mygame.com/dashboard", }); // With preset theme and redirect controller.open({ redirectUrl: "https://mygame.com/play", preset: "mygame", }); ``` #### Parameters The `open()` method accepts an options object with the following properties: * `redirectUrl?: string` - The URL to redirect to after authentication (defaults to current page) * `preset?: string` - The preset theme to use for the keychain interface :::info When using presets, make sure your preset is configured in the [@cartridge/presets](https://github.com/cartridge-gg/presets) repository. See the [presets guide](/controller/presets) for more information. ::: ### Providers Controller is initialized through a "provider." There are two providers, each with a different security and signing model. > "Connectors" are thin wrappers that plug providers into frameworks like `starknet-react`. #### ControllerProvider (iframe-based) > ControllerProvider is the recommended provider for web applications. The default `ControllerProvider` (often exported as `Controller`) embeds the Cartridge keychain in a sandboxed iframe. Both the owner signer and a **session key** live inside the iframe, employing a trust model similar to any injected wallet (e.g. MetaMask, Argent). When your app calls `execute()`, the request is forwarded to the iframe via `postMessage`. The keychain first tries to sign with the session key. If the call matches the app's [session policies](/controller/sessions), it signs automatically --- no user prompt. If the call doesn't match any policy, it falls back to the **owner key** and prompts the user for approval. > The session's authorization is cached onchain on first use, so no explicit `registerSession()` call is needed. #### SessionProvider (redirect-based) > SessionProvider is the recommended provider for native applications. The `SessionProvider` is designed for environments where an iframe cannot be used, such as native mobile apps (Capacitor, React Native) or server-side Node.js. Instead of embedding the owner key in an iframe, it: 1. Opens a browser to the Cartridge keychain for one-time user authentication 2. Generates an ephemeral session keypair locally 3. **Registers the session key onchain** with the approved policies compiled into a merkle root 4. Stores the session private key locally (in `localStorage` or on the filesystem) After registration, transactions are signed with the session key and executed via `executeFromOutside()` --- no further UI is needed. Because the session key is not the owner key, **policies are enforced onchain** --- every transaction must include a merkle proof showing the call matches the registered policies. The owner key is never exposed through this provider, so there is no fallback for calls outside the approved policies. > For a visual comparison of how each provider authenticates and signs, see the [Architecture](/controller/architecture#auth-flows) page. #### When to use which | | ControllerProvider | SessionProvider | | ---------------------- | ------------------------------------------ | ------------------------------------------------------------ | | **Environment** | Web browsers | Native apps, Node.js, or environments without iframe support | | **Signing** | Session key in iframe (owner key fallback) | Ephemeral session key stored locally | | **Policy enforcement** | Keychain (wallet-level) | Onchain (merkle proofs) | | **Non-policy calls** | Prompts user, signs with owner key | Not supported | | **Auth UX** | Embedded keychain modal | Browser redirect + deep link back | | **After auth** | Iframe signs each transaction | Transactions execute without UI | #### ControllerConnector (Web) `ControllerConnector` wraps `ControllerProvider` for use with frontend frameworks like `starknet-react`. ```tsx import React from "react"; import { constants } from "starknet"; import { sepolia, mainnet } from "@starknet-react/chains"; import { StarknetConfig, jsonRpcProvider, cartridge } from "@starknet-react/core"; import { SessionPolicies } from "@cartridge/controller"; import { ControllerConnector } from "@cartridge/connector"; const policies: SessionPolicies = { // Define session policies here }; // Create the controller connector const controller = new ControllerConnector({ policies, }); // Configure the JSON RPC provider const provider = jsonRpcProvider({ rpc: (chain) => { switch (chain) { case mainnet: default: return { nodeUrl: "https://api.cartridge.gg/x/starknet/mainnet" }; case sepolia: return { nodeUrl: "https://api.cartridge.gg/x/starknet/sepolia" }; } }, }); // Create the Starknet provider export function StarknetProvider({ children }: { children: React.ReactNode }) { return ( {children} ); } ``` ##### Dynamic Authentication Options The ControllerConnector supports dynamic authentication configuration per connection, enabling multiple branded authentication flows: ```tsx import { useConnect } from "@starknet-react/core"; import { ControllerConnector } from "@cartridge/connector"; function MyConnectComponent() { const { connect } = useConnect(); const connector = ControllerConnector.fromConnectors(connectors); return (
{/* Use default signupOptions from constructor */} {/* Override with specific options for branded auth flows */}
); } ``` ##### Wallet Standard Integration By default, the Controller exposes the `StarknetWindowObject` interface. However, the Controller also supports the [get-starknet wallet standard](https://get-starknet.vercel.app/), enabling integration with libraries like `starknet-react` and `solid.js`. You can use the Controller directly or through the ControllerConnector to cast to the wallet-standard compatible `WalletWithStarknetFeatures` interface: ```tsx import Controller from "@cartridge/controller"; import type { WalletWithStarknetFeatures } from "@starknet-io/get-starknet-wallet-standard/features"; // Direct usage without starknet-react const controller = new Controller({ /* options */ }); const walletStandard: WalletWithStarknetFeatures = controller.asWalletStandard(); ``` ```tsx import { ControllerConnector } from "@cartridge/connector"; import type { WalletWithStarknetFeatures } from "@starknet-io/get-starknet-wallet-standard/features"; // Using ControllerConnector (delegates to the controller's implementation) const connector = new ControllerConnector(); const walletStandard: WalletWithStarknetFeatures = connector.asWalletStandard(); ``` :::warning The `asWalletStandard()` method is an experimental feature and may contain bugs. Please [report any issues](https://github.com/cartridge-gg/controller/issues) you encounter. ::: #### SessionConnector (Native) `SessionConnector` wraps `SessionProvider` for use in native and mobile applications. It requires explicit `rpc` and `chainId` parameters because it operates outside a framework like `starknet-react` that would normally provide these. The `redirectUrl` is where the browser will redirect after authentication completes. :::tip To learn more about native application workflows, read our [native integration guide](/controller/native/overview). ::: ```tsx import { constants } from "starknet"; import { SessionPolicies } from "@cartridge/controller"; import { SessionConnector } from "@cartridge/connector"; const policies: SessionPolicies = { // Define session policies here }; // Using manual policies const sessionConnector = new SessionConnector({ policies, rpc: "https://api.cartridge.gg/x/starknet/mainnet", chainId: constants.StarknetChainId.SN_MAIN, redirectUrl: "myapp://auth-callback", }); // Or using a preset (recommended for verified games) const sessionConnector = new SessionConnector({ preset: "my-game", rpc: "https://api.cartridge.gg/x/starknet/mainnet", chainId: constants.StarknetChainId.SN_MAIN, redirectUrl: "myapp://auth-callback", }); ``` #### Migration Notes for v0.10.0 ##### StarkNet v8 Breaking Changes If you're using `WalletAccount` directly in your application, you'll need to update the constructor call: ```typescript // Before (v0.9.x) const account = new WalletAccount(provider, address, signer); // After (v0.10.0) const account = new WalletAccount({ nodeUrl: provider, address: address, signer: signer }); ``` ##### Ethereum Wallet Integration Changes The MetaMask SDK has been removed in favor of the EIP-6963 standard. If your application relied on MetaMask SDK-specific features: * Wallet detection now uses EIP-6963 standard wallet detection * All Ethereum wallets are now handled through a shared base class * This change improves bundle size and wallet compatibility ##### Lodash Removal The lodash dependency has been completely removed. If you were importing lodash utilities from this package, you'll need to replace them with custom utilities or install lodash separately in your application. ### Development Workflow If you're contributing to the Cartridge Controller or running the examples locally, you have two development modes available: #### Local Development Mode ```bash pnpm dev ``` This runs all services locally with local API endpoints, perfect for offline development and testing changes to the Controller itself. #### Production API Testing Mode ```bash pnpm dev:live ``` This hybrid mode runs the keychain and examples locally while connecting to production APIs. The `dev:live` mode provides production RPC endpoints, Auth0, Stripe, and Turnkey configurations while keeping the keychain frame at `localhost:3001` and your application at `localhost:3002`. :::warning The `dev:live` mode connects to production APIs, so be careful with any transaction testing as it will use real network resources. ::: ### Examples For more detailed examples of how to use Cartridge Controller in different environments, check out our integration guides: 1. [React](/controller/examples/react) * Integration with `starknet-react` * Hooks and components * State management 2. [Svelte](/controller/examples/svelte) * Svelte stores and reactivity * Component lifecycle * Event handling 3. [Rust](/controller/examples/rust) * Native integration * Error handling * Async operations Each guide provides comprehensive examples and best practices for integrating Cartridge Controller in your preferred environment. ### Next Steps * Learn about [session policies](/controller/sessions) * Display [toast notifications](/controller/toast-notifications) for user feedback * Set up [multiple signers](/controller/signer-management) for backup authentication * Integrate [starter packs](/controller/starter-packs) for game monetization * Customize your [Controller](/controller/presets) * Set up [usernames](/controller/usernames) * Configure [paymaster](/controller/configuration) ## Headless Authentication Headless authentication enables programmatic authentication with the Cartridge Controller SDK without displaying any user interface. This is ideal for automated workflows, server-side applications, and creating seamless user experiences where you want to minimize UI interruptions. ### Overview Headless mode works by: 1. Passing credentials directly to `controller.connect({ username, signer, password? })` 2. Performing authentication in a hidden iframe without opening any modal 3. Only showing UI if session policies require explicit user approval 4. Returning the authenticated account for immediate use ``` Controller SDK → Hidden Keychain iframe → Backend API → Authenticated Account ``` ### Basic Usage #### Recommended: Lookup-First Flow Pattern The recommended pattern for headless authentication checks for account existence and available signers before attempting to connect: ```typescript import Controller from "@cartridge/controller"; const controller = new Controller({}); try { // First, lookup the username to check existence and available signers const lookupResult = await controller.lookupUsername("alice"); if (!lookupResult.exists) { // Account doesn't exist - handle auto-signup or show error console.log("Account does not exist"); return; } // Use the normalized signer options from lookup const availableSigners = lookupResult.signers; const preferredSigner = availableSigners.includes("webauthn") ? "webauthn" : availableSigners[0]; const account = await controller.connect({ username: "alice", signer: preferredSigner, }); console.log("Authenticated successfully:", account.address); } catch (error) { console.error("Authentication failed:", error.message); } ``` #### WebAuthn/Passkey Authentication The most secure option for headless authentication uses WebAuthn (passkeys): ```typescript import Controller from "@cartridge/controller"; const controller = new Controller({}); try { const account = await controller.connect({ username: "alice", signer: "webauthn", }); console.log("Authenticated successfully:", account.address); } catch (error) { console.error("Authentication failed:", error.message); } ``` #### Password Authentication For scenarios where WebAuthn isn't available: ```typescript const account = await controller.connect({ username: "alice", signer: "password", password: "your-secure-password", }); ``` :::warning Never hardcode passwords in your source code. Use environment variables, secure configuration files, or prompt users for their passwords at runtime. ::: #### OAuth Providers Authenticate using social login providers: ```typescript // Google OAuth await controller.connect({ username: "alice", signer: "google" }); // Discord OAuth await controller.connect({ username: "alice", signer: "discord" }); ``` #### EVM Wallet Authentication Connect using Ethereum wallets via EIP-191 signing: ```typescript // MetaMask await controller.connect({ username: "alice", signer: "metamask" }); // Phantom EVM await controller.connect({ username: "alice", signer: "phantom-evm" }); // Rabby Wallet await controller.connect({ username: "alice", signer: "rabby" }); ``` #### WalletConnect For mobile wallet connections: ```typescript await controller.connect({ username: "alice", signer: "walletconnect" }); ``` #### SMS Authentication Authenticate using SMS-based one-time passcodes: ```typescript await controller.connect({ username: "alice", signer: "sms" }); ``` :::note SMS authentication requires a phone number and uses one-time passcode (OTP) verification. The SMS signer is registered as EIP-191 with provider `"sms"`. ::: ### Supported Signer Options Headless mode supports all implemented authentication methods: * `webauthn` - WebAuthn/Passkey (most secure) * `password` - Username/password authentication * `google` - Google OAuth * `discord` - Discord OAuth * `metamask` - MetaMask wallet * `rabby` - Rabby wallet * `phantom-evm` - Phantom EVM wallet * `walletconnect` - WalletConnect protocol * `sms` - SMS one-time passcode authentication For complete details on available authentication methods, see [Signer Management](./signer-management). ### Session Approval Flow If your application uses [session policies](./sessions) that haven't been verified or include spending limits that require approval, the keychain will automatically open the approval UI after successful authentication: ```typescript const controller = new Controller({ policies: { contracts: { "0x1234...": { name: "My Game Contract", methods: [{ name: "play", entrypoint: "play" }], }, }, }, }); // This may open session approval UI after authentication const account = await controller.connect({ username: "alice", signer: "webauthn", }); // Account is ready to use once connect() resolves await account.execute(/* your transaction */); ``` ### Error Handling Headless authentication provides specific error handling: ```typescript import { HeadlessAuthenticationError } from "@cartridge/controller"; try { const account = await controller.connect({ username: "alice", signer: "webauthn", }); } catch (error) { if (error instanceof HeadlessAuthenticationError) { // Handle authentication-specific errors console.error("Auth failed:", error.message); // Common reasons: // - Username doesn't exist // - Signer not associated with username // - Invalid credentials // - Network connectivity issues } else { // Handle other errors console.error("Unexpected error:", error); } } ``` ### Username Lookup API #### lookupUsername Method The `lookupUsername` method allows you to check if a username exists and what authentication methods are available: ```typescript const lookupResult = await controller.lookupUsername("alice"); console.log(lookupResult.exists); // true/false console.log(lookupResult.signers); // ["webauthn", "google", "discord"] ``` **Return Type:** ```typescript interface UsernameLookupResult { exists: boolean; signers: string[]; // Available authentication methods } ``` This method is particularly useful for: * Validating usernames before attempting authentication * Displaying appropriate login options to users * Implementing auto-signup flows when accounts don't exist * Preventing unnecessary authentication attempts #### Auto-Signup Support Version 0.13.7 adds auto-signup functionality for headless flows. When a username doesn't exist, you can automatically create an account: ```typescript try { const lookupResult = await controller.lookupUsername("newuser"); if (!lookupResult.exists) { // Auto-signup: create new account with the desired signer const account = await controller.connect({ username: "newuser", signer: "webauthn", // or any preferred authentication method }); console.log("New account created:", account.address); } else { // Existing account: use available signers const account = await controller.connect({ username: "newuser", signer: lookupResult.signers[0], }); console.log("Existing account authenticated:", account.address); } } catch (error) { console.error("Authentication failed:", error.message); } ``` :::note Auto-signup maintains strict signer matching for existing accounts. If an account exists but the specified `signer` is not associated with it, authentication will fail rather than creating a duplicate account. ::: ### Integration Patterns #### React Hook Pattern with Lookup Create a reusable hook for headless authentication with username lookup: ```tsx import { useCallback, useState } from 'react'; import { useConnect } from '@starknet-react/core'; import { ControllerConnector } from '@cartridge/connector'; export function useHeadlessAuth() { const { connectAsync, connectors } = useConnect(); const [loading, setLoading] = useState(false); const controller = connectors[0] as ControllerConnector; const authenticateHeadless = useCallback(async ( username: string, signer?: string ) => { setLoading(true); try { // Disconnect if already connected if (controller.account) { await controller.disconnect(); } // Lookup username to check existence and available signers const lookupResult = await controller.lookupUsername(username); let finalSigner = signer; if (!signer) { // Auto-select best available signer if (lookupResult.exists) { finalSigner = lookupResult.signers.includes("webauthn") ? "webauthn" : lookupResult.signers[0]; } else { // Default signer for new accounts finalSigner = "webauthn"; } } // Headless authentication (with auto-signup if needed) const account = await controller.connect({ username, signer: finalSigner }); if (!account) { throw new Error('Authentication failed'); } // Sync with starknet-react await connectAsync({ connector: controller }); return { account, isNewAccount: !lookupResult.exists, availableSigners: lookupResult.signers }; } finally { setLoading(false); } }, [controller, connectAsync]); return { authenticateHeadless, loading }; } ``` #### ControllerConnector Helper Method The `ControllerConnector` also exposes the `lookupUsername` helper for starknet-react applications: ```tsx import { ControllerConnector } from '@cartridge/connector'; // In your React component or hook const connector = connectors.find(c => c.id === 'cartridge') as ControllerConnector; const lookupResult = await connector.lookupUsername("alice"); if (lookupResult.exists) { console.log("Available signers:", lookupResult.signers); } ``` #### Server-Side Pattern (Node.js) For server-side applications, use the SessionProvider: ```typescript import { SessionProvider } from "@cartridge/connector"; const sessionProvider = new SessionProvider({ rpc: "https://api.cartridge.gg/x/starknet/mainnet", chainId: "SN_MAIN", // Note: SessionProvider doesn't support headless mode directly // Use regular browser-based headless authentication for programmatic flows }); ``` :::note Server-side headless authentication is currently only available through the browser-based Controller SDK. For true server-side usage, consider the [native headless Controller](/controller/native/headless) using C++ bindings. ::: ### Security Considerations #### Credential Storage * **Never commit credentials to source code** * Use environment variables for production credentials * Consider secure key management systems for sensitive applications * Rotate credentials regularly #### Authentication Method Selection * **WebAuthn (recommended)**: Most secure, hardware-backed when available * **OAuth**: Good for user convenience, relies on third-party security * **Password**: Least secure, but widely compatible * **EVM Wallets**: Security depends on wallet implementation and user practices #### Session Management ```typescript // Always handle session lifecycle properly const account = await controller.connect({ username, signer }); // Use the account for transactions await account.execute(calls); // Clean up when done await controller.disconnect(); ``` ### Differences from Native Headless Mode This web-based headless authentication is different from the [native headless Controller](/controller/native/headless): | Feature | Web Headless Authentication | Native Headless Controller | | ------------------ | ----------------------------- | -------------------------------- | | **Environment** | Browser applications | Server-side, native apps | | **Implementation** | Hidden iframe + postMessage | Direct API integration | | **Key Management** | Managed by Cartridge keychain | Application-managed private keys | | **UI Fallback** | Can open UI for approvals | No UI available | | **Use Case** | Seamless web UX | Backend services, automation | ### Troubleshooting #### Common Issues 1. **"User not found"**: Username doesn't exist in the system * *Solution*: Use `lookupUsername()` to check existence before attempting to connect * *Auto-signup*: Consider enabling auto-signup for new users 2. **"Signer not found"**: The specified `signer` isn't associated with the username * *Solution*: Use `lookupUsername()` to get available signers for the username * *Fallback*: Implement signer selection UI based on available options 3. **"Not ready to connect"**: Controller initialization is still in progress 4. **Network timeouts**: Check network connectivity and RPC endpoint availability #### Debug Mode Enable debug logging to troubleshoot issues: ```typescript // Enable debug mode in development const controller = new Controller({ // Add debug configuration if available }); // Check browser console for detailed error messages ``` #### Validation Validate inputs before attempting authentication: ```typescript function validateHeadlessOptions(username: string, signer: string) { if (!username || username.trim().length === 0) { throw new Error("Username is required"); } const validSigners = [ "webauthn", "password", "google", "discord", "metamask", "rabby", "phantom-evm", "walletconnect", "sms" ]; if (!validSigners.includes(signer)) { throw new Error(`Invalid signer: ${signer}`); } } ``` ### Next Steps * Learn about [Sessions](./sessions) for fine-grained transaction control * Explore [React integration patterns](./examples/react) for web applications * Consider [native headless mode](/controller/native/headless) for backend services * Set up [error handling and logging](./configuration) for production use ## Inventory Controller provides Inventory modal to manage account assets (`ERC-20`, `ERC-721`) with integrated marketplace functionality for buying and selling digital assets. ### Configure tokens By default, commonly used tokens are indexed and automatically shown. Full list of default tokens are listed in [`torii-config/public-tokens/mainnet.toml`](https://github.com/cartridge-gg/controller/blob/main/packages/torii-config/public-tokens/mainnet.toml). This list can be extended by running your own Torii instance with a custom indexing config. #### Configure additional token to index ```toml # torii-config.toml [indexing] contracts = [ "erc20:", "erc721:" ] ``` #### Run a Torii instance Self-host Torii against your indexing config. See the [Torii documentation](https://book.dojoengine.org/toolchain/torii/configuration#indexing-configuration) for setup and configuration options. #### Configure Controller Provide your Torii instance URL to `ControllerOptions`. For detailed configuration options, see [configuration](./configuration). ```typescript const controller = new Controller({ slot: "" }); // or via connector const connector = new CartridgeConnector({ slot: "" }) ``` #### Open Inventory modal ```typescript controller.openProfile("inventory"); ``` ### Marketplace Integration The inventory system includes built-in marketplace functionality for ERC721 and ERC1155 assets: #### Features * **Asset Purchasing**: Buy digital assets from marketplace listings with transparent fee structure * **Collection Browsing**: Browse and purchase items from specific collections * **Multi-token Support**: Purchase with supported tokens (ETH, STRK, USDC, LORDS) * **Transaction Safety**: All marketplace transactions include proper fee disclosure and confirmation flows #### Fee Transparency When purchasing assets through the marketplace, multiple fee types are automatically calculated and displayed: * **Client Fees**: Automatically calculated and applied to each transaction * **Creator Royalties**: Honor creator royalty settings for supported collections (when the `royalty_info` entrypoint is available) * **Marketplace Fees**: Platform fees as configured by the marketplace * **Smart Amount Rendering**: Automatically adjusts decimal precision for small amounts to ensure readability * **Interactive Fee Tooltips**: Hover over fee information to see detailed breakdowns ## Overview #### TL;DR: Cartridge Controller is: * A gaming-focused smart contract wallet for Starknet * Makes Web3 gaming accessible and fun via session keys and gasless transactions * Handles seamless player onboarding with passkey authentication * Provides identity, achievements, and customization features for games * Compatible with popular frameworks like Starknet React and can be integrated across platforms ![Cartridge Controller Overview](/controller.png) ### Key Features #### Simple & Secure * Passwordless authentication using passkeys for one-click onboarding * Multi-signer support with passkeys, password auth, social login, and external wallets * Self-custodial embedded wallets that put players in control #### Designed for Fun * Session keys eliminate transaction popups during gameplay * Secure transaction delegation lets games submit actions on behalf of players * Free transactions through the Cartridge paymaster so players focus on playing #### Customizable * Full theme customization to match your game's branding * Dynamic UI components for displaying game assets and achievements * Extensible plugin system for adding custom functionality #### Identity and Reputation * Universal player identity that works across all Cartridge-enabled games * Built-in achievement system for tracking player accomplishments * Social features to connect players and build communities #### Monetization and Payments * Multi-chain cryptocurrency and fiat support * Starter pack bundles combining credits and game assets * Booster pack reward systems with Merkle Drop claiming * ERC721 and ERC1155 NFT marketplace support with automated fee management This guide provides a comprehensive overview of how to create and apply custom themes, provide verified session policies, and configure Apple App Site Association (AASA) for iOS integration with the Cartridge Controller. ### Creating a Theme To create a theme, teams should commit their theme config to the `configs` folder in [`@cartridge/presets`](https://github.com/cartridge-gg/presets/tree/main/configs) with the icon and banner included. ```json { "origin": "https://flippyflop.gg", "theme": { "colors": { "primary": "#F38332" }, "cover": "cover.png", "icon": "icon.png", "name": "FlippyFlop" } } ``` #### Origin Configuration The `origin` field specifies which origins are authorized to use your preset. This is important for security and preventing unauthorized use of your configuration. ##### Web Applications For standard web applications, use your domain: ```json { "origin": "https://yourdomain.com" } ``` ##### Multiple Origins You can specify multiple origins as an array: ```json { "origin": ["https://yourdomain.com", "https://staging.yourdomain.com"] } ``` ##### Capacitor Apps with Custom Hostnames For Capacitor mobile apps using custom hostnames, include the custom hostname in your origins: ```json { "origin": ["https://yourdomain.com", "my-custom-app"] } ``` This authorizes both your web app and your Capacitor app with the custom hostname: * **Web**: `https://yourdomain.com` * **iOS Capacitor**: `capacitor://my-custom-app` * **Android Capacitor**: `https://my-custom-app` **Note**: The default `localhost` origin (`capacitor://localhost`) is always allowed for development convenience and doesn't need to be explicitly listed in presets. See an example pull request [`here`](https://github.com/cartridge-gg/presets/pull/8/files) ### Verified Sessions Session policies can be provided in the preset configuration, providing a smoother experience for your users. In order to submit verified policies, create a commit with them to your applications `config.json` in [`@cartridge/presets`](https://github.com/cartridge-gg/presets/tree/main/configs). For detailed information about session policies, see [Sessions](./sessions). :::warning **Policy Precedence Rules:** 1. When `shouldOverridePresetPolicies: true` and policies are provided → uses URL policies 2. When preset is configured and has policies for the current chain → uses preset policies (ignores URL policies) 3. When preset is configured but has no policies for the current chain → falls back to URL policies 4. When no preset is configured → uses URL policies To force manually provided policies over preset policies, set `shouldOverridePresetPolicies: true`. ::: For an example, see [dope-wars](https://github.com/cartridge-gg/presets/blob/main/configs/dope-wars/config.json): ```json { "origin": "dopewars.game", "chains": { "SN_MAIN": { "policies": { "contracts": { "0x051Fea...": { "name": "VRF Provider", "description": "Provides verifiable random functions", "methods": [ { "name": "Request Random", "description": "Request a random number", "entrypoint": "request_random" } ] } } } } }, ... } ``` ### Paymaster Predicate Support Session policies now support **paymaster predicates**, which provide additional conditional logic for transaction sponsorship. This is particularly useful for games that need to sponsor transactions based on specific conditions or game state. #### Using Predicates in Presets To add paymaster predicate support to your preset configuration, include a `predicate` field in your method definition: ```json { "origin": "mygame.example.com", "chains": { "SN_MAIN": { "policies": { "contracts": { "0x123...abc": { "name": "Game Contract", "description": "Main game contract with paymaster support", "methods": [ { "name": "Move Player", "description": "Move player with conditional sponsorship", "entrypoint": "move_player", "is_paymastered": true, "predicate": { "address": "0x456...def", "entrypoint": "check_move_eligibility" } }, { "name": "Attack Enemy", "description": "Attack with unconditional sponsorship", "entrypoint": "attack_enemy", "is_paymastered": true } ] } } } } } } ``` #### Predicate Structure The `predicate` field contains: * `address`: The contract address that contains the predicate logic * `entrypoint`: The function name that will be called to evaluate the condition When a transaction is submitted: 1. If a method has `is_paymastered: true` without a predicate, it will always be sponsored 2. If a method has both `is_paymastered: true` and a predicate, the predicate function will be called first 3. The transaction will only be sponsored if the predicate function returns a truthy value This allows for sophisticated gas sponsorship policies based on game state, user eligibility, or other conditional logic. ### Apple App Site Association The [Apple App Site Association (AASA)](https://developer.apple.com/documentation/xcode/supporting-associated-domains) configuration enables iOS app integration with Cartridge Controller, allowing for usage of Web Credentials (Passkeys) in native applications. #### Configuration To add your iOS app to the AASA file, include the `apple-app-site-association` section in your game's `config.json`: ##### JSON Configuration ```json { ..., "apple-app-site-association": { "webcredentials": { "apps": ["ABCDE12345.com.example.yourgame"] } }, ..., } ``` ## Sessions and Policies Cartridge Controller supports session-based authorization and policy-based transaction approvals. When policies are pre-approved by the user, games can execute transactions seamlessly without requesting approval for each interaction, creating a smooth gaming experience. ### How Sessions Work 1. **Policy Definition**: Games define which contract methods they need to call 2. **User Approval**: Users approve these policies once during initial connection 3. **Session Creation**: Controller creates a session with approved transaction permissions 4. **Gasless Execution**: Games can execute approved transactions without user prompts 5. **Paymaster Integration**: Transactions can be sponsored through Cartridge paymaster ### Transactions Without Policies Cartridge Controller can execute transactions **without** defining policies. When no policies are provided: * Each transaction requires manual user approval via the Cartridge interface * Users will see a confirmation screen for every transaction * No gasless transactions or paymaster integration * Suitable for simple applications that don't need session-based authorization :::warning Running without policies **does not work on local Katana**. The paymaster requires policies to deploy the controller before the first transaction. See [Using Katana for Local Development](/controller/configuration#using-katana-for-local-development) for setup details. ::: ```typescript // Controller without policies - requires manual approval for each transaction const controller = new Controller(); const account = await controller.connect(); // This will prompt the user for approval const tx = await account.execute([ { contractAddress: "0x123...", entrypoint: "transfer", calldata: ["0x456...", "100"], } ]); ``` ### Transactions With Policies ```typescript const policies = { // Define your policies here } // Using the controller directly const controller = new Controller({ policies, // other options }); // Using the starknet-react connector const connector = new CartridgeConnector({ policies, // other options }); // Future transactions will not require approval ``` :::info Full integration examples [are available here](https://github.com/cartridge-gg/controller/blob/main/examples/next/src/components/providers/StarknetProvider.tsx). ::: ### Sessions vs. Manual Approval | Feature | With Policies (Sessions) | Without Policies (Manual) | | -------------------- | -------------------------------------------------------------------------------------- | ------------------------------------ | | Transaction Approval | Pre-approved via session policies | Manual approval each time | | User Experience | Seamless gameplay | Confirmation prompts | | Gasless Transactions | Yes (via paymaster) | No | | Error Handling | Configurable (see [Configuration](/controller/configuration#propagate-session-errors)) | Always shows keychain UI | | Setup Complexity | Higher (policy definition) | Lower (basic setup) | | Best For | Games, frequent transactions | Simple apps, occasional transactions | ### Session Options ```typescript export type SessionOptions = { rpc: string; // RPC endpoint URL chainId: string; // Chain ID for the session policies?: SessionPolicies; // Approved transaction policies (optional if using preset) preset?: string; // Preset name for verified session policies shouldOverridePresetPolicies?: boolean; // Override preset policies with manual policies when both are provided redirectUrl: string; // URL to redirect after registration disconnectRedirectUrl?: string; // Optional URL to redirect after disconnect/logout signupOptions?: AuthOptions; // Optional authentication methods available during session creation }; ``` #### Updating Session Policies The `updateSession()` method allows you to update session policies at runtime without requiring a full reconnect. This is useful when you need to add new permissions during gameplay or change policy configurations dynamically. ```typescript // Update using a preset await controller.updateSession({ preset: "loot-survivor", }); // Update using direct policies await controller.updateSession({ policies: newSessionPolicies, }); ``` Either `policies` or `preset` must be provided. The method opens the keychain interface where users can approve the updated policies, following the same approval flow as initial session creation. #### Using Presets with SessionProvider The `preset` parameter allows you to use verified session policies from `@cartridge/presets` instead of manually defining policies. This ensures consistency between SessionProvider and ControllerProvider and simplifies configuration for games with verified presets. **Basic preset usage:** ```typescript const sessionProvider = new SessionProvider({ rpc: "https://starknet-mainnet.public.blastapi.io/rpc/v0.7", chainId: "SN_MAIN", preset: "my-game", // Load verified policies from preset redirectUrl: "https://myapp.com/", }); ``` **Policy precedence rules:** 1. **Manual policies only**: When only `policies` are provided, manual policies are used 2. **Preset only**: When only `preset` is provided, policies are resolved from the preset configuration 3. **Both provided**: When both `preset` and `policies` are provided: * If `shouldOverridePresetPolicies: true` → uses manual policies (overrides preset) * If preset has policies for the current chain → uses preset policies (ignores manual policies with console warning) * If preset has no policies for the current chain → falls back to manual policies 4. **No policies found**: If no policies are available from any source, Controller operates without session policies **Example with policy override:** ```typescript const sessionProvider = new SessionProvider({ rpc: "https://starknet-mainnet.public.blastapi.io/rpc/v0.7", chainId: "SN_MAIN", preset: "my-game", policies: customPolicies, // These would normally be ignored shouldOverridePresetPolicies: true, // Override preset with custom policies redirectUrl: "https://myapp.com/", }); ``` **Benefits of using presets:** * **Consistency**: Same policies used across SessionProvider and ControllerProvider * **Verification**: Preset policies are verified and provide enhanced trust indicators * **Maintenance**: Policy updates happen centrally in the preset configuration * **Error Prevention**: Eliminates policy hash divergence that can cause session/not-registered errors #### Authentication Options The `signupOptions` parameter allows you to customize which authentication methods are available during session creation, providing the same flexibility as the ControllerProvider: ```typescript type AuthOptions = ( | "google" // Google OAuth | "webauthn" // WebAuthn/passkeys | "discord" // Discord OAuth | "twitter" // Twitter/X OAuth | "walletconnect" // WalletConnect | "metamask" // MetaMask | "password" // Email/Password | "sms" // SMS one-time passcode | "rabby" // Rabby Wallet )[]; ``` **Benefits of customizing authentication options:** * **Consistent UX**: Provide the same authentication methods across ControllerProvider and SessionProvider * **Targeted Experience**: Show only the authentication methods that work best for your application * **Platform Optimization**: Customize options based on platform (e.g., remove external wallets on mobile) * **Branding**: Focus on authentication methods that align with your user base **Example: Shared authentication configuration** ```typescript const signupOptions: AuthOptions = [ "google", "webauthn", "discord", "twitter", "walletconnect", "metamask", ]; // Use the same preset for both connectors const controller = new ControllerConnector({ preset: "my-game", // Using preset signupOptions, // other options... }); const session = new SessionConnector({ preset: "my-game", // Same preset for consistency rpc: "https://starknet-mainnet.public.blastapi.io/rpc/v0.7", chainId: "SN_MAIN", redirectUrl: "https://myapp.com/", signupOptions, // Same authentication options }); ``` ### Defining Policies Policies allow your application to define permissions that can be pre-approved by the user: ```typescript type SessionPolicies = { contracts: { [address: string]: ContractPolicy; // Contract interaction policies }; messages?: TypedDataMessage[]; // Optional signed message policies }; type ContractPolicy = { name?: string; // Human-readable name of the contract description?: string; // Description of the contract methods: Method[]; // Allowed contract methods }; type ContractMethod = { name: string; // Method name entrypoint: string; // Contract method entrypoint description?: string; // Optional method description spender?: string; // Required for approve methods: address authorized to spend amount?: string; // Required for approve methods: spending limit (hex format) }; type SignMessagePolicy = TypedDataPolicy & { name?: string; // Human-readable name of the policy description?: string; // Description of the policy }; type TypedDataPolicy = { types: Record; primaryType: string; domain: StarknetDomain; }; ``` ### Error Handling in Sessions When using session policies, Controller provides configurable error handling options through the `errorDisplayMode` setting. This works in conjunction with the existing `propagateSessionErrors` option to give you fine-grained control over how transaction errors are presented to users. #### Error Display Configuration ```typescript const controller = new Controller({ policies: sessionPolicies, errorDisplayMode: "notification", // "modal" | "notification" | "silent" propagateSessionErrors: false, // Optional: control error propagation }); ``` #### Error Display Modes **Modal Mode (Default)** * Transaction errors open the controller modal interface * Users see detailed error information and retry options * Preserves existing session error handling behavior **Notification Mode** * Transaction errors display as clickable toast notifications * Users can continue their session and address errors when convenient * Clicking the toast opens the modal for manual retry * Ideal for gaming applications where modal interruptions are disruptive **Silent Mode** * No UI is displayed for transaction errors * Errors are logged to console for programmatic handling * Applications must implement custom error handling logic #### Special Error Cases Certain session-related errors always display UI regardless of the `errorDisplayMode` setting: * **SessionRefreshRequired**: Always opens modal to refresh expired sessions * **ManualExecutionRequired**: Always opens modal when manual approval is needed These exceptions ensure users can complete required authentication flows even in silent mode. #### Error Handling Examples **Gaming Application with Minimal Interruptions** ```typescript const gameController = new Controller({ policies: gameSessionPolicies, errorDisplayMode: "notification", // Show clickable toast notifications }); // Transaction errors show as toast notifications // Players can continue gameplay and retry when convenient const account = gameController.account; await account.execute(gameMoves); // Failed moves show toast notifications ``` **DeFi Application with Detailed Error Handling** ```typescript const defiController = new Controller({ policies: tradingPolicies, errorDisplayMode: "modal", // Show detailed error modals propagateSessionErrors: false, }); // Transaction errors open detailed modal interface // Users get comprehensive error information for financial operations ``` **Custom Error Management** ```typescript const customController = new Controller({ policies: sessionPolicies, errorDisplayMode: "silent", propagateSessionErrors: true, // Errors are thrown for custom handling }); try { await account.execute(calls); } catch (error) { // Implement custom error UI and retry logic handleCustomErrorFlow(error); } ``` ### Disconnect Redirect The `disconnectRedirectUrl` option allows you to redirect users to a specific URL after they disconnect or logout from their session. This is particularly useful for: * **Mobile Apps**: Redirect users back to your mobile app using deep links (e.g., `"myapp://logout-complete"`) * **Web Apps**: Send users to a logout confirmation page or back to your landing page * **Cross-Platform**: Handle logout flows consistently across different platforms ```typescript const session = new SessionConnector({ policies, rpc: "https://starknet-mainnet.public.blastapi.io/rpc/v0.7", chainId: "SN_MAIN", redirectUrl: "https://myapp.com/", disconnectRedirectUrl: "whatsapp://", // Deep link example signupOptions: ["google", "webauthn", "discord"], // Optional: customize auth methods }); ``` When `disconnect()` is called, users will be redirected to the keychain logout page, and after successful logout, they will be automatically redirected to your specified URL. **Note**: If no `disconnectRedirectUrl` is provided, users will remain on the keychain logout page after disconnection. ### Manual Session Processing For native applications (like Capacitor apps), you may need to manually process session data from redirect URLs. The `ingestSessionFromRedirect()` method allows you to handle this: ```typescript const provider = new SessionProvider({ rpc: "https://api.cartridge.gg/x/starknet/sepolia", chainId: constants.StarknetChainId.SN_SEPOLIA, redirectUrl: "myapp://session", preset: "my-game", // Using preset instead of manual policies }); // Handle deep link with session data const handleDeepLink = async (url: string) => { const parsed = new URL(url); const sessionData = parsed.searchParams.get("startapp"); if (sessionData) { // Process the encoded session payload const session = provider.ingestSessionFromRedirect(sessionData); if (session) { // Session stored successfully const account = await provider.probe(); console.log("Session ready:", account?.address); } } }; ``` This is particularly useful for: * **Capacitor apps**: Processing deep links from authentication flows * **Native mobile apps**: Handling custom URL scheme redirects * **Manual session handling**: When you need custom control over session processing The method automatically decodes the session payload and stores it in localStorage for future use. ### Verified Sessions Verified session policies provide a better user experience by attesting to the validity of a game's policy configuration, giving confidence to the players. ![Verified Session](/verified-session.svg) **Enhanced Session Creation** When using verified session policies, the user experience is improved with enhanced trust indicators: * **Trust Indicators**: Verified sessions display clear verification badges and streamlined approval flows * **Enhanced Security**: Verified policies provide additional context and confidence to users during approval * **Consistent Experience**: All session creation flows require user approval to maintain security standards Both verified and unverified policies follow the same approval flow, with verified policies providing enhanced trust indicators and streamlined user interfaces. The session creation interface organizes permissions using expandable cards for better user comprehension. **Getting Verified** Verified configs can be committed to the `configs` folder in [`@cartridge/presets`](https://github.com/cartridge-gg/presets/tree/main/configs). Before they are merged, the team will need to collaborate with Cartridge to verify the policies. ### Usage Examples ##### Contract Interaction Policies Contract interaction policies allow the application to send contract transactions without manual approval from the user. ```typescript const policies: SessionPolicies = { contracts: { "0x4ed3a7...": { name: "Pillage", description: "Allows you to raid and pillage a structure", methods: [ { name: "Pillage Structure", description: "Pillage a structure", entrypoint: "pillage_structure" } ] }, "0x2620f6...": { name: "Battle", description: "Required to engage in battles", methods: [ { name: "Battle Start", description: "Start a battle", entrypoint: "battle_start" }, { name: "Battle Join", description: "Join a battle", entrypoint: "battle_join" }, ] }, // Include other contracts as needed } }; ``` ##### Token Spending Limits When defining `approve` methods in your contract policies, you can specify spending limits using the `amount` parameter. This creates a spending limit that users can see and approve during session creation. ```typescript const policies: SessionPolicies = { contracts: { // ETH contract with spending limit "0x049d36570d4e46f48e99674bd3fcc84644ddd6b96f7c741b1562b82f9e004dc7": { name: "Ethereum", methods: [ { name: "approve", entrypoint: "approve", spender: "0x1234567890abcdef1234567890abcdef12345678", // Address authorized to spend amount: "0x3", // Limit to 3 ETH (in wei, hex format) description: "Approve spending up to 3 ETH" }, { name: "transfer", entrypoint: "transfer" } ] }, // STRK contract with unlimited spending "0x04718f5a0Fc34cC1AF16A1cdee98fFB20C31f5cD61D6Ab07201858f4287c938D": { name: "Starknet Token", methods: [ { name: "approve", entrypoint: "approve", spender: "0xabcdef1234567890abcdef1234567890abcdef12", // Address authorized to spend amount: "*", // Unlimited (shorthand for max uint128) description: "Approve unlimited STRK spending" } ] } } }; ``` **Spending Limit Display** When users connect with spending limits configured, the session creation interface organizes permissions for better clarity: 1. **Contract Authorization**: Non-approve contract methods are grouped into an expandable "Authorize \[game]" card at the top 2. **Token Consent**: A consent notice explains the spending permissions 3. **Spending Limit Card**: Contracts with approve methods are displayed separately in a dedicated spending limit section: * Unverified policies show each token, its approved amount, USD value when price data is available, and clearly labeled unlimited limits * Verified policies show a compact summary with token count, token icons, and the total USD spending limit; if any token has an unlimited limit, the summary is labeled "Unlimited" This organization separates general contract permissions from token spending approvals, making it easier for users to understand what they're authorizing. **Required Fields for Approve Methods** * **`spender`**: The contract address authorized to spend tokens (required) * **`amount`**: The spending limit in hexadecimal format (required) * Both fields are required to create proper ApprovalPolicy objects and avoid deprecation warnings **Amount Format** * Use hexadecimal format (e.g., `"0x3"` for 3) * For unlimited spending, use `"*"` as a shorthand or the full hex value `"0xffffffffffffffffffffffffffffffff"` * For ERC20 tokens, amounts should account for token decimals * Maximum value for unlimited spending is `2^128 - 1` (`0xffffffffffffffffffffffffffffffff`) ##### Signed Message Policies Signed Message policies allow the application to sign a typed message without manual approval from the user. ```typescript const policies: SessionPolicies = { messages: [ { name: "Eternum Message Signing", description: "Allows signing messages for Eternum", types: { StarknetDomain: [ { name: "name", type: "shortstring" }, { name: "version", type: "shortstring" }, { name: "chainId", type: "shortstring" }, { name: "revision", type: "shortstring" } ], "s0_eternum-Message": [ { name: "identity", type: "ContractAddress" }, { name: "channel", type: "shortstring" }, { name: "content", type: "string" }, { name: "timestamp", type: "felt" }, { name: "salt", type: "felt" } ] }, primaryType: "s0_eternum-Message", domain: { name: "Eternum", version: "1", chainId: "SN_MAIN", revision: "1" } } ] }; ``` ## Signer Management Cartridge Controller supports **multi-signer** functionality, allowing you to add multiple authentication methods to your account for enhanced security and convenience. This feature is now generally available and enables you to sign in using different methods while maintaining access to the same Controller account and assets. ### Overview Multi-signer support provides several benefits: * **Flexibility**: Choose the authentication method that works best for each situation * **Security**: Distribute access across multiple secure authentication methods ### Supported Signer Types Controller supports five categories of signers: #### 1. Passkey (WebAuthn) * **Biometric authentication** using Face ID, Touch ID, or hardware security keys * **Platform-native** security with device-based credential storage * **Cross-platform** compatibility with password managers like Bitwarden, 1Password ##### Passkey Platform Support Passkeys are [generally well supported](https://www.passkeys.io/compatible-devices) across modern platforms. You can use them with: * Device authenticators directly * Mobile device pairing via QR code flow If your device does not natively support them, several password managers support passkey creation and management: * [Bitwarden (free)](https://bitwarden.com/help/storing-passkeys/) * [1Password](https://1password.com/) * [Dashlane](https://www.dashlane.com/) When using a password manager, be sure to install the browser extension as well. ##### Passkey Backup Passkey backup is handled differently depending on your platform or password manager: * **Apple Devices**: Passkeys are backed up with your keychain in iCloud. [Learn more](https://support.apple.com/en-us/102195) * **Android Devices**: Passkeys are backed up with your Google account. [Learn more](https://support.google.com/chrome/answer/13168025) * **Windows Devices**: Passkeys can be created and managed as part of your Windows account. [Learn more](https://learn.microsoft.com/en-us/windows/security/identity-protection/passkeys) #### 2. Password Authentication * **Password-based authentication** with encrypted private key storage * **Non-recoverable**: Password loss means permanent account loss * **Minimum requirements**: 8-character minimum password length > **⚠️ Important**: Password authentication is currently marked as "Testing Only" and should not be used for production applications. Password loss results in permanent account access loss as there are no recovery mechanisms. #### 3. SMS Authentication * **One-time passcodes** delivered by SMS * **Phone-based onboarding** for users who prefer using a phone number * **OTP verification** during signup and login #### 4. Social Login Controller offers native social login options through Google, Discord, and Twitter/X: * **Streamlined onboarding** for users with existing social accounts * **Secure integration** via Turnkey wallet infrastructure with Auth0 * **Native implementation** using OAuth2 flows for improved security and UX > **⚠️ Native App Limitation**: OAuth-based social login flows (Google, Discord, Twitter/X) may not work correctly in native applications that use webviews. > Many OAuth providers block or restrict authentication attempts from embedded webviews for security reasons. > For native integrations, consider using passkey authentication or see the [Native Integration](./native/overview) documentation for recommended approaches. All social login providers use an intelligent authentication flow that adapts to browser restrictions: 1. **Primary Method**: Attempts to open OAuth in a popup window for seamless experience 2. **Fallback Method**: Automatically redirects to OAuth provider when popups are blocked 3. **Error Handling**: Gracefully handles iframe restrictions and Content Security Policy (CSP) issues 4. **Nonce Security**: Implements proper OIDC token validation with nonce verification #### 5. External Wallets Controller offers integration with popular external web3 wallets, including Braavos, MetaMask, Rabby, Base, Phantom, and WalletConnect. ### Adding Signers #### Accessing Signer Management 1. Connect to your Controller account using any existing authentication method 2. Open the **Settings** panel within the Controller interface 3. Navigate to the **Signer(s)** section 4. Click **Add Signer** to begin adding a new authentication method > **Note**: Signer management is available on **Mainnet only**. The "Add Signer" button will be disabled on testnet environments. #### Adding a Passkey 1. In the Add Signer interface, select **Passkey** 2. Your browser will prompt you to create a new passkey using: * Device biometrics (Face ID, Touch ID, Windows Hello) * Hardware security key (USB, NFC, or Bluetooth) * Password manager (if configured for passkey storage) 3. Follow your device's authentication flow 4. Once created, the passkey will be added to your account #### Adding Password Authentication > **⚠️ Testing Only**: Password authentication is available but marked for testing purposes only. 1. In the signup or login interface, select **Password** from the authentication options 2. For new accounts: * Enter a password (minimum 8 characters) * Confirm your password by entering it again * Review the security warning about password recovery 3. For existing password accounts: * Simply enter your password to login * Password must match exactly (case-sensitive) > **Security Warning**: Password accounts cannot be recovered if you lose your password. This authentication method does not provide any recovery mechanisms, making it unsuitable for production use. #### Adding SMS Authentication 1. In the signup or login interface, select **SMS** from the authentication options 2. Enter your phone number when prompted 3. Enter the one-time passcode sent by SMS to complete verification 4. For future logins, use the same phone number and OTP flow #### Adding Social Login ##### Adding Google Login 1. Select **Google** from the signer options 2. The system uses an intelligent OAuth flow: * **In iframe environments**: Opens Google OAuth in a popup window for seamless UX * **Standard environments**: Uses redirect flow for better compatibility * **Fallback handling**: Automatically switches to redirect if popup is blocked 3. Complete the Google OAuth authorization: * Sign in to your Google account if not already logged in * Authorize Cartridge Controller to access your Google identity 4. The system creates a secure Turnkey wallet linked to your Google account 5. Your Google login is now available as a Controller authentication method > **Technical Details**: The implementation uses Auth0 for OAuth management with Turnkey for secure wallet creation. OIDC tokens are validated with proper nonce verification to prevent replay attacks. ##### Adding Discord Login 1. Select **Discord** from the signer options 2. The system uses the same intelligent OAuth flow as Google: * **Popup-first approach**: Attempts popup for seamless authentication * **Redirect fallback**: Automatically falls back to full redirect when necessary * **Browser compatibility**: Handles CSP restrictions and iframe limitations 3. Complete the Discord OAuth authorization: * Sign in to your Discord account if not already logged in * Authorize Cartridge Controller to access your Discord identity 4. The system creates a secure Turnkey wallet linked to your Discord account 5. Your Discord login is now available as a Controller authentication method > **Technical Details**: Discord authentication uses the same Auth0 + Turnkey infrastructure as Google login, ensuring consistent security and user experience across all social providers. ##### Adding Twitter/X Login 1. Select **Twitter** from the signer options 2. The system uses the same intelligent OAuth flow as other social providers: * **Popup-first approach**: Attempts popup for seamless authentication * **Redirect fallback**: Automatically falls back to full redirect when necessary * **Browser compatibility**: Handles CSP restrictions and iframe limitations 3. Complete the Twitter OAuth authorization: * Sign in to your Twitter/X account if not already logged in * Authorize Cartridge Controller to access your Twitter identity 4. The system creates a secure Turnkey wallet linked to your Twitter account 5. Your Twitter login is now available as a Controller authentication method > **Technical Details**: Twitter authentication uses the same Auth0 + Turnkey infrastructure as Google and Discord login, ensuring consistent security and user experience across all social providers. #### Adding External Wallets 1. Select **Wallet** to see external wallet options 2. Choose from the supported wallet types: * **Argent**: Starknet-native wallet with advanced security features and account management * **Braavos**: Starknet-native wallet with built-in security features * **MetaMask**: Popular browser extension wallet (desktop only) * **Phantom**: Multi-chain wallet supporting Solana, Ethereum, and other networks (desktop only) * **Rabby**: Security-focused multi-chain wallet (desktop only) * **Base**: Coinbase's official wallet with multi-chain support (desktop only) * **Phantom**: Multi-chain wallet with EVM-compatible mode support (desktop only) * **WalletConnect**: Use QR code or deep link to connect mobile/desktop wallets (desktop only) 3. Follow the wallet-specific connection flow 4. Sign the verification message to link the wallet to your account > **Mobile Limitation**: Ethereum-based wallets (MetaMask, Phantom, Rabby, Base, WalletConnect) will not appear as options on mobile browsers and are automatically filtered out for better mobile user experience. For information about programmatic authentication with external wallets, see [Headless Authentication](./headless-authentication). ### Managing Existing Signers #### Viewing Your Signers The Signer(s) section displays all authentication methods associated with your account: * **Signer type** with recognizable icons (fingerprint for passkey, Discord logo, wallet icons) * **Current status** indicating which signer you're currently using * **Identifying information** such as wallet addresses (partially masked for privacy) #### Signer Information Display Each signer card shows: * **Type**: Passkey, Password, SMS, Google, Discord, Twitter, Argent, Braavos, MetaMask, Phantom, Rabby, or WalletConnect * **Status**: "(current)" label for the active authentication method * **Identifier**: Shortened wallet address for external wallets, or authentication type for others #### Switching Between Signers When connecting to your Controller: * The connection interface will show all available authentication methods * Select any of your registered signers to authenticate * Your account and assets remain the same regardless of which signer you use #### Account Synchronization for Starknet Wallets Cartridge Controller automatically stays synchronized with account changes in connected Starknet wallets (Argent and Braavos). This ensures that when users switch accounts within their external wallet, the Controller is immediately updated to reflect the new active account. **Automatic Synchronization Features:** * **Real-time Updates**: Controller automatically detects when users switch accounts in Argent or Braavos wallets * **Seamless Experience**: No manual reconnection required when switching accounts * **Memory Management**: Proper cleanup of event listeners to prevent memory leaks * **Connection Reliability**: Automatic listener re-establishment on reconnection **How It Works:** 1. When connecting an Argent or Braavos wallet, Controller registers an account change listener 2. The listener monitors the wallet's `accountsChanged` events 3. When an account switch is detected, Controller updates its internal state 4. Connected accounts list and active account are automatically synchronized 5. On disconnect, listeners are properly cleaned up to prevent memory issues > **Note**: Account synchronization is currently available for Starknet wallets (Argent and Braavos). Other external wallets maintain their existing connection behavior. #### Chain Switching for External Wallets External wallets (MetaMask, Rabby, Base, Phantom, WalletConnect) support programmatic chain switching through the Controller interface. This allows applications to request that connected external wallets switch to a specific blockchain network. **Supported Functionality:** * **Automatic Chain Switching**: Applications can programmatically request external wallets to switch chains * **Cross-Chain Compatibility**: Works with Ethereum, Starknet, and other supported networks **How It Works:** 1. Your application calls the chain switch method through the Controller 2. The request is forwarded to the connected external wallet 3. The wallet handles the chain switching process (may show user confirmation) 4. The application receives confirmation of the successful chain switch **Example Usage:** ```typescript // Switch connected external wallet to a different chain const success = await controller.externalSwitchChain( walletType, // e.g., "metamask", "rabby", "base", "phantom" chainId // Target chain identifier ); ``` **Wallet-Specific Limitations:** * **Braavos**: Does not support the `wallet_switchStarknetChain` API. Chain switching requests are ignored, and the wallet remains on its current chain. * **Other Wallets**: Chain switching availability depends on the specific external wallet's capabilities and the target chain support. #### Transaction Confirmation for External Wallets External wallets (MetaMask, Rabby, Phantom, Argent, WalletConnect) support waiting for transaction confirmations through the Controller interface. This allows applications to monitor transaction status and receive confirmation when transactions are mined. **Supported Functionality:** * **Transaction Monitoring**: Wait for transaction confirmations with configurable timeouts * **Receipt Retrieval**: Returns transaction receipt upon successful confirmation **How It Works:** 1. Your application calls the wait method through the Controller after sending a transaction 2. The request is forwarded to the connected external wallet 3. The wallet polls the blockchain for transaction confirmation 4. The application receives the transaction receipt or timeout error **Example Usage:** ```typescript // Wait for transaction confirmation with default timeout (60s) const response = await controller.externalWaitForTransaction( walletType, // e.g., "metamask", "rabby", "phantom" txHash // Transaction hash from sendTransaction ); // Wait with custom timeout (30 seconds) const responseWithTimeout = await controller.externalWaitForTransaction( walletType, txHash, 30000 // 30 seconds in milliseconds ); ``` **Return Format:** ```typescript interface ExternalWalletResponse { success: boolean; wallet: string; result?: any; // Transaction receipt when successful error?: string; // Error message when failed account?: string; // Connected account address } ``` **Error Handling:** * **Connection Errors**: Wallet not available or not connected * **Timeout Errors**: Transaction not confirmed within the specified time * **Network Errors**: RPC or blockchain connectivity issues * **Transaction Failures**: Transaction reverted or failed on-chain **Note:** Transaction confirmation times vary by network conditions and the specific blockchain. Ethereum transactions typically confirm faster than other networks during low congestion periods. ### Security Considerations #### Best Practices * **Multiple Backups**: Add at least 2-3 different signer types to ensure account recovery * **Secure Storage**: For passkeys, ensure your device backup (iCloud, Google) is secure * **Regular Access**: Periodically test each authentication method to ensure they work #### Account Recovery If you lose access to your primary authentication method: 1. Use any other registered signer to access your account 2. Consider adding additional backup authentication methods 3. Remove compromised signers using the remove signer functionality #### Removing Signers You can now remove signers from your account for security or convenience: 1. Navigate to the **Signer(s)** section in Controller Settings 2. Find the signer you want to remove 3. Click the **Remove** option for that signer > **Important**: Ensure you have at least one other working authentication method before removing a signer to avoid losing access to your account. #### Deleting Your Account You can permanently delete your Controller account from the Settings panel: 1. Navigate to **Settings** in the Controller interface 2. Scroll to the bottom to find the **Delete Account** section 3. Click **Delete Account** to open the confirmation sheet 4. Type your exact username to confirm deletion 5. Click **DELETE** to permanently remove your account > **Warning**: Account deletion is permanent and irreversible. This action will delete all controllers, sessions, and associated data. Ensure you have withdrawn any assets before deleting your account. #### Current Limitations * **Mainnet Only**: Signer management is currently restricted to Mainnet * **No Hierarchy**: All signers have equal access; there's no primary/secondary distinction #### Getting Help If you encounter issues with signer management: * Review the passkey section above for WebAuthn-specific help * Verify your wallet setup in the respective wallet's documentation * Ensure you're using a supported browser and have the latest wallet extensions installed ### Developer Integration #### Social Login Technical Implementation For developers integrating Controller's social login, the implementation includes: **Authentication Flow:** ```typescript // Controller automatically handles social login based on environment const controller = new Controller({ // Supports various AuthOptions including social login and external wallets signupOptions: ["webauthn", "google", "discord", "twitter", "phantom-evm", "password", "sms"] }); // Social providers are automatically available in connection flow await controller.connect(); ``` ### Social Connections (OAuth) In addition to managing authentication signers, Controller allows you to connect social media accounts for enhanced platform features. Social connections are separate from authentication signers and are used specifically for content publishing and social integrations. #### Overview Social connections enable: * **Content Publishing**: Connect TikTok to enable video publishing features * **Profile Integration**: Display social profile information within Controller * **Cross-Platform Features**: Unified social identity across gaming experiences > **Note**: Social connections are currently behind a feature flag and being rolled out gradually. This feature may not be available to all users immediately. #### Supported Social Platforms ##### TikTok Controller supports TikTok OAuth integration for content creators and users who want to publish gaming content directly to TikTok. **Features:** * **OAuth Authentication**: Secure connection via TikTok's official OAuth flow * **Profile Information**: Display TikTok username and avatar * **Connection Status**: Monitor connection health and token expiration * **Content Publishing**: Enable video publishing capabilities (when available) #### Managing Social Connections ##### Accessing Social Connections 1. Connect to your Controller account using any authentication method 2. Open the **Settings** panel within the Controller interface 3. Navigate to the **Connected Accounts** section (appears next to Signers when available) 4. View your existing connections or add new ones ##### Adding Social Connections **Connecting TikTok:** 1. In the Connected Accounts section, click **Connect Socials** 2. You'll be redirected to the Add Connection interface 3. Select **TikTok** from the available social platforms 4. Click the TikTok connection button to initiate OAuth flow 5. A popup window will open with TikTok's authorization page 6. Sign in to your TikTok account and authorize Cartridge Controller 7. Complete the authorization process in the popup 8. Once successful, you'll be redirected back to Settings with your TikTok account connected **OAuth Flow Details:** * Opens TikTok authentication in a secure popup window * Uses official TikTok OAuth 2.0 flow for maximum security * Requests minimal necessary permissions for content publishing * Stores secure tokens for ongoing API access ##### Managing Existing Connections **Viewing Connected Accounts:** The Connected Accounts section displays: * **Platform Icon**: Visual identifier for the connected social platform * **Profile Information**: Username and avatar from the connected account * **Connection Status**: Active connections and any expiration warnings * **Account Details**: Partially masked account information for privacy **Connection Status Indicators:** Each connected account shows its current status: * **Active**: Connection is healthy and ready for use * **Expired**: OAuth token has expired and requires reconnection * **Error**: Connection encountered an issue and may need attention **Disconnecting Social Accounts:** To remove a social connection: 1. Navigate to the **Connected Accounts** section in Settings 2. Find the social account you want to disconnect 3. Click on the account card to open connection details 4. Select **Disconnect** from the options 5. Confirm the disconnection when prompted > **Important**: Disconnecting a social account will remove access to associated publishing features and may affect content that was previously shared through the platform. #### Privacy and Security ##### OAuth Security * **Secure Token Storage**: All OAuth tokens are encrypted and stored securely * **Minimal Permissions**: Controller requests only the permissions necessary for enabled features * **Token Expiration**: Regular token refresh ensures ongoing security * **Revocation Support**: Users can disconnect accounts at any time to revoke access ##### Data Handling * **Profile Information**: Only public profile data (username, avatar) is stored * **No Content Access**: Controller cannot access private content or personal information * **User Control**: All social connections are user-initiated and user-managed #### Troubleshooting Social Connections ##### Connection Issues **Popup Blocked:** * Ensure your browser allows popups for the Controller domain * Try disabling popup blockers temporarily during connection * Some browsers may require clicking the connection button directly (not programmatically) **Authorization Failed:** * Check that you're signed in to the social platform account you want to connect * Ensure your social account has the necessary permissions to authorize third-party apps * Try clearing your browser cache and cookies for the social platform **Connection Expired:** * Navigate to Connected Accounts in Settings * Look for expired connection indicators * Click on the expired connection and select "Reconnect" * Complete the OAuth flow again to refresh the connection ##### Feature Availability If you don't see the Connected Accounts section: * The feature may be behind a feature flag that's not yet enabled for your account * Check back later as the feature is being rolled out gradually * Ensure you're using the latest version of Controller #### Developer Integration For developers looking to integrate with social connections: ##### Feature Detection ```typescript import { useFeatures } from "@/hooks/features"; const SocialSection = () => { const { isFeatureEnabled } = useFeatures(); if (!isFeatureEnabled("connections")) { return null; // Feature not available } // Render social connections UI return ; }; ``` ##### Connection Management The social connections feature uses GraphQL queries for managing connections: ```typescript // Query user's OAuth connections const GET_OAUTH_CONNECTIONS = gql` query GetOAuthConnections($username: String!) { account(username: $username) { oauthConnections { id provider profile { providerUserId username avatarUrl } isExpired createdAt updatedAt } } } `; // Disconnect an OAuth connection const DISCONNECT_OAUTH = gql` mutation DisconnectOAuth($provider: OAuthProvider!) { disconnectOAuth(provider: $provider) } `; ``` ### Next Steps * Learn about [Session Keys](./sessions) for gasless gaming transactions * Explore [Controller Configuration](./configuration) options * Set up [Usernames](./usernames) for your account ## Starter Packs Starter packs are pre-configured bundles of game assets, NFTs, and in-game currency that provide a seamless onboarding and monetization experience for your players. Cartridge Controller makes it easy to offer both paid starter packs and free claimable packs with support for multiple payment methods across different blockchain networks. ### Overview Starter packs enable you to: * **Create Custom Bundles**: Configure packs with fungible tokens, NFTs, and on-chain items with automatic contract execution * **Offer Paid Packs**: Accept payments via cryptocurrency across Ethereum, Base, Arbitrum, and Optimism * **Enable Free Claims**: Distribute free packs using Merkle Drop technology with cross-chain eligibility verification * **Flexible Configuration**: Build packs programmatically or reference pre-configured packs by ID * **Multichain Payment Support**: Unified payment interface with automatic token bridging via Layerswap * **Multiple Wallet Integration**: Support for popular wallets with automatic chain switching where supported * **NFT Marketplace Support**: ERC721 and ERC1155 listing and purchase capabilities with integrated fee structure ### Quick Start Opening a starter pack or bundle interface is straightforward: ```typescript import Controller from "@cartridge/controller"; const controller = new Controller(); // Open an existing starter pack by ID (works for both paid and claimed packs) controller.openStarterPack("starterpack-id-123"); // Numeric IDs are also supported for onchain starter packs controller.openStarterPack(42); // Open a bundle with social claim support (new in v2) await controller.openBundle(0, "0x1c53584fdbebd996c163fa2d5d5ad37f4b2f06643ea2bb897c5bee578a2e715"); ``` ### API Reference #### openBundle(bundleId: number, registryAddress: string, options?: BundleOptions) Opens the bundle interface for a specific starter pack bundle with advanced features including conditional claiming. Bundles support social claim flows where users can claim packs by completing social actions (e.g., following and sharing on X/Twitter). ```typescript controller.openBundle(bundleId: number, registryAddress: string, options?: BundleOptions); ``` **Parameters:** * `bundleId` (number): The bundle ID registered in the onchain registry * `registryAddress` (string): The contract address of the bundle registry * `options` (BundleOptions, optional): Configuration options for the bundle **BundleOptions:** * `onPurchaseComplete` (function, optional): Callback fired after the Play button closes the bundle modal * `socialClaimOptions` (object, optional): Options for social claim conditional bundles * `shareMessage` (string): Custom message to share on social media **Returns:** `Promise` **Usage Examples:** ```typescript // Open a bundle with social claim flow const handleSocialBundle = async () => { const username = await controller.username(); await controller.openBundle( 0, // bundleId "0x1c53584fdbebd996c163fa2d5d5ad37f4b2f06643ea2bb897c5bee578a2e715", // registry address { onPurchaseComplete: () => { console.log("Bundle claimed!"); }, socialClaimOptions: { shareMessage: `Check out this game!\nhttps://game.example.com/?ref=${username}` } } ); }; // Open a bundle without social claim const handleBundle = async () => { await controller.openBundle( 42, // bundleId "0x1c53584fdbebd996c163fa2d5d5ad37f4b2f06643ea2bb897c5bee578a2e715", { onPurchaseComplete: () => { console.log("Bundle purchase completed!"); } } ); }; ``` #### openStarterPack(starterpackId: string | number, options?: StarterpackOptions) Opens the starter pack interface for a specific starter pack bundle. This method works for both paid starter packs (requiring purchase) and claimed starter packs (that can be claimed based on eligibility). ```typescript controller.openStarterPack(starterpackId: string | number, options?: StarterpackOptions); ``` **Parameters:** * `starterpackId` (string | number): The starter pack ID. String IDs are used for claimed packs, numeric IDs for onchain packs * `options` (StarterpackOptions, optional): Configuration options for the starter pack **StarterpackOptions:** * `preimage` (string, optional): The preimage to use for claimed starter packs * `onPurchaseComplete` (function, optional): Callback fired after the Play button closes the starter pack modal **Returns:** `void` **Usage Examples:** ```typescript // Open a paid starter pack for purchase const handleBuyStarterpack = () => { controller.openStarterPack("beginner-pack-2024"); }; // Open a starter pack with play callback const handleBuyWithCallback = () => { controller.openStarterPack("beginner-pack-2024", { onPurchaseComplete: () => { console.log("Starter pack purchase completed!"); // Redirect to game or refresh inventory } }); }; // Open a free claimable starter pack with preimage const handleClaimStarterpack = () => { controller.openStarterPack("free-welcome-pack-2024", { preimage: "claim-preimage-data" }); }; // Open an onchain starter pack using numeric ID const handleOnchainStarterpack = () => { controller.openStarterPack(42); // Numeric ID for onchain pack }; ``` ### Starter Pack Configuration Starter packs are registered onchain through the [Arcade starter pack registry](/arcade/starter-packs) and referenced by numeric ID. The Controller SDK provides a simple interface to open these registered packs. To create your own starter pack, see [Creating Starter Packs](/arcade/starter-packs). #### Starter Pack Types **Claimed Starter Packs (String IDs):** * Use UUID-like string identifiers (e.g., "free-welcome-pack-2024") * Typically free packs distributed via merkle drops * Support cross-chain claiming from various networks **Onchain Starter Packs (Numeric IDs):** * Use numeric identifiers (e.g., 42) * Paid packs with smart contract execution * Support multiple payment methods and automatic contract calls #### Key Features * **Pre-configured**: Packs are set up through the Cartridge platform with predefined items and pricing * **Cross-chain Support**: Automatic token bridging and multi-network compatibility * **Smart Contract Integration**: Automatic execution of associated contract calls after payment * **Unified Interface**: Single method works for both paid and claimed packs * **Platform Managed**: No need to define complex item structures in your code * **Additional Payment Tokens**: Support for custom payment options beyond default ETH, STRK, and USDC through starter pack metadata configuration #### Paid Starter Packs Paid starter packs require purchase and support cryptocurrency payments. These typically include premium game assets, larger credit bundles, and exclusive items. Cross-chain crypto payments are powered by Layerswap. :::note Credit card payments via Coinflow are available in sandbox mode. Production credit card payments will be enabled in a future update. ::: #### Claimed Starter Packs Free starter packs that users can claim based on eligibility criteria. These starter packs: * **No payment required**: Users can claim them for free * **Eligibility checking**: System verifies if user meets claim requirements * **Collection showcase**: Display supported game collections with platform indicators * **Mint limits**: May have limited quantities or per-user claiming restrictions * **Cross-chain Claims**: Claims can originate from multiple blockchain networks and be delivered to Starknet The claiming flow automatically determines eligibility and guides users through the appropriate network selection for receiving their assets. #### Social Claim Bundles Bundles can include conditional claiming flows that require users to complete social actions before claiming. The social claim flow: * **Social Connection**: Users connect their social media account (e.g., X/Twitter) * **Follow Action**: Users follow a specified account * **Share Action**: Users share a custom message with their network * **Automatic Verification**: System verifies completion of all steps before allowing claim Use `controller.openBundle()` with `socialClaimOptions` to enable social claim flows. See the API Reference section for usage examples. ##### Merkle Drop Claims Claimable starter packs use **Merkle Drop** technology to enable secure, verifiable claims across multiple blockchain networks. This system allows users to claim assets that were originally distributed on other networks and receive them in their Cartridge account on Starknet. **DevConnect Integration**: Cartridge supports DevConnect booster pack claims through the Merkle claim system, allowing users to claim DevConnect rewards using preimage-derived EVM addresses. This enables seamless cross-chain reward distribution for DevConnect participants. **How Merkle Drop Claims Work:** 1. **Eligibility Verification**: The system checks if the user's external wallet address is included in the merkle tree for the starter pack 2. **Cryptographic Proof**: Claims are validated using merkle proofs that mathematically prove eligibility without revealing the entire distribution list 3. **Cross-chain Signature**: For EVM-based claims, users must sign a message with their external wallet to prove ownership 4. **Forwarder Contract**: Claims are processed through a forwarder contract on Starknet that verifies the proof and signature before distributing assets **Supported Networks for Claims:** * **Starknet**: Native claims without additional signature requirements * **Ethereum Mainnet/Testnet**: MetaMask, Rabby, Coinbase Wallet supported * **Base Mainnet/Testnet**: MetaMask, Rabby, Coinbase Wallet supported * **Arbitrum One/Testnet**: MetaMask, Rabby, Coinbase Wallet supported * **Optimism Mainnet/Testnet**: MetaMask, Rabby, Coinbase Wallet supported :::note Solana payment functionality is currently disabled and will be re-enabled in a future update. ::: ### Purchase and Claim Flows #### Purchase Flow (Paid Starter Packs) The purchase process follows these steps: 1. **Item Selection**: User selects starter pack or credit amount 2. **Streamlined Checkout**: Improved onchain starter pack purchase flow with direct navigation, removing intermediate screens and defaulting to controller wallet for faster transactions 3. **Wallet Selection Drawer**: Enhanced onchain checkout with inline slide-up drawer for wallet selection, replacing navigation-based flow for more seamless UX 4. **Payment Method & Network Selection**: Choose from available options on a unified screen: * **Cryptocurrency**: Pay with Crypto from Ethereum, Base, Arbitrum, or Optimism * **Coinbase Onramp**: Integrated fiat-to-crypto onramp, including Apple Pay for eligible US users, with automatic client IP detection for order creation and transaction queries 5. **Wallet Connection**: Connect external wallet with automatic chain switching (supported on MetaMask, Rabby, Base, and WalletConnect) 6. **Cross-Chain Bridging**: Layerswap automatically handles token bridging to Starknet if needed 7. **Transaction Processing**: Complete payment through selected method with automatic bridging fees calculation 8. **Confirmation**: Receive purchase confirmation and assets in your Cartridge account ### Cross-Chain Bridging with Layerswap Cartridge uses Layerswap to enable seamless cross-chain payments. When users pay with cryptocurrency from supported networks (Ethereum, Base, Arbitrum, or Optimism), Layerswap automatically bridges the tokens to your Cartridge account on Starknet. #### Wallet Chain Switching Behavior During the payment process, Controller attempts to automatically switch connected wallets to the optimal chain for the transaction: * **MetaMask, Rabby, Base, WalletConnect**: Support automatic chain switching via the `wallet_switchStarknetChain` API * **Braavos**: Does not support automatic chain switching and will remain on the currently connected chain If a wallet doesn't support chain switching, users can manually switch chains within their wallet before completing the transaction. #### Fee Structure Cryptocurrency payments include several fee components: * **Base Cost**: The actual purchase amount (starter pack or credit value) * **Layerswap Bridging Fee**: Variable fee based on source network and token (typically 0.1-0.5%) * **Network Gas Fees**: Standard blockchain transaction fees (paid separately by user) The total cost including all fees is displayed upfront before payment confirmation. ##### NFT Marketplace Fees For ERC721 and ERC1155 marketplace transactions, additional fees apply: * **Marketplace Fee**: Variable fee set by the marketplace platform * **Creator Royalties**: Fees paid to the original creator of the NFT (if applicable) * **Client Fee**: Processing fee calculated using configurable numerator/denominator ratios, automatically applied to marketplace transactions These fees are transparently displayed in the purchase interface before transaction confirmation, including percentage breakdowns and total amounts. #### Claim Flow (Free Starter Packs) The claiming process follows these steps: 1. **Starter Pack Selection**: User opens a claimable starter pack 2. **Eligibility Check**: System automatically verifies claim eligibility and mint limits 3. **Collection Preview**: View supported game collections and platform compatibility 4. **Network & Wallet Selection**: Choose the blockchain network where your claim originated and connect the corresponding wallet 5. **Signature Verification**: For EVM-based claims, sign a verification message with your external wallet to prove ownership 6. **Merkle Proof Validation**: System validates your claim using cryptographic merkle proofs 7. **Claim Processing**: Complete the free claim transaction via the forwarder contract on Starknet 8. **Confirmation**: Receive claim confirmation and assets in your Cartridge account ### Gasless Transactions Starter packs enable gasless gaming experiences through integration with [session policies](/controller/sessions). When properly configured, users can receive and interact with their starter pack assets without paying gas fees for each transaction. ### Credit Purchases In addition to starter packs, Controller provides direct credit purchase functionality for topping up user accounts with credits for gasless transactions and other platform services. #### openPurchaseCredits() Opens the credit purchase interface where users can buy credits using the same payment methods available for starter packs, including cryptocurrency and eligible Coinbase fiat options such as Apple Pay. ```typescript controller.openPurchaseCredits(); ``` **Parameters:** None **Returns:** `void` **Usage Example:** ```typescript // Add a "Buy Credits" button in your game's UI const handleBuyCredits = () => { controller.openPurchaseCredits(); }; ``` Credits purchased through this interface use the same unified payment flow as starter packs, including support for multiple blockchains, automatic token bridging, and available fiat and crypto payment options. See [Coinbase Onramp](/controller/coinbase-onramp) for Apple Pay and regional fiat availability. ### Getting Help If you encounter issues with purchase integration: * Check the browser console for detailed error messages * Verify your Controller setup matches the [getting started guide](/controller/getting-started) * Ensure you're using the latest version of the Controller SDK * Review [external wallet setup](/controller/signer-management) for wallet-related issues ### Next Steps * Learn about [Sessions](/controller/sessions) for gasless gaming experiences * Explore [Controller Configuration](/controller/configuration) options * Set up [External Wallet Integration](/controller/signer-management) * Review [Paymaster Configuration](/services/paymaster) for gasless transactions ## Toast Notifications The Controller SDK provides a built-in toast notification API that enables you to display contextual, user-friendly notifications directly within the Controller interface. This API supports various notification types including transaction updates, achievements, network changes, and marketplace activities. ### Overview The toast API allows you to: * **Display Transaction Status**: Show transaction confirmation, pending, and error states * **Network Switch Notifications**: Notify users of network changes with visual indicators * **Achievement Celebrations**: Showcase earned achievements with XP amounts and visual flair * **Quest Notifications**: Display quest completion and claiming status * **Marketplace Activities**: Show purchase confirmations with item details * **Error Handling**: Display user-friendly error messages ### Quick Start ```typescript import { toast } from "@cartridge/controller"; // Display a simple error message toast({ variant: "error", message: "Transaction failed", }); // Show transaction confirmation toast({ variant: "transaction", status: "confirming", isExpanded: true, }); ``` ### API Reference #### toast(options: ToastOptions) The main toast function accepts a `ToastOptions` object with variant-specific properties. ```typescript import { toast } from "@cartridge/controller"; toast(options: ToastOptions); ``` ### Toast Variants #### Error Toast Display error messages with clear visual indicators. Error toasts can be made clickable to provide additional functionality. ```typescript // Basic error toast toast({ variant: "error", message: "Transaction failed", }); // Clickable error toast (used with errorDisplayMode: "notification") toast({ variant: "error", message: "Transaction failed", onClick: () => { // Handle click action (e.g., open modal for retry) console.log("Error toast clicked"); }, }); ``` **Properties:** * `variant: "error"` - Sets the toast type to error * `message: string` - The error message to display * `onClick?: () => void` - Optional click handler for interactive error toasts **Interactive Error Toasts:** * When `onClick` is provided, the error toast becomes clickable with hover states * Commonly used with `errorDisplayMode: "notification"` to allow users to retry failed transactions * The toast automatically dismisses when clicked to prevent duplicate interactions #### Transaction Toast Show transaction status updates with real-time progress indicators. ```typescript toast({ variant: "transaction", status: "confirming", isExpanded: true, }); ``` **Properties:** * `variant: "transaction"` - Sets the toast type to transaction * `status: string` - Transaction status (e.g., "confirming", "confirmed", "failed") * `isExpanded?: boolean` - Whether to show expanded view with more details #### Network Switch Toast Notify users of network changes with network branding. ```typescript toast({ variant: "network-switch", networkName: "Starknet Mainnet", networkIcon: "https://example.com/starknet-icon.png", }); ``` **Properties:** * `variant: "network-switch"` - Sets the toast type to network switch * `networkName: string` - Display name of the new network * `networkIcon?: string` - URL to the network's icon image #### Achievement Toast Celebrate user achievements with XP amounts and visual effects. ```typescript toast({ variant: "achievement", title: "First Achievement!", subtitle: "Earned!", xpAmount: 50, isDraft: true, }); ``` **Properties:** * `variant: "achievement"` - Sets the toast type to achievement * `title: string` - Achievement title * `subtitle?: string` - Achievement subtitle or description * `xpAmount?: number` - XP amount earned (displays with special formatting) * `isDraft?: boolean` - Whether this is a draft/preview achievement #### Quest Toast Display quest completion and claiming notifications. ```typescript toast({ variant: "quest", title: "First Quest!", subtitle: "Claimed!", }); ``` **Properties:** * `variant: "quest"` - Sets the toast type to quest * `title: string` - Quest title * `subtitle?: string` - Quest status or description #### Marketplace Toast Show marketplace purchase confirmations with item details. ```typescript toast({ variant: "marketplace", action: "purchased", itemName: "Cool NFT #123", itemImage: "https://picsum.photos/seed/adventurer/200/200", }); ``` **Properties:** * `variant: "marketplace"` - Sets the toast type to marketplace * `action: string` - Action performed (e.g., "purchased", "listed", "sold") * `itemName: string` - Name of the item involved in the transaction * `itemImage?: string` - URL to the item's image ### Usage Examples #### Transaction Flow Integration ```typescript import { toast } from "@cartridge/controller"; async function handleTransaction() { try { // Show pending state toast({ variant: "transaction", status: "pending", isExpanded: false, }); const txHash = await executeTransaction(); // Show confirming state toast({ variant: "transaction", status: "confirming", isExpanded: true, }); await waitForTransaction(txHash); // Show success (transaction toast automatically handles success) toast({ variant: "transaction", status: "confirmed", isExpanded: false, }); } catch (error) { // Show error toast({ variant: "error", message: "Transaction failed: " + error.message, }); } } ``` #### Achievement System Integration ```typescript import { toast } from "@cartridge/controller"; function onAchievementEarned(achievement: Achievement) { toast({ variant: "achievement", title: achievement.name, subtitle: "Achievement Unlocked!", xpAmount: achievement.xpReward, isDraft: false, }); } ``` #### Marketplace Purchase Flow ```typescript import { toast } from "@cartridge/controller"; async function handlePurchase(item: MarketplaceItem) { try { await purchaseItem(item); toast({ variant: "marketplace", action: "purchased", itemName: item.name, itemImage: item.imageUrl, }); } catch (error) { toast({ variant: "error", message: "Purchase failed", }); } } ``` #### Demo Implementation Here's a complete demo showing all toast variants with timing: ```typescript import { toast } from "@cartridge/controller"; function runToastDemo() { // Error toast toast({ variant: "error", message: "Transaction failed", }); // Transaction toast (1 second later) setTimeout(() => { toast({ variant: "transaction", status: "confirming", isExpanded: true, }); }, 1000); // Network switch toast (2 seconds later) setTimeout(() => { toast({ variant: "network-switch", networkName: "Starknet Mainnet", networkIcon: "https://imagedelivery.net/0xPAQaDtnQhBs8IzYRIlNg/1b126320-367c-48ed-cf5a-ba7580e49600/logo", }); }, 2000); // Achievement toast (3 seconds later) setTimeout(() => { toast({ variant: "achievement", title: "First Achievement!", subtitle: "Earned!", xpAmount: 50, isDraft: true, }); }, 3000); // Quest toast (4 seconds later) setTimeout(() => { toast({ variant: "quest", title: "First Quest!", subtitle: "Claimed!", }); }, 4000); // Marketplace toast (5 seconds later) setTimeout(() => { toast({ variant: "marketplace", action: "purchased", itemName: "Cool NFT #123", itemImage: "https://picsum.photos/seed/adventurer/200/200", }); }, 5000); } ``` #### Error Notification Integration The toast API integrates with the Controller's error display system. See [Configuration](/controller/configuration) for details on `errorDisplayMode` settings. ```typescript import { Controller } from "@cartridge/controller"; // Configure controller to use notification error display mode const controller = new Controller({ errorDisplayMode: "notification", // Show clickable error toasts // other options... }); // When transactions fail, Controller automatically displays clickable error toasts // Users can click the toast to open the modal and retry the transaction const account = controller.account; try { await account.execute(calls); } catch (error) { // With notification mode, error toasts are shown automatically // Users can click the toast to retry via the controller modal } ``` **Error Notification Flow:** 1. Transaction fails during execution 2. Controller displays a clickable error toast with the error message 3. User can click the toast to open the controller modal 4. Modal allows manual retry of the failed transaction 5. Toast automatically dismisses to prevent duplicate clicks ### Best Practices #### Timing and User Experience * **Avoid Toast Spam**: Don't display multiple toasts simultaneously that could overwhelm users * **Appropriate Duration**: Error messages should stay visible longer than success messages * **Progressive Disclosure**: Use `isExpanded` appropriately for transaction toasts * **Click Responsiveness**: For interactive error toasts, ensure click actions are clear and immediate #### Visual Design * **Consistent Branding**: Use appropriate network icons and maintain visual consistency * **Clear Messaging**: Keep toast messages concise and actionable * **Status Clarity**: Ensure transaction status messages clearly indicate the current state * **Interactive Indicators**: Make clickable toasts visually distinct with hover states #### Integration Patterns ```typescript // Good: Clear, specific messages toast({ variant: "error", message: "Insufficient balance to complete transaction", }); // Avoid: Vague error messages toast({ variant: "error", message: "Something went wrong", }); // Good: Meaningful achievement titles toast({ variant: "achievement", title: "Trading Expert", subtitle: "Complete 100 trades", xpAmount: 500, }); ``` ### Browser Support The toast API is built into the Controller SDK and works across all browsers that support the Controller, including: * Chrome/Chromium browsers * Safari (desktop and mobile) * Firefox * Edge ### Next Steps * Learn about [Sessions](/controller/sessions) for seamless transaction flows * Explore [Configuration](/controller/configuration) options * Set up [External Wallet Integration](/controller/signer-management) ## Looking up Usernames / Addresses A service for looking up usernames and addresses in the Cartridge ecosystem. You can use either the helper methods from the Controller SDK or query the endpoint directly. ### Direct API Access #### Account Lookup Endpoint The lookup endpoint can be accessed directly via HTTP POST: ```bash curl -X POST \ -H "Content-Type: application/json" \ -d '{"usernames": ["shinobi","sensei"]}' \ https://api.cartridge.gg/accounts/lookup ``` This query returns matching usernames along with their credit information and last update timestamp, perfect for implementing search and autocomplete functionality. Request Format ```json { usernames?: string[]; // Look up addresses for usernames addresses?: string[]; // Look up usernames for addresses } ``` Response Format ```json { "results": [ { "username": "shinobi", "addresses": ["0x123..."] // Array of addresses - future support for multiple controllers/signers }, { "username": "sensei", "addresses": ["0x456..."] } ] } ``` > **Note**: The API response includes an array of addresses per username to support multiple controllers/signers in the future. Currently, the helper methods assume a 1:1 relationship and use only the first address. ### Controller SDK Methods The Controller SDK provides dedicated methods for username lookup with enhanced functionality: ``` npm install @cartridge/controller ``` #### `lookupUsername(username: string)` - New in v0.13.7 Check if a single username exists and get normalized signer options: ```typescript import Controller from "@cartridge/controller"; const controller = new Controller({}); // Check if username exists and get available authentication methods const result = await controller.lookupUsername("alice"); console.log(result.exists); // true/false console.log(result.signers); // ["webauthn", "google", "discord"] ``` **Return Type:** ```typescript interface UsernameLookupResult { exists: boolean; signers: string[]; // Available authentication methods for the username } ``` **Use Cases:** * **Headless authentication flows**: Check account existence before attempting to connect * **Form validation**: Validate usernames in real-time * **Authentication method selection**: Show users only the authentication methods they have configured * **Auto-signup flows**: Determine whether to create a new account or authenticate existing one #### ControllerConnector Integration The `lookupUsername` method is also available through `ControllerConnector` for starknet-react applications: ```typescript import { ControllerConnector } from '@cartridge/connector'; import { useConnectors } from '@starknet-react/core'; function useUsernameLookup() { const connectors = useConnectors(); const controller = connectors.find(c => c.id === 'cartridge') as ControllerConnector; return async (username: string) => { return await controller.lookupUsername(username); }; } ``` ### Legacy Helper Methods For bulk lookups and backwards compatibility, you can use the legacy helper methods: ```typescript import { lookupUsernames, lookupAddresses } from '@cartridge/controller'; // Look up addresses for usernames const userMap = await lookupUsernames(['shinobi']); console.log(userMap.get('shinobi')); // Returns address: '0x123...' // Look up usernames for addresses const addressMap = await lookupAddresses(['0x123...']); console.log(addressMap.get('0x123...')); // Returns username: 'shinobi' ``` #### `lookupUsernames(usernames: string[]): Promise>` * Fetches addresses for given usernames. * Input: Array of usernames * Returns: Map of username to address * Caching: Results are automatically cached #### `lookupAddresses(addresses: string[]): Promise>` * Fetches usernames for given addresses. * Input: Array of addresses * Returns: Map of address to username * Caching: Results are automatically cached ### Limitations and Rate Limiting When using the lookup methods or directly via the API, be aware of the following limitations and rate limiting measures: 1. **Maximum Items**: You can fetch up to 1000 items total in a single call, combining both addresses and usernames. For example: * 1000 addresses OR * 1000 usernames OR * Any combination (e.g., 400 addresses + 600 usernames) 2. **Rate Limiting**: The API is rate-limited to 10 requests per second to prevent overloading the server. 3. **Address Format Requirements**: * Addresses must be lowercase non-zero-padded hex * The helper methods handle address formatting automatically ### Error Handling The lookup methods may throw errors in the following cases: * If you provide more than 1000 addresses in a single call. * If you exceed the rate limit of 10 requests per second. * If there are network issues or the API is unavailable. Always wrap your calls to lookup methods in a try-catch block to handle potential errors gracefully. ### Performance Considerations To optimize performance when fetching usernames: 1. Batch your requests: Instead of making multiple calls for individual addresses, group them into a single call (up to 1000 addresses). 2. Utilize the built-in caching of the helper methods: Previously fetched usernames are cached, so subsequent requests for the same addresses will be faster. 3. Be mindful of the rate limit: If you need to fetch usernames for more than 1000 addresses, implement your own throttling mechanism. By following these guidelines, you can efficiently fetch and display usernames for Controller addresses in your Cartridge-powered application. ## Services Cartridge offers a set of platform services for onchain games and applications: * **[Paymaster](/services/paymaster)** — sponsor transaction fees so your users don't need to hold STRK for gas. * **[RPC](/services/rpc)** — Starknet RPC endpoints for mainnet and Sepolia, with API token and CORS-based authentication. * **[vRNG](/services/vrng)** — atomic, verifiable randomness for fully onchain games via EC-VRF on the Stark curve. Each service is self-served via the CLI and can be used independently or together. ## Paymaster The Cartridge Paymaster sponsors transaction fees on behalf of your users, eliminating the need for them to hold STRK for gas. Manage budgets, policies, and monitor usage through the CLI. ### Availability The paymaster service is available across all networks with different activation requirements: * **Testnet Networks** * Automatically enabled, no additional setup required * **Mainnet** * Available and fully self-served via the CLI * Define your own usage scopes and spending limits ### Integration One of the key benefits of the Cartridge Paymaster is that it requires zero additional integration work. When the paymaster is enabled for your application, it will automatically activate for all eligible transactions. No code changes or configuration are needed. ### Prerequisites Authenticate with the CLI: ```bash slot auth login ``` ### Team Setup Before creating a paymaster, you need a team with sufficient balance in the unit you plan to fund (USD or STRK). ### Checking Team Balance Before creating or topping up a paymaster, check that the team has enough USD or STRK to cover the budget: ```bash slot teams balance ``` **Output:** ``` ┌────────────────────────────┬───────────┐ │ Balance for team `my-team` ┆ │ ╞════════════════════════════╪═══════════╡ │ USD ┆ $25.000000│ ├╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌┤ │ STRK ┆ 100.000000│ └────────────────────────────┴───────────┘ ``` USD funds USD-denominated paymaster budgets; STRK funds STRK-denominated paymaster budgets. The two pools are tracked separately. ### Creating a Paymaster Create a new paymaster with an initial budget: ```bash slot paymaster create --team --budget --unit USD ``` The `--unit` flag accepts `USD` or `STRK`. STRK budgets are funded from the team's STRK pool; USD budgets are funded from the team's USD pool. #### Example ```bash slot paymaster my-game-pm create --team my-team --budget 10 --unit USD ``` **Output:** ``` ✅ Paymaster Created Successfully ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 🏢 Details: • Name: my-game-pm • Team: my-team 💰 Initial Budget: • Amount: $10.00 USD ``` :::info The initial budget is deducted from the team's pool that matches the selected unit. Run `slot teams balance` first to confirm the team has sufficient USD (or STRK) before creating a paymaster. ::: ### Managing Budget #### Increase Budget Add funds to your paymaster: ```bash slot paymaster budget increase --amount --unit USD ``` **Output:** ``` ✅ Budget Increased Successfully ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 🏢 Paymaster: my-game-pm 📈 Operation: • Action: Increased • Amount: 5 USD 💰 New Budget: • Amount: $15.00 USD ``` #### Decrease Budget Remove funds from your paymaster: ```bash slot paymaster budget decrease --amount --unit USD ``` **Output:** ``` ✅ Budget Decreased Successfully ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 🏢 Paymaster: my-game-pm 📉 Operation: • Action: Decreased • Amount: 2 USD 💰 New Budget: • Amount: $13.00 USD ``` ### Policy Management Policies define which contracts and entry points your paymaster will sponsor. #### Understanding Paymaster Predicates Paymaster policies now support **predicates** - conditional logic that determines whether a transaction should be sponsored. This enables sophisticated sponsorship rules based on game state, user eligibility, or other custom conditions. **How Predicates Work:** 1. When a transaction is submitted to a paymaster with a predicate-enabled policy 2. The paymaster first calls the predicate contract function 3. If the predicate returns `true`, the transaction is sponsored 4. If the predicate returns `false` or reverts, the transaction is not sponsored **Use Cases:** * Sponsor moves only for players with sufficient in-game energy * Sponsor attacks only during specific game phases * Sponsor crafting only for players with required materials * Rate-limit sponsorship per user or per game session :::tip Predicates are optional. Policies without predicates will always sponsor matching transactions, while policies with predicates add conditional logic. ::: #### vRNG Integration Paymasters can sponsor vRNG operations, providing gasless random number generation for your games. When configuring policies for vRNG-enabled contracts, the paymaster will automatically handle both the initial request and callback transactions. For detailed vRNG setup and usage, see [vRNG](/services/vrng). #### Add Policies from Preset (Recommended) The preferred way to add policies is using verified contract presets for your games: ```bash slot paymaster policy add-from-preset --name ``` **Example:** ```bash slot paymaster my-game-pm policy add-from-preset --name dope-wars ``` :::info Presets contain verified contracts from games in the Dojo ecosystem. Make sure to add your contracts to the preset repository first at [https://github.com/cartridge-gg/presets/tree/main/configs](https://github.com/cartridge-gg/presets/tree/main/configs) before using this method. ::: #### Add a Single Policy For individual contract policies or custom contracts not yet in presets: ```bash slot paymaster policy add --contract --entrypoint ``` #### Add Policies from JSON File For bulk adding multiple custom policies: ```bash slot paymaster policy add-from-json --file ``` **JSON Format:** ```json [ { "contractAddress": "0x1234...abcd", "entrypoint": "move_player" }, { "contractAddress": "0x5678...efgh", "entrypoint": "attack", "predicate": { "address": "0x9abc...1234", "entrypoint": "check_attack_eligibility" } } ] ``` :::info **Predicate Support**: You can include optional `predicate` objects in your policy JSON to add conditional logic for transaction sponsorship. The predicate must contain an `address` (contract address) and `entrypoint` (function name) that will be called to evaluate whether the transaction should be sponsored. ::: #### Remove a Policy ```bash slot paymaster policy remove --contract --entrypoint ``` **Output:** ``` Successfully removed policy: PolicyArgs { contract: "0x1234...abcd", entrypoint: "move_player" } ``` #### Remove All Policies ```bash slot paymaster policy remove-all ``` :::warning This action requires confirmation and cannot be undone. ::: #### List Policies ```bash slot paymaster policy list ``` ### Paymaster Information Get comprehensive information about your paymaster: ```bash slot paymaster info ``` **Output:** ``` 🔍 Paymaster Info for 'my-game-pm' ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 🏢 Details: • Team: my-team • Active: ✅ Yes 💰 Budget: • Total: $90.00 USD • Spent: $17.60 USD • Usage: [█████░░░░░░░░░░░░░░░░░░░░░░░░░] 19.7% 📋 Policies: • Count: 3 ``` ### Updating Paymaster Configuration Update your paymaster's basic configuration settings: ```bash slot paymaster update [OPTIONS] ``` #### Update Paymaster Name ```bash slot paymaster my-game-pm update --name new-game-pm ``` #### Change Team Association Transfer the paymaster to a different team: ```bash slot paymaster my-game-pm update --team new-team ``` #### Enable/Disable Paymaster Toggle the active state of your paymaster: ```bash # Disable paymaster (stops sponsoring transactions) slot paymaster my-game-pm update --active false # Re-enable paymaster slot paymaster my-game-pm update --active true ``` :::warning * Changing the team association will transfer the paymaster and its budget to the new team * Disabling a paymaster will immediately stop it from sponsoring new transactions * At least one update parameter (--name, --team, or --active) must be provided ::: ### Statistics and Monitoring View usage statistics for your paymaster: ```bash slot paymaster stats --last ``` **Example:** ```bash slot paymaster my-game-pm stats --last 24hr ``` **Time Period Options:** * `1hr`, `2hr`, `24hr` * `1day`, `2day`, `7day` * `1week` **Output:** ``` 📊 Paymaster Stats for 'my-game-pm' (Last 24hr) ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 📈 Transactions: • Total: 1,247 • Successful: 1,198 • Reverted: 49 • Success Rate: 96.1% 💰 Fees (USD): • Total (24hr): $12.45 • Average: $0.009988 • Minimum: $0.001234 • Maximum: $0.045678 👥 Users: • Unique Users: 89 ``` ### Transaction History View detailed transaction history for your paymaster with filtering and sorting options: ```bash slot paymaster transactions [OPTIONS] ``` #### Basic Usage View recent transactions: ```bash slot paymaster my-game-pm transactions ``` **Output:** ``` 📊 Paymaster Transactions for 'my-game-pm' (Last 24hr) ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ Transaction Hash Executed Status USD Fee ────────────────────────────────────────────────────────────────────────────────────────────────────────────── 0x50c2dd556593564fe2b814d61b3b1592682de83702552a993d24f9e897710e7 11s ago SUCCESS $0.0026 0x41b0f547741bd1fdc29dd4c82a80da2a452314e710ae7cbe0e05cb4cb1e6c0e 22s ago SUCCESS $0.0025 0x4b74ee2ab7764cb3d11f3319b64c2698b868727fdf99728bdf74aa023b5e77d 32s ago REVERTED $0.0028 0x2af69b9798355e91119c6a9adb1363b2f533f0557601e4687dcfe9725e8feaa 42s ago SUCCESS $0.0025 0x25dfc115dabda89a2027366790ee5cfcfefb861fe1b584c6fb15dc1588e0816 47s ago REVERTED $0.0032 ``` #### Filtering Options **Filter by Status:** ```bash # Show only successful transactions slot paymaster my-game-pm transactions --filter SUCCESS # Show only reverted transactions slot paymaster my-game-pm transactions --filter REVERTED # Show all transactions (default) slot paymaster my-game-pm transactions --filter ALL ``` **Time Period:** ```bash # Last hour slot paymaster my-game-pm transactions --last 1hr ``` **Sorting:** ```bash # Sort by fees (ascending) slot paymaster my-game-pm transactions --order-by FEES_ASC # Sort by fees (descending) slot paymaster my-game-pm transactions --order-by FEES_DESC # Sort by execution time (most recent first - default) slot paymaster my-game-pm transactions --order-by EXECUTED_AT_DESC # Sort by execution time (oldest first) slot paymaster my-game-pm transactions --order-by EXECUTED_AT_ASC ``` **Limit Results:** ```bash # Show up to 50 transactions (max 1000) slot paymaster my-game-pm transactions --limit 50 ``` ### Dune Analytics Queries Generate Dune Analytics queries to analyze your paymaster's transaction data: ```bash slot paymaster dune [OPTIONS] ``` #### Example Dashboard See a live example of paymaster analytics at [Blob Arena Stats](https://dune.com/cartridge/blob-arena) on Dune Analytics. #### Basic Usage Generate a comprehensive SQL query for your paymaster: ```bash slot paymaster my-game-pm dune ``` The query provides exhaustive analysis including: * Finds all execute\_from\_outside\_v3 selectors in transaction calldata * Handles both normal calls and multi-call vRNG helpers * Matches all patterns including nested vRNG calls * Comprehensive metrics with daily, weekly, and monthly breakdowns #### Time Period Options By default, queries use the paymaster's creation time. You can specify a custom time period: ```bash # Last 24 hours slot paymaster my-game-pm dune --last 24hr # Last week slot paymaster my-game-pm dune --last 1week ``` **Time Period Options:** * `1hr`, `2hr`, `24hr` * `1day`, `2day`, `7day` * `1week` #### Dune Template Parameters For dynamic queries in Dune dashboards, use template parameters: ```bash slot paymaster my-game-pm dune --dune-params ``` This generates a query with `{{start_time}}` and `{{end_time}}` parameters that you can configure in your Dune dashboard. #### Query Output The command generates a comprehensive SQL query that includes: **Daily Metrics:** * Transaction counts and unique users * New vs returning users * Fees in STRK and USD * Transactions per user ratio **Rolling Windows:** * 7-day active users (WAU) * 30-day active users (MAU) **Aggregated Views:** * Weekly and monthly summaries * Overall totals and averages * User acquisition and retention metrics #### Usage Tips **For Dashboard Creation:** * Use `--dune-params` for interactive dashboards * Copy the generated SQL directly into Dune Analytics * Set up time range parameters for flexible analysis **For One-time Analysis:** * Specify `--last` with appropriate time period * Query will include actual timestamps for immediate execution :::tip The query is optimized for comprehensive analysis but may timeout on very long time ranges. For historical analysis spanning months, consider breaking it into smaller time periods or using Dune's incremental refresh features. ::: #### Common Use Cases **Growth Analysis:** ```bash slot paymaster my-game-pm dune --last 1week ``` Analyze weekly growth trends, user acquisition, and engagement patterns. **Daily Monitoring:** ```bash slot paymaster my-game-pm dune --last 24hr ``` Monitor recent activity, transaction success rates, and costs. **Dashboard Setup:** ```bash slot paymaster my-game-pm dune --dune-params ``` Create flexible dashboards with configurable time ranges. #### Quick Debugging Use Cases The transaction history is useful for identifying issues: **View expensive transactions that might indicate inefficient contract calls:** ```bash slot paymaster my-game-pm transactions --order-by FEES_DESC --limit 10 ``` **Investigate failed transactions to debug contract issues:** ```bash slot paymaster my-game-pm transactions --filter REVERTED --last 24hr ``` ### Best Practices #### Budget Management * Start with a conservative budget and increase as needed * Monitor spending through the stats command * Keep sufficient team balance for paymaster operations #### Policy Management * Be specific with your policies to avoid sponsoring unintended transactions * **Use presets whenever possible** for verified game contracts in the Dojo ecosystem * Contribute your game contracts to the preset repository for community verification * Regularly review and update policies as your application evolves * Test policies with small budgets before scaling up #### Security * Only add policies for contracts you trust * Keep your team membership limited to necessary collaborators ### Common Workflows #### Setting up a new game paymaster ```bash # Create the paymaster (the team must already have a USD or STRK balance) slot paymaster my-game-pm create --team my-team --budget 10 --unit USD # Add game contract policies slot paymaster my-game-pm policy add --contract 0x123...abc --entrypoint move_player slot paymaster my-game-pm policy add --contract 0x123...abc --entrypoint attack_enemy slot paymaster my-game-pm policy add --contract 0x123...abc --entrypoint use_item # Check initial setup slot paymaster my-game-pm info ``` #### Monitoring and maintenance ```bash # Check daily stats slot paymaster my-game-pm stats --last 24hr # Check current status slot paymaster my-game-pm info # Add more budget if needed (ensure team has balance) slot paymaster my-game-pm budget increase --amount 5 --unit USD ``` #### Insufficient Balance Error If you encounter an insufficient balance error when creating or funding a paymaster, run `slot teams balance` to see the team's USD and STRK pools, then top up the relevant pool as needed. ## Cartridge RPC Cartridge provides dedicated RPC endpoints for Starknet networks with built-in authentication and CORS support. ### Pricing Cartridge RPC is free for up to 1M requests per month. Additional requests are charged at $5 per 1M requests, billed to the team that owns the API token. ### Endpoints #### Mainnet ``` https://api.cartridge.gg/x/starknet/mainnet ``` #### Sepolia Testnet ``` https://api.cartridge.gg/x/starknet/sepolia ``` ### Authentication Cartridge RPC supports two authentication methods: #### API Token Authentication Authenticate requests using an API token with the `Authorization` header: ```bash curl https://api.cartridge.gg/x/starknet/mainnet \ -H "Authorization: Bearer YOUR_API_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "jsonrpc": "2.0", "method": "starknet_chainId", "params": [], "id": 1 }' ``` #### Domain Whitelisting For browser-based applications, whitelist your domains to make direct RPC calls without exposing API tokens. Once configured, your whitelisted domains can make requests directly: ```javascript const response = await fetch('https://api.cartridge.gg/x/starknet/mainnet', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ jsonrpc: '2.0', method: 'starknet_chainId', params: [], id: 1, }), }); ``` :::info Requests from whitelisted domains without an API token are rate limited per IP address. ::: ### Setup via CLI #### Prerequisites Authenticate with the CLI: ```bash slot auth login ``` #### Managing API Tokens Create a new RPC API token: ```bash slot rpc tokens create --team ``` List all RPC API tokens: ```bash slot rpc tokens list --team ``` Delete an RPC API token: ```bash slot rpc tokens delete --team ``` #### Managing CORS Whitelist Domain whitelists are always specified as root domains, all subdomains are automatically included. Add a domain to the CORS whitelist: ```bash slot rpc whitelist add --team ``` Examples: ```bash # Whitelist a specific domain slot rpc whitelist add example.com --team my-team ``` List all whitelisted domains: ```bash slot rpc whitelist list --team ``` Remove a domain from the CORS whitelist: ```bash slot rpc whitelist remove --team ``` #### Viewing RPC Logs View RPC request logs for your team: ```bash slot rpc logs --team ``` ##### Options * `--after `: Fetch logs after a specific cursor for pagination * `--limit `: Limit the number of log entries returned (default: 100) * `--since `: Fetch logs from a specific time period (e.g., `30m`, `1h`, `24h`) ##### Examples ```bash # View the last 5 log entries from the past 30 minutes slot rpc logs --team my-team --limit 5 --since 30m # Paginate through logs using a cursor slot rpc logs --team my-team --after gaFpuWNtaDVhZ3k2Nzc2c2gwMWR4N3FsYXlhc2s --limit 5 # View recent logs with time filter slot rpc logs --team my-team --since 1h ``` ## CLI The Controller CLI enables automated execution of Starknet transactions from the terminal. It uses a human-in-the-loop workflow: you authorize sessions in the browser, then execute transactions from the command line without further prompts. This is useful for scripting, backend automation, and AI agent integration. ### Installation Install via the command-line: ```bash curl -fsSL https://raw.githubusercontent.com/cartridge-gg/controller-cli/main/install.sh | bash ``` Or install via Cargo: ```bash cargo install --git https://github.com/cartridge-gg/controller-cli ``` ### Quick Start #### 1. Define policies Create a `policies.json` file specifying which contracts and methods the session can call. See [Sessions](/controller/sessions) for detailed policy configuration and examples. ```json { "contracts": { "0x49d36570d4e46f48e99674bd3fcc84644ddd6b96f7c741b1562b82f9e004dc7": { "name": "ETH Token", "methods": [ { "name": "transfer", "entrypoint": "transfer", "description": "Transfer ETH tokens to another address" } ] } } } ``` #### 2. Authorize a session Using a policy file: ```bash controller session auth --file policies.json --chain-id SN_MAIN ``` Or use a preset for popular games/apps: ```bash controller session auth --preset loot-survivor --chain-id SN_MAIN ``` Available presets include: `loot-survivor`, `influence`, `realms`, `pistols`, `dope-wars`, and more. This generates a new keypair, creates an authorization URL, and automatically polls until you approve in the browser. Session credentials are stored once authorized. Sessions expire after 7 days by default --- use `--expires` to customize (e.g., `--expires 1day`, `--expires 1hr`). #### 3. Execute transactions **Single call (positional args):** ```bash controller execute \ 0x49d36570d4e46f48e99674bd3fcc84644ddd6b96f7c741b1562b82f9e004dc7 \ transfer \ 0x1234,u256:1000000000000000000 ``` **Multiple calls from a file:** ```bash controller execute --file calls.json --wait ``` Where `calls.json` contains: ```json { "calls": [ { "contractAddress": "0x4718f5a0fc34cc1af16a1cdee98ffb20c31f5cd61d6ab07201858f4287c938d", "entrypoint": "transfer", "calldata": ["0x1234", "0x100", "0x0"] } ] } ``` Transactions are auto-subsidized via paymaster when possible. Use `--no-paymaster` to pay with user funds directly. #### 4. Read-only calls ```bash controller call \ 0x49d36570d4e46f48e99674bd3fcc84644ddd6b96f7c741b1562b82f9e004dc7 \ balance_of \ 0xaddress ``` Use `--block-id` to query at a specific block (`latest`, `pending`, a block number, or block hash). #### 5. Starter packs ```bash # Check what's in a starter pack controller starterpack info --chain-id SN_MAIN # Get the price controller starterpack quote --chain-id SN_MAIN # Purchase via browser (crosschain/Apple Pay) controller starterpack purchase --chain-id SN_MAIN # Purchase directly from wallet controller starterpack purchase --direct --chain-id SN_MAIN ``` #### 6. Marketplace ```bash # Check an order controller marketplace info --order-id 42 --collection 0x123... --token-id 1 --chain-id SN_MAIN # Buy an NFT controller marketplace buy --order-id 42 --collection 0x123... --token-id 1 --chain-id SN_MAIN --wait ``` #### Calldata formats Calldata values support multiple formats: | Format | Example | Description | | ------------ | -------------------------- | ------------------------------------------ | | Hex | `0x64` | Standard hex felt | | Decimal | `100` | Decimal felt | | `u256:` | `u256:1000000000000000000` | Auto-splits into low/high 128-bit felts | | `str:` | `str:hello` | Cairo short string | | `bytearray:` | `bytearray:hello` | Cairo ByteArray (multi-felt serialization) | The `u256:` prefix eliminates the need to manually split token amounts into low/high parts. The `bytearray:` prefix serializes strings into Cairo ByteArray format (multiple felts): ```bash # String encoding controller execute 0xCONTRACT set_name bytearray:hello --json # Raw bytes encoding controller execute 0xCONTRACT set_data "bytearray:[0x48,0x65,0x6c,0x6c,0x6f]" --json ``` ### Commands #### `session auth` Generates a keypair, creates an authorization URL, and waits for the user to approve in the browser. ```bash # Using a policy file controller session auth --file --chain-id SN_MAIN # Using a preset controller session auth --preset --chain-id SN_MAIN # With custom expiration (default: 7days) controller session auth --file --chain-id SN_MAIN --expires 1day ``` | Flag | Description | Default | | ------------- | ------------------------------------------------------------------------------------ | ------- | | `--file` | Path to policy JSON file | --- | | `--preset` | Use a pre-defined policy preset | --- | | `--chain-id` | Chain ID (e.g., `SN_MAIN`, `SN_SEPOLIA`) | --- | | `--rpc-url` | RPC URL (overrides config) | --- | | `--overwrite` | Overwrite existing session without confirmation | off | | `--expires` | Session expiration duration (e.g., `1min`, `1hr`, `1day`, `7days`, `1week`, `1year`) | `7days` | **Presets:** Popular games/apps have pre-defined policies: * `loot-survivor` --- Loot Survivor game * `influence` --- Influence space strategy * `realms` --- Realms world * `pistols` --- Pistols at Dawn * `dope-wars` --- Dope Wars See all presets at [github.com/cartridge-gg/presets](https://github.com/cartridge-gg/presets/tree/main/configs). #### `session status` Displays current session status, keypair info, and expiration details. ```bash controller session status ``` Returns one of three states: `no_session`, `keypair_only`, or `active`. #### `session list` Lists all active sessions with pagination. ```bash controller session list controller session list --limit 20 --page 2 ``` #### `session clear` Removes all stored session data and keypairs. ```bash controller session clear [--yes] ``` #### `execute` Executes a transaction using the active session. ```bash # Single call (positional: contract entrypoint calldata) controller execute
[--chain-id ] [--no-paymaster] [--wait] [--timeout ] # Multiple calls from file controller execute --file [--chain-id ] [--no-paymaster] [--wait] [--timeout ] ``` | Flag | Description | Default | | ---------------- | ----------------------------------- | ----------- | | `--chain-id` | Chain ID (`SN_MAIN`, `SN_SEPOLIA`) | From config | | `--no-paymaster` | Bypass paymaster, pay fees directly | off | | `--wait` | Wait for transaction confirmation | off | | `--timeout` | Confirmation timeout in seconds | 300 | #### `call` Performs a read-only contract call (no transaction). ```bash controller call
[calldata] [--block-id ] [--chain-id ] ``` #### `transaction` Gets the status of a transaction. ```bash controller transaction --chain-id SN_SEPOLIA ``` Add `--wait` to poll until the transaction reaches a final status. #### `receipt` Gets the full receipt of a transaction including execution status, fees, events, and messages. ```bash controller receipt --chain-id SN_SEPOLIA ``` Add `--wait` to poll until the receipt is available. #### `balance` Queries ERC20 token balances for the active session account. ```bash # All known token balances controller balance # Specific token controller balance eth ``` Built-in tokens: ETH, STRK, USDC, USD.e, LORDS, SURVIVOR, WBTC. Custom tokens can be added via `config set token.
`. #### `username` Displays the Cartridge username associated with the active session account. ```bash controller username ``` #### `lookup` Resolves Cartridge controller usernames to addresses or vice versa. ```bash # Look up addresses for usernames controller lookup --usernames shinobi,sensei # Look up usernames for addresses controller lookup --addresses 0x123...,0x456... ``` Output format: `username:address` pairs. #### `config` Manages CLI configuration values. ```bash # Set a config value controller config set rpc-url https://api.cartridge.gg/x/starknet/mainnet # Get a config value controller config get rpc-url # List all config values controller config list # Add a custom token for balance tracking controller config set token.MYTOKEN 0x123... ``` Valid keys: `rpc-url`, `keychain-url`, `api-url`, `storage-path`, `json-output`, `colors`, `callback-timeout`, `token.`. #### `starterpack info` Fetches metadata for a starter pack (name, description, image, included items). ```bash controller starterpack info --chain-id SN_MAIN ``` #### `starterpack quote` Gets a price quote including base price, fees, and total cost. ```bash controller starterpack quote --chain-id SN_MAIN ``` #### `starterpack purchase` Purchases a starter pack. Two modes are available: **UI mode (default):** Opens the Cartridge purchase page in your browser. Supports crosschain payments and Apple Pay. ```bash controller starterpack purchase --chain-id SN_MAIN # or explicitly: controller starterpack purchase --ui --chain-id SN_MAIN ``` **Direct mode:** Executes the purchase on-chain using the active session. Requires session policies that include `approve` on the payment token and `issue` on the starter pack contract. ```bash controller starterpack purchase --direct --chain-id SN_MAIN ``` | Flag | Description | Default | | ---------------- | ------------------------------------------------ | ------------------ | | `--ui` | Open browser for purchase (crosschain/Apple Pay) | default | | `--direct` | Purchase directly via Controller wallet | off | | `--recipient` | Send to a different address | current controller | | `--quantity` | Number to purchase | 1 | | `--wait` | Wait for transaction confirmation (direct only) | off | | `--timeout` | Confirmation timeout in seconds (direct only) | 300 | | `--no-paymaster` | Pay gas directly (direct only) | off | #### `marketplace info` Queries marketplace order validity and details before purchasing. ```bash controller marketplace info \ --order-id 42 \ --collection 0x123...abc \ --token-id 1 \ --chain-id SN_MAIN ``` #### `marketplace buy` Purchases an NFT from an active marketplace listing. Requires an active session with policies for `execute` on the marketplace contract and `approve` on the payment token. ```bash controller marketplace buy \ --order-id 42 \ --collection 0x123...abc \ --token-id 1 \ --chain-id SN_MAIN \ --wait ``` | Flag | Description | Default | | ---------------- | ------------------------------------------ | ------- | | `--order-id` | Marketplace order ID (required) | --- | | `--collection` | NFT collection contract address (required) | --- | | `--token-id` | Token ID in the collection (required) | --- | | `--asset-id` | Asset ID for ERC1155 tokens | 0 | | `--quantity` | Quantity to purchase | 1 | | `--no-royalties` | Skip paying creator royalties | off | | `--wait` | Wait for transaction confirmation | off | | `--timeout` | Confirmation timeout in seconds | 300 | | `--no-paymaster` | Pay gas directly | off | ### Global Flags All commands support: | Flag | Description | | ---------------------- | --------------------------------------------------------------- | | `--json` | Machine-readable JSON output | | `--no-color` | Disable colored terminal output | | `--account ` | Cartridge Controller account to use (e.g., `--account shinobi`) | #### Multi-Account Support Use `--account` to specify which Cartridge Controller account to use. When provided, the username is prefilled in the session authorization UI. When omitted, the user can choose which account to authorize in the browser. Each account gets its own isolated session storage, so you can manage multiple accounts on the same machine. ```bash # Authorize with a specific account (username prefilled) controller session auth --file policy.json --chain-id SN_MAIN --account shinobi # Authorize without specifying (choose in browser) controller session auth --file policy.json --chain-id SN_MAIN # Execute as a specific account controller execute 0x... transfer 0x...,u256:100 --account shinobi ``` ### Network Selection Specify the network using `--chain-id` or configure a default RPC URL: | Chain ID | RPC URL | Usage | | ------------ | --------------------------------------------- | ---------------- | | `SN_MAIN` | `https://api.cartridge.gg/x/starknet/mainnet` | Starknet Mainnet | | `SN_SEPOLIA` | `https://api.cartridge.gg/x/starknet/sepolia` | Starknet Sepolia | RPC URL precedence: `--chain-id` flag > config `rpc-url` > environment variable. ### Paymaster Control By default, transactions use the paymaster (free execution). If the paymaster is unavailable, the transaction **fails** rather than falling back to user-funded execution. Use `--no-paymaster` to bypass the paymaster and pay with user funds: ```bash controller execute \ 0x49d36570d4e46f48e99674bd3fcc84644ddd6b96f7c741b1562b82f9e004dc7 \ transfer \ 0x1234,u256:1000000000000000000 \ --no-paymaster ``` ### Configuration The CLI reads configuration from `~/.config/controller-cli/config.toml`: ```toml [session] storage_path = "~/.config/controller-cli" rpc_url = "https://api.cartridge.gg/x/starknet/sepolia" keychain_url = "https://x.cartridge.gg" api_url = "https://api.cartridge.gg/query" [cli] json_output = false use_colors = true callback_timeout_seconds = 300 [tokens] MYTOKEN = "0x123..." ``` Settings can also be managed with `controller config set/get/list`, or overridden with environment variables: | Variable | Description | | ------------------------ | ----------------------------------- | | `CARTRIDGE_STORAGE_PATH` | Session storage location | | `CARTRIDGE_RPC_URL` | Default RPC endpoint | | `CARTRIDGE_JSON_OUTPUT` | Default to JSON output (`true`/`1`) | Precedence: CLI flags > environment variables > config file. ### Error Handling Common errors and how to fix them: | Error | Meaning | Fix | | ------------------------- | ------------------------------------------ | ------------------------------------------ | | `NoSession` | No keypair found | Run `session auth` | | `SessionExpired` | Session has expired | Run `session auth` again | | `InvalidSessionData` | Corrupted session data | Run `session clear` and start over | | `TransactionFailed` | Execution failed | Check policies and calldata | | `CallbackTimeout` | Authorization timed out | Run `session auth` again | | `ManualExecutionRequired` | No authorized session for this transaction | Register session with appropriate policies | | `InvalidInput` | Invalid input parameters | Check command syntax and calldata | When using `--json`, errors return structured responses with machine-readable codes and recovery hints: ```json { "status": "error", "error_code": "SessionExpired", "message": "Session expired at 2025-01-01 00:00:00 UTC", "recovery_hint": "Run 'controller session auth' to create a new session" } ``` ### AI Agent Integration For AI agents and LLMs integrating with the CLI, see the [LLM Usage Guide](https://github.com/cartridge-gg/controller-cli/blob/main/LLM_USAGE.md) in the repository for detailed integration patterns and best practices. ### Source [github.com/cartridge-gg/controller-cli](https://github.com/cartridge-gg/controller-cli) ## Cartridge Controller Node.js Integration This guide demonstrates how to integrate the Cartridge Controller with a Node.js application. ### Installation :::code-group ```bash [npm] npm install @cartridge/controller starknet ``` ```bash [pnpm] pnpm add @cartridge/controller starknet ``` ```bash [yarn] yarn add @cartridge/controller starknet ``` ```bash [bun] bun add @cartridge/controller starknet ``` ::: ### Basic Setup #### Using Presets ```typescript import SessionProvider, { ControllerError, } from "@cartridge/controller/session/node"; import { constants } from "starknet"; import path from "path"; async function main() { const storagePath = process.env.CARTRIDGE_STORAGE_PATH || path.join(process.cwd(), ".cartridge"); // Create a session provider using a verified preset const provider = new SessionProvider({ rpc: "https://api.cartridge.gg/x/starknet/sepolia", chainId: constants.StarknetChainId.SN_SEPOLIA, preset: "my-game", // Load verified policies from preset basePath: storagePath, }); console.log("Registering a session..."); console.log("Open the URL printed below to authorize the session."); try { const account = await provider.connect(); if (!account) { console.log("Session not ready yet. Complete the browser flow and rerun."); return; } console.log("Session ready!"); console.log("Account address:", account.address); } catch (error: unknown) { const controllerError = error as ControllerError; if (controllerError?.code) { console.error("Error:", { code: controllerError.code, message: controllerError.message, data: controllerError.data, }); } else { console.error("Error:", error); } } } main().catch(console.error); ``` #### Using Manual Policies ```typescript import SessionProvider, { ControllerError, } from "@cartridge/controller/session/node"; import { constants } from "starknet"; import path from "path"; export const STRK_CONTRACT_ADDRESS = "0x049d36570d4e46f48e99674bd3fcc84644ddd6b96f7c741b1562b82f9e004dc7"; async function main() { const storagePath = process.env.CARTRIDGE_STORAGE_PATH || path.join(process.cwd(), ".cartridge"); // Create a session provider with manual policies const provider = new SessionProvider({ rpc: "https://api.cartridge.gg/x/starknet/sepolia", chainId: constants.StarknetChainId.SN_SEPOLIA, policies: { contracts: { [STRK_CONTRACT_ADDRESS]: { methods: [ { name: "transfer", entrypoint: "transfer", description: "Transfer STRK", }, ], }, }, }, basePath: storagePath, }); console.log("Registering a session..."); console.log("Open the URL printed below to authorize the session."); try { const account = await provider.connect(); if (!account) { console.log("Session not ready yet. Complete the browser flow and rerun."); return; } console.log("Session ready!"); console.log("Account address:", account.address); // Example: Transfer STRK const recipient = account.address; // Self transfer for demo const amount = "0x0"; // Keep it minimal for a demo const result = await account.execute([ { contractAddress: STRK_CONTRACT_ADDRESS, entrypoint: "transfer", calldata: [recipient, amount, "0x0"], }, ]); console.log("Transaction hash:", result.transaction_hash); } catch (error: unknown) { const controllerError = error as ControllerError; if (controllerError?.code) { console.error("Execute error:", { code: controllerError.code, message: controllerError.message, data: controllerError.data, }); } else { console.error("Execute error:", error); } } } main().catch(console.error); ``` ### Important Notes 1. The `basePath` parameter specifies where session data will be stored. Make sure the directory is writable. 2. When running the application for the first time, you'll need to complete the session creation in your browser. The application will provide instructions. 3. Session data is persisted between runs, so you don't need to create a new session each time. 4. The example includes proper error handling for Controller-specific errors, which include additional context through the `code` and `data` fields. 5. Keep your RPC endpoints and contract addresses secure, preferably in environment variables. ## Cartridge Controller React Integration This guide demonstrates how to integrate the Cartridge Controller with a React application. ### Installation :::code-group ```bash [npm] npm install @cartridge/connector @cartridge/controller @starknet-react/core @starknet-react/chains starknet npm install -D tailwindcss vite-plugin-mkcert ``` ```bash [pnpm] pnpm add @cartridge/connector @cartridge/controller @starknet-react/core @starknet-react/chains starknet pnpm add -D tailwindcss vite-plugin-mkcert ``` ```bash [yarn] yarn add @cartridge/connector @cartridge/controller @starknet-react/core @starknet-react/chains starknet yarn add -D tailwindcss vite-plugin-mkcert ``` ```bash [bun] bun add @cartridge/connector @cartridge/controller @starknet-react/core @starknet-react/chains starknet bun add -D tailwindcss vite-plugin-mkcert ``` ::: ### Basic Setup #### 1. Configure the Starknet Provider First, set up the Starknet provider with the Cartridge Controller connector: You can customize the `ControllerConnector` by providing configuration options during instantiation. The `ControllerConnector` accepts an options object that allows you to configure various settings such as policies, RPC URLs, theme, and more. > ⚠️ **Important**: The `ControllerConnector` instance must be created outside of any React components. Creating it inside a component will cause the connector to be recreated on every render, which can lead to connection issues. ```typescript import { sepolia, mainnet } from "@starknet-react/chains"; import { StarknetConfig, jsonRpcProvider, cartridge, } from "@starknet-react/core"; import { ControllerConnector } from "@cartridge/connector"; import { SessionPolicies } from "@cartridge/controller"; // Define your contract addresses const ETH_TOKEN_ADDRESS = '0x049d36570d4e46f48e99674bd3fcc84644ddd6b96f7c741b1562b82f9e004dc7' // Define session policies const policies: SessionPolicies = { contracts: { [ETH_TOKEN_ADDRESS]: { methods: [ { name: "approve", entrypoint: "approve", spender: "0x1234567890abcdef1234567890abcdef12345678", amount: "0xffffffffffffffffffffffffffffffff", description: "Approve spending of tokens", }, { name: "transfer", entrypoint: "transfer" }, ], }, }, } // Initialize the connector const connector = new ControllerConnector({ policies, // With the defaults, you can omit chains if you want to use: // - chains: [ // { rpcUrl: "https://api.cartridge.gg/x/starknet/sepolia" }, // { rpcUrl: "https://api.cartridge.gg/x/starknet/mainnet" }, // ] }) // Configure RPC provider const provider = jsonRpcProvider({ rpc: (chain: Chain) => { switch (chain) { case mainnet: default: return { nodeUrl: 'https://api.cartridge.gg/x/starknet/mainnet' }; case sepolia: return { nodeUrl: 'https://api.cartridge.gg/x/starknet/sepolia' } } }, }) export function StarknetProvider({ children }: { children: React.ReactNode }) { return ( {children} ) } ``` #### 2. Create a Wallet Connection Component Use the `useConnect`, `useDisconnect`, and `useAccount` hooks to manage wallet connections: ```typescript import { useEffect, useState } from 'react' import { useAccount, useConnect, useDisconnect } from '@starknet-react/core' import { ControllerConnector } from '@cartridge/connector' import { Button } from '@cartridge/controller-ui' export function ConnectWallet() { const { connect, connectors } = useConnect() const { disconnect } = useDisconnect() const { address } = useAccount() const controller = connectors[0] as ControllerConnector const [username, setUsername] = useState() useEffect(() => { if (!address) return controller.username()?.then((n) => setUsername(n)) }, [address, controller]) return (
{address && ( <>

Account: {address}

{username &&

Username: {username}

} )} {address ? ( ) : (
{/* Standard connection using default signupOptions */} {/* Dynamic authentication options for branded flows */}
)}
) } ``` #### 3. Dynamic Authentication Options The ControllerConnector now supports dynamic authentication configuration per connection call. This allows you to create multiple branded authentication flows while using a single Controller instance: ```typescript // Direct connector method - bypasses starknet-react state management controller.connect({ signupOptions: ["phantom-evm"] }) // For starknet-react integration, use the standard connect method connect({ connector: controller }) ``` ##### Key Points: * **Per-call Override**: `signupOptions` passed to `connect()` override the constructor defaults * **Branded Flows**: Create specific authentication buttons like "Login with Phantom", "Login with Google" * **Single Instance**: Use one Controller instance for multiple authentication methods * **React Integration**: Note that direct `controller.connect()` calls bypass starknet-react's state management ##### Complete Example with Multiple Auth Options: ```tsx import { useConnect, useAccount } from '@starknet-react/core' import { ControllerConnector } from '@cartridge/connector' export function MultiAuthConnectWallet() { const { connect, connectors } = useConnect() const { address } = useAccount() const controller = connectors[0] as ControllerConnector const handleSpecificAuth = async (signupOptions: string[]) => { try { // Direct controller connection for specific auth options await controller.connect({ signupOptions }) // Manually trigger starknet-react state update connect({ connector: controller }) } catch (error) { console.error('Connection failed:', error) } } if (address) { return
Connected: {address}
} return (

Choose your authentication method:

{/* Standard multi-option flow */} {/* Branded single-option flows */}
) } ``` #### 4. Headless Authentication For programmatic authentication without opening any UI, you can use headless mode in your React components: ```tsx import { useCallback, useState } from 'react' import { useConnect } from '@starknet-react/core' import { ControllerConnector } from '@cartridge/connector' export function HeadlessLogin() { const { connectAsync, connectors } = useConnect() const [username, setUsername] = useState('') const [loading, setLoading] = useState(false) const controller = connectors[0] as ControllerConnector const handleHeadlessLogin = useCallback(async (signer: string) => { if (!username) { alert('Please enter a username') return } setLoading(true) try { // Ensure we start fresh if (controller.account) { await controller.disconnect() } // Headless authentication const account = await controller.connect({ username, signer, }) if (!account) { throw new Error('Failed to authenticate') } // Sync with starknet-react state await connectAsync({ connector: controller }) alert(`Successfully authenticated as ${username}!`) } catch (error) { console.error('Headless authentication failed:', error) alert('Authentication failed: ' + (error as Error).message) } finally { setLoading(false) } }, [username, controller, connectAsync]) return (
setUsername(e.target.value)} placeholder="Enter your username" disabled={loading} />
{loading &&

Authenticating...

}
) } ``` :::warning Headless mode requires that the user already has the specified signer (passkey, OAuth account, EVM wallet) associated with their Cartridge username. For new user registration, use the regular `connect()` flow which opens the UI. ::: #### 5. Performing Transactions Execute transactions using the `account` object from `useAccount` hook: ```typescript import { useAccount, useExplorer } from '@starknet-react/core' import { useCallback, useState } from 'react' const ETH_CONTRACT = '0x049d36570d4e46f48e99674bd3fcc84644ddd6b96f7c741b1562b82f9e004dc7' export const TransferEth = () => { const [submitted, setSubmitted] = useState(false) const { account } = useAccount() const explorer = useExplorer() const [txnHash, setTxnHash] = useState() const execute = useCallback( async (amount: string) => { if (!account) return setSubmitted(true) setTxnHash(undefined) try { const result = await account.execute([ { contractAddress: ETH_CONTRACT, entrypoint: 'approve', calldata: [account?.address, amount, '0x0'], }, { contractAddress: ETH_CONTRACT, entrypoint: 'transfer', calldata: [account?.address, amount, '0x0'], }, ]) setTxnHash(result.transaction_hash) } catch (e) { console.error(e) } finally { setSubmitted(false) } }, [account], ) if (!account) return null return (

Transfer ETH

{txnHash && (

Transaction hash:{' '} {txnHash}

)}
) } ``` #### 4. Username Lookup The Controller provides a `lookupUsername` method that allows you to check if a username exists and see what authentication options are available for existing accounts. This is particularly useful for headless flows where you want to determine login vs signup flows: ```typescript import { useState, useCallback } from 'react' import { useConnect } from '@starknet-react/core' import { ControllerConnector } from '@cartridge/connector' export function UsernameLookup() { const { connectors } = useConnect() const controller = connectors[0] as ControllerConnector const [username, setUsername] = useState('') const [lookupResult, setLookupResult] = useState(null) const [isLoading, setIsLoading] = useState(false) const handleLookup = useCallback(async () => { if (!username.trim()) return setIsLoading(true) try { const result = await controller.lookupUsername(username.trim()) setLookupResult(result) } catch (error) { console.error('Lookup failed:', error) setLookupResult(null) } finally { setIsLoading(false) } }, [controller, username]) const handleHeadlessConnect = useCallback(async (signer: string) => { try { await controller.connect({ username: username.trim(), signer: signer as any, }) } catch (error) { console.error('Connection failed:', error) } }, [controller, username]) return (
setUsername(e.target.value)} className="border p-2 rounded" />
{lookupResult && (

Username: {lookupResult.username}

Exists: {lookupResult.exists ? 'Yes' : 'No'}

{lookupResult.exists && lookupResult.signers.length > 0 && (

Available authentication methods:

{lookupResult.signers.map((signer: string) => ( ))}
)} {!lookupResult.exists && (

Username is available for signup

)}
)}
) } ``` ##### Lookup Response Format The `lookupUsername` method returns an object with the following structure: ```typescript interface HeadlessUsernameLookupResult { username: string; // The username that was looked up exists: boolean; // Whether the username exists signers: AuthOption[]; // Available authentication methods, e.g. ["webauthn", "google", "password"] } ``` Available `AuthOption` values include: * `"webauthn"` - Passkey/WebAuthn authentication * `"password"` - Password-based authentication * `"sms"` - SMS one-time passcode authentication * `"google"` - Google OAuth * `"discord"` - Discord OAuth * `"walletconnect"` - WalletConnect * `"metamask"` - MetaMask wallet * `"rabby"` - Rabby wallet * `"phantom-evm"` - Phantom wallet (EVM) #### 5. Add Components to Your App ```typescript import { StarknetProvider } from './context/StarknetProvider' import { ConnectWallet } from './components/ConnectWallet' import { TransferEth } from './components/TransferEth' import { UsernameLookup } from './components/UsernameLookup' function App() { return ( ) } export default App ``` ### Development and Testing If you're working with the Cartridge Controller repository examples, you can use two development modes: ```bash # Local development with local APIs pnpm dev # Testing with production APIs (hybrid mode) pnpm dev:live ``` The `dev:live` mode is useful when you need to test your React application against production data while keeping your local development environment. ### Important Notes Make sure to use HTTPS in development by configuring Vite: ```typescript import { defineConfig } from 'vite' import react from '@vitejs/plugin-react' import mkcert from 'vite-plugin-mkcert' export default defineConfig({ plugins: [react(), mkcert()], }) ``` ### External Wallet Integration If you're using external wallets (MetaMask, Rabby, etc.) with Cartridge Controller, you can wait for transaction confirmations using the `externalWaitForTransaction` method: ```typescript import { useState, useCallback } from 'react' import { ControllerConnector } from '@cartridge/connector' import { useConnect } from '@starknet-react/core' export const ExternalWalletTransaction = () => { const { connectors } = useConnect() const controller = connectors[0] as ControllerConnector const [txHash, setTxHash] = useState() const [isWaiting, setIsWaiting] = useState(false) const [receipt, setReceipt] = useState() const waitForTransaction = useCallback(async () => { if (!txHash || !controller) return setIsWaiting(true) try { // Wait for transaction confirmation with 30-second timeout const response = await controller.externalWaitForTransaction( 'metamask', // or 'rabby', 'phantom', etc. txHash, 30000 // 30 seconds ) if (response.success) { setReceipt(response.result) console.log('Transaction confirmed:', response.result) } else { console.error('Transaction failed:', response.error) } } catch (error) { console.error('Error waiting for transaction:', error) } finally { setIsWaiting(false) } }, [txHash, controller]) return (

External Wallet Transaction Monitor

setTxHash(e.target.value)} /> {receipt && (

Transaction Receipt:

{JSON.stringify(receipt, null, 2)}
)}
) } ``` #### External Wallet Methods The Controller provides several methods for interacting with external wallets: * `externalSwitchChain(walletType, chainId)` - Switch the connected wallet to a different chain * `externalWaitForTransaction(walletType, txHash, timeoutMs?)` - Wait for transaction confirmation * `externalSendTransaction(walletType, transaction)` - Send a transaction through the external wallet These methods work with all supported external wallet types: `metamask`, `rabby`, `phantom`, `argent`, and `walletconnect`. ## Rust ### Installation Add the `account_sdk` crate to your `Cargo.toml`: ```toml [dependencies] account_sdk = { git = "https://github.com/cartridge-gg/controller-rs.git", package = "account_sdk" } starknet = "0.10" # Make sure to use a compatible version ``` #### Importing Necessary Modules ```rust use account_sdk::{ controller::Controller, signers::Signer, }; use starknet::{ accounts::Account, providers::Provider, signers::SigningKey, core::types::FieldElement, }; ``` #### Setting Up the Controller Initialize the Controller with necessary parameters: ```rust #[tokio::main] async fn main() { // Create a signer (replace with your own private key) let owner = Signer::Starknet(SigningKey::from_secret_scalar(FieldElement::from_hex_be("0xYourPrivateKey").unwrap())); // Initialize the provider (replace with your RPC URL) let provider = Provider::try_from("http://localhost:5050").unwrap(); let chain_id = provider.chain_id().await.unwrap(); // Create a new Controller instance let username = "testuser".to_string(); let controller = Controller::new( "your_app_id".to_string(), username.clone(), FieldElement::from_hex_be("0xYourClassHash").unwrap(), // Class hash "http://localhost:5050".parse().unwrap(), // RPC URL owner.clone(), FieldElement::from_hex_be("0xYourControllerAddress").unwrap(), // Controller address chain_id, ); // Deploy the controller controller.deploy().await.unwrap(); // Interact with the controller // For example, execute a transaction let call = your_function_call(); // Define your function call controller.execute(vec![call], None).await.unwrap(); } ``` #### Performing Transactions Define function calls and execute them: ```rust fn your_function_call() -> starknet::core::types::FunctionCall { starknet::core::types::FunctionCall { contract_address: FieldElement::from_hex_be("0xYourContractAddress").unwrap(), entry_point_selector: starknet::core::utils::get_selector_from_name("yourEntryPoint").unwrap(), calldata: vec![FieldElement::from(123)], // Replace with your calldata } } ``` Execute the function call using the Controller: ```rust controller.execute(vec![your_function_call()], None).await.unwrap(); ``` ## Svelte ### Installation :::code-group ```bash [npm] npm install @cartridge/controller starknet ``` ```bash [pnpm] pnpm install @cartridge/controller starknet ``` ```bash [yarn] yarn add @cartridge/controller starknet ``` ```bash [bun] bun add @cartridge/controller starknet ``` ::: ### Setting Up the Controller Import the `Controller` and create an instance: ```typescript // src/routes/+page.svelte import { onMount } from "svelte"; import Controller from "@cartridge/controller"; import { account, username } from "../stores/account"; import { ETH_CONTRACT } from "../constants"; let controller = new Controller({ policies: { contracts: { [ETH_CONTRACT]: { methods: [ { name: "approve", entrypoint: "approve", spender: "0x1234567890abcdef1234567890abcdef12345678", amount: "0xffffffffffffffffffffffffffffffff", description: "Approve spending of tokens", }, { name: "transfer", entrypoint: "transfer", description: "Transfer tokens", }, ], }, }, }, }); ``` #### Connecting a Wallet Use the `connect` method to establish a connection: ```svelte ``` #### Disconnecting a Wallet Implement a disconnect function: ```svelte ``` #### Displaying User Information Create a `UserInfo` component to show account details: ```svelte

User Information

{#if accountAddress}

Account Address: {accountAddress}

{:else}

No account connected

{/if} {#if username}

Username: {username}

{/if}
``` #### Performing Transactions Create a `TransferEth` component for executing transactions: ```svelte

Transfer Eth

``` #### Full Example Here's how your main `+page.svelte` might look: ```svelte

SvelteKit + Controller Example

{#if loading}

Loading

{:else if $account} {:else} {/if}
{#if $account && !loading} {/if} ``` This example demonstrates how to set up the Controller, connect/disconnect a wallet, display user information, and perform transactions in a Svelte application using the Cartridge Controller. ## Android Integration Cartridge Controller provides native Android support through UniFFI-generated Kotlin bindings. This enables seamless integration with Android applications while maintaining full access to Controller functionality. ### Prerequisites * Android Studio with NDK installed * Rust toolchain with Android targets: ```bash rustup target add aarch64-linux-android armv7-linux-androideabi x86_64-linux-android i686-linux-android ``` * cargo-ndk for cross-compilation: ```bash cargo install cargo-ndk ``` ### Installation Add the UniFFI-generated bindings to your Android project. The bindings are generated from the Controller Rust library and include all necessary Kotlin classes and interfaces. Place the generated `controller_uniffi.kt` file in your source directory and ensure the native libraries are included in `jniLibs/`. ### Core Types The binding exposes several key types for interacting with Controller. #### FieldElement Blockchain field elements are represented as strings: ```kotlin typealias FieldElement = String ``` #### Call Represents a contract call: ```kotlin data class Call( var contractAddress: FieldElement, var entrypoint: String, var calldata: List ) ``` #### Session Policies Define permissions for session accounts: ```kotlin data class SessionPolicy( var contractAddress: FieldElement, var entrypoint: String ) data class SessionPolicies( var policies: List, var maxFee: FieldElement ) ``` #### SignerType Supported authentication methods: ```kotlin enum class SignerType { WEBAUTHN, STARKNET } ``` ### Creating a Controller Initialize a headless Controller with your own signing key: ```kotlin import com.cartridge.controller.* val owner = Owner(privateKey = "0x...") // Class hash for Controller contract (use appropriate version) val classHash = "0x05f..." val controller = controllerNewHeadless( appId = "my_app", username = "player123", classHash = classHash, rpcUrl = "https://api.cartridge.gg/x/starknet/sepolia", owner = owner, chainId = "0x534e5f5345504f4c4941" // SEPOLIA ) // Access controller properties val address: FieldElement = controller.address() val appId: String = controller.appId() val chainId: FieldElement = controller.chainId() val username: String = controller.username() ``` ### User Authentication Handle user signup and chain switching: ```kotlin // Sign up with signer type try { controller.signup( signerType = SignerType.STARKNET, sessionExpiration = null, cartridgeApiUrl = null ) } catch (e: ControllerException) { // Handle signup error } // Switch to a different chain controller.switchChain(rpcUrl = "https://api.cartridge.gg/x/starknet/mainnet") // Disconnect the controller controller.disconnect() ``` ### Creating a SessionAccount Create a session account for executing transactions without repeated signatures: ```kotlin val sessionAccount = SessionAccount( rpcUrl = "https://api.cartridge.gg/x/starknet/sepolia", privateKey = "0x...", address = "0x...", ownerGuid = "0x...", chainId = "0x534e5f5345504f4c4941", policies = SessionPolicies( policies = listOf( SessionPolicy( contractAddress = "0x049d36570d4e46f48e99674bd3fcc84644ddd6b96f7c741b1562b82f9e004dc7", entrypoint = "transfer" ) ), maxFee = "0x2386f26fc10000" ), sessionExpiration = 3600UL // 1 hour ) ``` Alternatively, create from subscription (waits for browser authorization): ```kotlin val sessionAccount = sessionAccountCreateFromSubscribe( privateKey = "0x...", policies = policies, rpcUrl = "https://api.cartridge.gg/x/starknet/sepolia", cartridgeApiUrl = "https://api.cartridge.gg" ) ``` ### Executing Transactions Execute transactions through the Controller or SessionAccount: ```kotlin val calls = listOf( Call( contractAddress = "0x049d36570d4e46f48e99674bd3fcc84644ddd6b96f7c741b1562b82f9e004dc7", entrypoint = "transfer", calldata = listOf( "0x1234...", // recipient "0x100", // amount low "0x0" // amount high ) ) ) // Execute via Controller try { val txHash = controller.execute(calls = calls) println("Transaction submitted: $txHash") } catch (e: ControllerException) { // Handle execution error } // Execute via SessionAccount try { val txHash = sessionAccount.execute(calls = calls) println("Session transaction: $txHash") } catch (e: ControllerException) { // Handle error } // Execute from outside (for meta-transactions) try { val txHash = sessionAccount.executeFromOutside(calls = calls) println("External transaction: $txHash") } catch (e: ControllerException) { // Handle error } ``` #### Transfer Helper Transfer tokens directly: ```kotlin try { val txHash = controller.transfer( recipient = "0x1234...", amount = "0x100" ) } catch (e: ControllerException) { // Handle transfer error } ``` ### Error Handling The bindings define `ControllerException` for error handling: ```kotlin try { controller.execute(calls = calls) } catch (e: ControllerException.InitializationException) { println("Initialization failed: ${e.message}") } catch (e: ControllerException.SignupException) { println("Signup failed: ${e.message}") } catch (e: ControllerException.ExecutionException) { println("Execution failed: ${e.message}") } catch (e: ControllerException.NetworkException) { println("Network error: ${e.message}") } catch (e: ControllerException.StorageException) { println("Storage error: ${e.message}") } catch (e: ControllerException.InvalidInput) { println("Invalid input: ${e.message}") } catch (e: ControllerException.DisconnectException) { println("Disconnect failed: ${e.message}") } ``` ### Building Native Libraries Build the native libraries for each Android ABI using cargo-ndk: ```bash # Build for all supported architectures cargo ndk -t armeabi-v7a -t arm64-v8a -t x86 -t x86_64 -o ./jniLibs build --release # Or build for specific targets cargo ndk -t arm64-v8a -o ./jniLibs build --release ``` Place the resulting `.so` files in your Android project: ``` app/ src/ main/ jniLibs/ arm64-v8a/ libcontroller_uniffi.so armeabi-v7a/ libcontroller_uniffi.so x86/ libcontroller_uniffi.so x86_64/ libcontroller_uniffi.so ``` Load the library in your application: ```kotlin companion object { init { System.loadLibrary("controller_uniffi") } } ``` ## Capacitor [Capacitor](https://capacitorjs.com/) allows you to wrap an existing web application for native iOS and Android distribution. This approach uses Controller's SessionProvider for session-based authentication via deep links and custom URL schemes. ### When to Use Capacitor Capacitor is ideal when: * You have an existing web app using the Controller * You want to distribute through the App Store and Play Store * You need access to native features (push notifications, in-app purchases, haptics) * Your team is more comfortable with web technologies than native development ### Prerequisites * Node.js >= 18 * Xcode (for iOS) * Android Studio (for Android) * An existing web app with the Controller integration ### Installation Add Capacitor to your project: ```bash npm install @capacitor/core @capacitor/cli npm install @capacitor/ios @capacitor/android npm install @capacitor/app @capacitor/browser npx cap init ``` ### Configuration Create `capacitor.config.ts` in your project root: ```typescript import type { CapacitorConfig } from "@capacitor/cli"; const config: CapacitorConfig = { appId: "com.yourapp.id", appName: "Your App Name", webDir: "dist", // Your build output directory server: { hostname: "my-custom-app", // Recommended: Set a custom hostname for production androidScheme: "https", iosScheme: "capacitor", }, }; export default config; ``` #### Security and Custom Hostnames For production apps, it is **strongly recommended** to set a custom hostname to prevent other Capacitor apps from potentially spoofing your origin. By default, Capacitor apps use `localhost` as the hostname (`capacitor://localhost` on iOS). While this is automatically verified by the Keychain for development convenience, custom hostnames require explicit authorization in your Controller preset configuration. ##### Setting up Custom Hostnames: 1. **Configure your Capacitor app** with a custom hostname: ```typescript // capacitor.config.ts server: { hostname: "my-custom-app", iosScheme: "capacitor", androidScheme: "https" } ``` 2. **Authorize the hostname in your Controller preset**. The Keychain will only verify custom Capacitor origins if the hostname is explicitly listed in your preset's allowed origins: ```json { "origin": ["my-custom-app"], // ... other preset configuration } ``` This ensures your app is verified on both platforms: * **iOS**: `capacitor://my-custom-app` * **Android**: `https://my-custom-app` **Important**: The default `localhost` origin (`capacitor://localhost`) is always allowed for development convenience, but custom hostnames must be explicitly authorized in your preset configuration. ### Controller Setup Use `SessionProvider` from `@cartridge/controller/session` for native apps. The key difference is the `redirectUrl` parameter, which uses a custom URL scheme. ```typescript import SessionProvider from "@cartridge/controller/session"; import { constants } from "starknet"; const policies = { contracts: { "0x049d36570d4e46f48e99674bd3fcc84644ddd6b96f7c741b1562b82f9e004dc7": { methods: [{ name: "transfer", entrypoint: "transfer" }], }, }, }; const provider = new SessionProvider({ rpc: "https://api.cartridge.gg/x/starknet/sepolia", chainId: constants.StarknetChainId.SN_SEPOLIA, redirectUrl: "myapp://session", // Custom URL scheme policies, keychainUrl: "https://x.cartridge.gg", // Optional: customize keychain URL }); ``` ### Browser Interception In native Capacitor apps, you need to intercept window\.open calls to ensure the system browser opens for authentication: ```typescript import { Capacitor } from "@capacitor/core"; import { Browser } from "@capacitor/browser"; if (Capacitor.isNativePlatform()) { const originalOpen = window.open; window.open = ((url: string | URL) => { Browser.open({ url: url.toString() }).catch((error) => { console.warn("Failed to open browser", error); }); return null as Window | null; }) as typeof window.open; window.addEventListener("beforeunload", () => { window.open = originalOpen; }); } ``` ### Deep Link Handling The authentication flow opens an in-app browser, then redirects back via your custom URL scheme. #### iOS Configuration Add your URL scheme to `ios/App/App/Info.plist`: ```xml CFBundleURLTypes CFBundleURLSchemes cartridge-session ``` #### Android Configuration Add intent filters inside the main `` tag in `android/app/src/main/AndroidManifest.xml`: ```xml ``` Note: The `android:launchMode="singleTask"` ensures deep links open in the existing app instance. #### Handling the Redirect Listen for the app URL open event and process the session data: ```typescript import { App } from "@capacitor/app"; import { Browser } from "@capacitor/browser"; const handleDeepLink = async (url: string) => { try { const parsed = new URL(url); const startapp = parsed.searchParams.get("startapp"); if (!startapp) { return; } // Ingest the session from the redirect payload const stored = provider.ingestSessionFromRedirect(startapp); if (!stored) { throw new Error("Invalid session payload"); } await Browser.close().catch(() => undefined); const account = await provider.probe(); if (account) { console.log("Session ready. Address:", account.address); } } catch (error) { console.error("Failed to handle deep link", error); } }; if (Capacitor.isNativePlatform()) { App.addListener("appUrlOpen", ({ url }) => { if (url) { handleDeepLink(url); } }); } ``` ### Build and Deploy Build your web app, then sync with Capacitor: ```bash npm run build npx cap copy ``` Open in the native IDE: :::code-group ```bash [iOS] npx cap open ios ``` ```bash [Android] npx cap open android ``` ::: From Xcode or Android Studio, build and archive for distribution. ### Platform Detection Use Capacitor's platform detection for conditional logic: ```typescript import { Capacitor } from "@capacitor/core"; const platform = Capacitor.getPlatform(); const isNative = platform === "ios" || platform === "android"; const isIOS = platform === "ios"; const isAndroid = platform === "android"; ``` ### Configuration Options #### Custom Keychain URL You can customize the keychain URL using the `keychainUrl` parameter or environment variable: ```typescript const provider = new SessionProvider({ // ... other config keychainUrl: process.env.VITE_KEYCHAIN_URL || "https://x.cartridge.gg", }); ``` This is useful for: * Testing with development keychain instances * Enterprise deployments with custom keychain servers * Local development with different keychain configurations ### Additional Native Features Capacitor provides plugins for common native features: * `@capacitor/push-notifications` - Push notifications * `@capacitor/preferences` - Key-value storage * `@capacitor/haptics` - Vibration feedback * `@capacitor/device` - Device information Install and configure these as needed for your application. ### Session Management Example A complete Capacitor session example with iOS app integration is available in the repository. This example demonstrates: * **Mobile Session Management**: Comprehensive cross-platform session handling * **Deep Link Integration**: Proper URL scheme handling for iOS and Android * **Browser Interception**: Native browser management for authentication flows * **Session Persistence**: Local storage management and session restoration To run the example: ```bash # From repo root pnpm install pnpm -C examples/capacitor dev # For native iOS testing pnpm -C examples/capacitor exec -- cap add ios pnpm -C examples/capacitor exec -- cap sync pnpm -C examples/capacitor exec -- cap run ios -l --external ``` The example includes complete setup for both iOS and Android platforms with proper deep link configuration. ### Example Projects * **Complete Reference**: See the [Capacitor session example](https://github.com/cartridge-gg/controller/tree/main/examples/capacitor) in the repository * **Production Example**: [Jokers of Neon](https://github.com/caravana-studio/jokers-of-neon-app) repository ## Native Headless Controller :::info This guide covers **native headless mode** using C++ bindings for server-side applications. For **web-based headless authentication** in browser applications, see the [Headless Authentication](../headless-authentication) guide. ::: ### Setup Here is an example of how to set up a headless Controller with the UniFFI C++ bindings: ```cpp #include "controller.hpp" #include #include // IMPORTANT: never commit private keys in source code std::string private_key = "0x1234..."; std::string app_id = "my_app"; std::string username = "player123"; std::string rpc_url = "https://api.cartridge.gg/x/starknet/sepolia"; std::string chain_id = "0x534e5f5345504f4c4941"; // SEPOLIA // Get the latest Controller class hash std::string class_hash = controller::get_controller_class_hash( controller::Version::kLatest ); // Create owner from locally-generated private key std::shared_ptr owner = controller::Owner::init(private_key); // Create a new headless Controller std::shared_ptr ctrl = controller::Controller::new_headless( app_id, username, class_hash, rpc_url, owner, chain_id ); // Access controller properties std::string address = ctrl->address(); std::string user = ctrl->username(); // Register a new Controller account onchain ctrl->signup( controller::SignerType::kStarknet, std::nullopt, // session_expiration std::nullopt // cartridge_api_url ); // Execute transactions controller::Call call{ "0x049d36570d4e46f48e99674bd3fcc84644ddd6b96f7c741b1562b82f9e004dc7", "transfer", {"0x1234...", "0x100", "0x0"} }; std::string tx_hash = ctrl->execute({call}); ``` :::warning Never commit private keys in source code. Store them securely using environment variables, secret managers, or hardware security modules. ::: ### Full Example A complete working example is available in the Controller.c repository: [C++ example](https://github.com/cartridge-gg/controller.c/tree/main/examples/cpp) ## iOS Integration Cartridge Controller provides native iOS support through UniFFI-generated Swift bindings. This enables seamless integration with iOS applications while maintaining full access to Controller functionality. ### Prerequisites * Xcode 15+ * iOS 17+ deployment target * Swift 5.5+ * Rust toolchain (for building the library) ### Installation Build the Swift bindings from the Controller.c repository: ```bash cd controller.c cargo build --release ./scripts/build_swift.sh ``` This generates: * `libcontroller_uniffi.dylib` - The native library * `controller_uniffi.swift` - Swift wrapper * `controller_uniffiFFI.h` - C headers for bridging Add these files to your Xcode project and configure: * **Bridging Header:** Include `controller_uniffiFFI.h` * **Other Linker Flags:** `-lcontroller_uniffi` * **Library Search Paths:** Path to `target/release/` ### Core Types #### FieldElement Blockchain field elements are represented as strings: ```swift typealias ControllerFieldElement = String ``` #### Call Represents a contract call: ```swift struct Call { var contractAddress: ControllerFieldElement var entrypoint: String var calldata: [ControllerFieldElement] } ``` #### Session Policies Define permissions for session accounts: ```swift struct SessionPolicy { var contractAddress: ControllerFieldElement var entrypoint: String } struct SessionPolicies { var policies: [SessionPolicy] var maxFee: ControllerFieldElement } ``` ### Creating a ControllerAccount Initialize a headless ControllerAccount with your own signing key: ```swift import Foundation let owner = try Owner(privateKey: "0x...") let classHash = try getControllerClassHash(version: .latest) let controller = try ControllerAccount.newHeadless( appId: "my_app", username: "player123", classHash: classHash, rpcUrl: "https://api.cartridge.gg/x/starknet/sepolia", owner: owner, chainId: "0x534e5f5345504f4c4941" // SEPOLIA ) // Access controller properties let address = try controller.address() let username = try controller.username() ``` ### Creating a SessionAccount Generate a keypair and create a session via browser authorization. For more details on session-based authentication, see [Session Flow](./session-flow). ```swift // Generate random private key var bytes = [UInt8](repeating: 0, count: 32) SecRandomCopyBytes(kSecRandomDefault, bytes.count, &bytes) let privateKey = "0x" + bytes.map { String(format: "%02x", $0) }.joined() let publicKey = try getPublicKey(privateKey: privateKey) // Define policies let policies = SessionPolicies( policies: [ SessionPolicy( contractAddress: "0x049d36570d4e46f48e99674bd3fcc84644ddd6b96f7c741b1562b82f9e004dc7", entrypoint: "transfer" ) ], maxFee: "0x2386f26fc10000" ) // Create session from API subscription let session = try SessionAccount.createFromSubscribe( privateKey: privateKey, policies: policies, rpcUrl: "https://api.cartridge.gg/x/starknet/sepolia", cartridgeApiUrl: "https://api.cartridge.gg" ) // Check session metadata let address = session.address() let username = session.username() let expiresAt = session.expiresAt() let isExpired = session.isExpired() ``` ### Executing Transactions Execute transactions through the Controller or SessionAccount: ```swift let call = Call( contractAddress: "0x049d36570d4e46f48e99674bd3fcc84644ddd6b96f7c741b1562b82f9e004dc7", entrypoint: "transfer", calldata: ["0x1234...", "0x1000", "0x0"] ) // Via Controller let txHash = try controller.execute(calls: [call]) // Via SessionAccount (no signature required) let txHash = try session.executeFromOutside(calls: [call]) ``` ### Error Handling The bindings define `ControllerError` for error handling: ```swift do { try controller.execute(calls: [call]) } catch let error as ControllerError { switch error { case .InitializationError(let message): print("Init failed: \(message)") case .SignupError(let message): print("Signup failed: \(message)") case .ExecutionError(let message): print("Execution failed: \(message)") case .NetworkError(let message): print("Network error: \(message)") case .StorageError(let message): print("Storage error: \(message)") case .InvalidInput(let message): print("Invalid input: \(message)") case .DisconnectError(let message): print("Disconnect failed: \(message)") } } ``` ### Utility Functions ```swift // Derive public key from private key let publicKey = try getPublicKey(privateKey: privateKey) // Convert signer to GUID let guid = try signerToGuid(privateKey: privateKey) // Get Controller class hash let classHash = try getControllerClassHash(version: .latest) // Validate felt format let isValid = try validateFelt(felt: "0x123...") // Check if storage exists for app let hasStorage = try controllerHasStorage(appId: "my_app") ``` ### Example Projects Complete working examples are available in the Controller.c repository: * [Swift examples](https://github.com/cartridge-gg/controller.c/tree/main/examples/swift) * [iOS app examples](https://github.com/cartridge-gg/controller.c/tree/main/examples/ios-app) ## Native Integration Controller was initially developed as a web wallet for browser-based applications. As the ecosystem has grown, native integration is now supported across mobile and server-side platforms. The most important decision when integrating Controller natively is **which connection flow** to use. Your choice depends on your platform, your UX requirements, and whether you need browser-based authentication. ### Connection Flows #### Browser-Based Sessions The most common flow for native apps. Your app generates a local keypair, opens a browser for the user to authenticate with their Controller account, and receives a session signer back. The session signer can then execute transactions without further user interaction. **How it works:** 1. Generate a new keypair locally and store the private key securely 2. Open a browser to the Controller session URL, passing the public key and session policies 3. The user authenticates via WebAuthn in the browser 4. The app receives the user's Controller address via callback 5. The app signs transactions using the local private key, validated against the registered session **Best for:** Mobile apps, games, any app where end users authenticate interactively. **Platform guides:** [React Native](/controller/native/react-native) | [Android](/controller/native/android) | [iOS](/controller/native/ios) | [Capacitor](/controller/native/capacitor) **Reference:** [Session URL Reference](/controller/native/session-flow) for URL parameters, policy format, and callback metadata. #### Headless (App-Managed Keys) Your app supplies its own signing keys rather than using the Cartridge keychain. No browser authentication is involved --- the app directly controls a Controller account using a private key it manages. **Best for:** * Server-side execution and automated game backends * Single owner managing multiple Controller accounts * Bots, NPCs, or game systems that interact with the blockchain * Applications with specific key management or compliance requirements **Implementation:** [Headless Controller](/controller/native/headless) (C++ UniFFI bindings) #### Web Wrapper (Capacitor) If you already have a web app using Controller, you can wrap it in a native shell for app store distribution. Your app uses the web `SessionProvider` directly, with deep link redirects handling the browser-to-app callback. This is not a different authentication flow --- it uses the same browser-based session flow as the web SDK. The difference is in the integration approach: you're packaging a web app rather than building a native one. **Best for:** Existing web apps, faster time-to-market, teams stronger in web than native development. **Platform guide:** [Capacitor](/controller/native/capacitor) ### Choosing a Flow | Use Case | Flow | Platform Guides | | ------------------------------- | ---------------------- | --------------------------------------------------------------------------------------------------------------------- | | Mobile game with player login | Browser-based sessions | [React Native](/controller/native/react-native), [Android](/controller/native/android), [iOS](/controller/native/ios) | | Existing web app → app store | Web wrapper | [Capacitor](/controller/native/capacitor) | | Game backend / server-side bots | Headless | [Headless Controller](/controller/native/headless) | | Custom key management | Headless | [Headless Controller](/controller/native/headless) | ### Passkey Authentication on Native To enable passkey sign-in on native applications, you must configure your preset with Apple App Site Association (AASA). This allows the operating system to recognize your app as authorized for WebAuthn credentials associated with your domain. See the [Presets documentation](/controller/presets#apple-app-site-association) for configuration details. ## React Native The Controller SDK can be integrated into React Native applications using TurboModules and the Controller.c FFI bindings. This enables session-based authentication and transaction execution in cross-platform mobile apps. :::info This guide uses the native Controller.c bindings to implement the [session flow](/controller/native/session-flow) directly. This is the native equivalent of [SessionProvider](/controller/getting-started#sessionprovider-redirect-based) --- it generates a local session keypair, authenticates via browser, and executes transactions with the session key. It is **not** the [headless controller](/controller/native/headless) pattern, which uses application-managed owner keys without any browser authentication. If you are wrapping an existing web app for mobile distribution, consider [Capacitor](/controller/native/capacitor) instead, which uses the JS `SessionProvider` directly. ::: ### Prerequisites * Node.js >= 20 * pnpm * Xcode (for iOS) * CocoaPods (for iOS) * Android Studio with NDK (for Android) * Rust with Android targets (for building Android native libs) ### Quick Start #### Installation ```bash pnpm install ``` #### Generate Native Projects ```bash pnpm exec expo prebuild ``` #### Run the App :::code-group ```bash [iOS] pnpm run ios ``` ```bash [Android] pnpm run android ``` ::: ### Module Setup The Controller SDK native module is available through the generated bindings from `uniffi-bindgen-react-native`. Import the module to access Controller functionality: ```typescript import Controller from './modules/controller/src'; // Access controller functions const publicKey = Controller.controller.getPublicKey(privateKey); ``` ### Session Management The `useSessionManager` hook handles the complete session lifecycle: key generation, browser-based authentication, and transaction execution. #### Key Generation and Storage The hook generates a random 32-byte private key and stores it in AsyncStorage. The corresponding public key is derived using the Controller module. ```typescript import 'react-native-get-random-values'; // Required for crypto.getRandomValues import AsyncStorage from '@react-native-async-storage/async-storage'; import Controller from './modules/controller/src'; const STORAGE_KEY = 'session_private_key'; const generateRandomKey = (): string => { const bytes = new Uint8Array(32); crypto.getRandomValues(bytes); return '0x' + Array.from(bytes).map(b => b.toString(16).padStart(2, '0')).join(''); }; // Load existing key or generate new one const savedKey = await AsyncStorage.getItem(STORAGE_KEY); if (savedKey) { const publicKey = Controller.controller.getPublicKey(savedKey); } else { const newKey = generateRandomKey(); await AsyncStorage.setItem(STORAGE_KEY, newKey); const publicKey = Controller.controller.getPublicKey(newKey); } ``` #### Opening Browser for Session Auth Sessions are created by opening a browser to the Cartridge keychain with the public key and requested session policies. ```typescript import * as WebBrowser from 'expo-web-browser'; const KEYCHAIN_URL = 'https://x.cartridge.gg'; const RPC_URL = 'https://api.cartridge.gg/x/starknet/sepolia'; const policies = [ { target: '0x049d36570d4e46f48e99674bd3fcc84644ddd6b96f7c741b1562b82f9e004dc7', method: 'transfer' }, { target: '0x049d36570d4e46f48e99674bd3fcc84644ddd6b96f7c741b1562b82f9e004dc7', method: 'approve' }, ]; const params = new URLSearchParams({ public_key: publicKey, policies: JSON.stringify(policies), rpc_url: RPC_URL, }); const url = `${KEYCHAIN_URL}/session?${params.toString()}`; await WebBrowser.openBrowserAsync(url); ``` #### Creating SessionAccount from Subscription While the browser is open, the app subscribes for session authorization. Once the user authorizes, a `SessionAccount` is created. ```typescript import { SessionAccount, type SessionPolicy, type Call } from './modules/controller/src'; const CARTRIDGE_API_URL = 'https://api.cartridge.gg'; const sessionPolicies = { policies: [ { contractAddress: '0x049d...', entrypoint: 'transfer' }, { contractAddress: '0x049d...', entrypoint: 'approve', spender: '0x1234567890abcdef1234567890abcdef12345678', amount: '0xffffffffffffffffffffffffffffffff' }, ], maxFee: '0x2386f26fc10000', // ~0.01 ETH }; const session = SessionAccount.createFromSubscribe( privateKey, sessionPolicies, RPC_URL, CARTRIDGE_API_URL ); // Access session metadata const address = session.address(); const username = session.username(); const expiresAt = session.expiresAt(); ``` #### Executing Transactions Once a session is established, transactions can be executed without additional user approval. ```typescript const calls: Call[] = [{ contractAddress: '0x049d36570d4e46f48e99674bd3fcc84644ddd6b96f7c741b1562b82f9e004dc7', entrypoint: 'transfer', calldata: [recipientAddress, amount, '0x0'], }]; try { const txHash = sessionAccount.executeFromOutside(calls); console.log('Transaction submitted:', txHash); } catch (error) { console.error('Transaction failed:', error); } ``` ### Full Example ```typescript import { useSessionManager } from '../hooks/useSessionManager'; export default function App() { const { publicKey, sessionAccount, sessionMetadata, openSessionInWebView, executeTransaction, isLoading, errorMessage, reset, } = useSessionManager(); const handleExecuteTransfer = async () => { const ethContract = '0x049d36570d4e46f48e99674bd3fcc84644ddd6b96f7c741b1562b82f9e004dc7'; const recipient = '0x1234...'; const amount = '0x1'; await executeTransaction(ethContract, 'transfer', [recipient, amount, '0x0']); }; return ( {!sessionAccount ? ( ) : ( <> Connected: {sessionMetadata.username} )} ); } ``` ### Full Example Repository For a complete working example including project structure, native module configuration, and build scripts, see the [React Native example on GitHub](https://github.com/cartridge-gg/controller.c/tree/main/examples/react-native). ## Session URL Reference This page documents the URL format and callback metadata for the [browser-based session flow](./overview#browser-based-sessions). ### Session URL Format The session URL follows this structure: ``` https://x.cartridge.gg/session?public_key={public_key}&policies={policies}&rpc_url={rpc_url}&redirect_uri={redirect_uri} ``` Or, when using a preset: ``` https://x.cartridge.gg/session?public_key={public_key}&preset={preset}&rpc_url={rpc_url}&redirect_uri={redirect_uri} ``` | Parameter | Required | Description | | --------------------- | ----------------------------------- | ------------------------------------------------------------------------------------------------------------------------- | | `public_key` | Yes | The public key generated by the native application | | `policies` | Yes (unless `preset` is provided) | JSON-encoded session policies object | | `preset` | Yes (unless `policies` is provided) | Preset name to resolve policies from `@cartridge/presets` | | `rpc_url` | Yes | The RPC URL for the target chain | | `redirect_uri` | No | URL to redirect after session creation | | `redirect_query_name` | No | Query parameter name for the redirect | | `callback_uri` | No | URL to POST session data after creation | | `account` | No | Username to prefill and lock during authentication. Forces logout if user is logged in with a different account | | `expires_at` | No | Unix timestamp (seconds) to override session expiration. When provided, this value is used instead of the duration picker | ### Policy Structure Each policy object defines which contract methods the session key is authorized to call: ```json { "target": "0x049d36570d4e46f48e99674bd3fcc84644ddd6b96f7c741b1562b82f9e004dc7", "method": "transfer" } ``` The `policies` parameter should be a JSON array of these objects. For more details on session policies configuration, see [Sessions](/controller/sessions). ### Session Metadata After successful session creation, the following metadata is available: | Field | Type | Description | | ----------- | ------- | ------------------------------------------- | | `address` | string | The user's Controller contract address | | `ownerGuid` | string | Unique identifier for the account owner | | `expiresAt` | number | Session expiration timestamp (Unix seconds) | | `username` | string? | The user's Cartridge username | | `sessionId` | string? | Unique identifier for this session | | `appId` | string? | The application identifier | | `isRevoked` | boolean | Whether the session has been revoked | #### Additional Fields for Already-Registered Sessions When a session is already registered and authorized, the callback payload includes additional session identifiers: | Field | Type | Description | | --------------------- | ------- | -------------------------------------------------- | | `allowedPoliciesRoot` | string | Root hash of the allowed policies for this session | | `metadataHash` | string | Hash of the session metadata | | `sessionKeyGuid` | string | Unique identifier for the session key | | `guardianKeyGuid` | string | Unique identifier for the guardian key | | `alreadyRegistered` | boolean | Flag indicating this is an existing session | These fields are only included when returning an existing authorized session (when `alreadyRegistered` is `true`). For new session registrations, only the basic metadata fields are returned. Fields marked with `?` are only populated when creating sessions via the subscription API flow (`createFromSubscribe`). When creating sessions directly with the constructor, these fields will be `null`. ## How vRNG Works ### The Onchain Randomness Problem Blockchains are deterministic by design — every node must compute the same result for every transaction. This makes genuine randomness impossible to produce from within a smart contract alone. Common workarounds and their weaknesses: | Approach | Problem | | ------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------- | | Block hash / timestamp | Miners/sequencers can influence or predict these values | | Commit-reveal schemes | Require multiple transactions across multiple blocks, adding latency and cost | | Hash of onchain state | Anyone can compute the same hash and predict the outcome before submitting | | External oracles (e.g. Chainlink VRF) | Randomness arrives in a *separate* transaction, breaking atomicity — your game action resolves in one tx, but the random outcome arrives later | For onchain games, these tradeoffs are unacceptable. A dice roll that takes two transactions and 30 seconds breaks the gameplay loop. A sequencer that can predict outcomes breaks the game's integrity. ### The Core Idea The [Cartridge Paymaster](/services/paymaster) already acts as an offchain executor — it wraps player transactions for gas sponsorship and submits them onchain via Starknet's [SNIP-9](https://github.com/starknet-io/SNIPs/blob/main/SNIPS/snip-9.md) `execute_from_outside` protocol. The vRNG extends this existing role. In addition to sponsoring gas, the paymaster holds a **secret key** and uses it to generate a verifiable random number as part of the same execution flow. The random value and its cryptographic proof are injected into the transaction *before* the player's game action executes, so the game contract can consume verified randomness atomically — no extra transactions, no waiting. This works because the paymaster is already in the transaction path. The vRNG is not a separate oracle service; it's a natural extension of the execution infrastructure that's already there. ### Why Not Just Hash? A hash function like `hash(seed)` is deterministic and publicly computable — anyone with the seed can compute the output. Since seeds are derived from onchain state, a player could predict the outcome before submitting their transaction. A **Verifiable Random Function (VRF)** adds a secret key to the computation: `VRF(secret_key, seed) → (output, proof)`. Only the key holder can compute the output, but anyone can *verify* it was computed correctly using the public key. | | Hash | VRF | | ------------------- | ---------------------------------- | ------------------------------------------- | | **Who can compute** | Anyone with the input | Only the secret key holder | | **Predictable?** | Yes — inputs are public | No — requires the secret key | | **Verifiable?** | Trivially (recompute it) | Yes — via cryptographic proof | | **Manipulable?** | No (but predictable = exploitable) | No — deterministic for a given seed and key | The "verifiable" part is what distinguishes a VRF from simple encryption — the proof ensures the key holder can't lie about what the output should be for a given input. ### Cryptographic Details Cartridge's vRNG uses an **Elliptic Curve VRF (EC-VRF)** built on the Stark curve — the native curve of Starknet, which enables efficient onchain verification via Poseidon hashing. #### Proof Structure The VRF provider generates a proof consisting of: * **gamma** — a point on the Stark curve (the core VRF output) * **c** — a scalar challenge value * **s** — a scalar response value * **sqrt\_ratio\_hint** — an optimization hint for efficient onchain verification The random value is derived from `gamma` via hashing. The `(c, s)` pair constitutes a Schnorr-like proof that `gamma` was correctly computed from the seed and the provider's secret key. #### Seed Construction The seed is computed deterministically using a Poseidon hash of context-specific data: * **`Source::Nonce(address)`**: `poseidon_hash(nonce, address, consumer_contract, chain_id)` * **`Source::Salt(salt)`**: `poseidon_hash(salt, consumer_contract, chain_id)` Including the consumer contract address and chain ID prevents cross-contract and cross-chain replay. The `Nonce` source auto-increments, guaranteeing a unique seed per request without the caller needing to manage entropy. ### Transaction Flow The player signs their game action as normal. The paymaster intercepts it, generates the VRF proof, and wraps everything into a nested SNIP-9 `execute_from_outside_v2` structure: ``` ┌─────────────────────────────────────────────────────────────┐ │ Paymaster │ │ calls execute_from_outside_v2 on VRF Account │ │ │ │ ┌────────────────────────────────────────────────────────┐ │ │ │ VRF Account (outer execution, signed by provider key) │ │ │ │ │ │ │ │ 1. submit_random(seed, proof) │ │ │ │ → verifies proof against stored public key │ │ │ │ → stores random value for this transaction │ │ │ │ │ │ │ │ 2. execute_from_outside_v2 on Player Account │ │ │ │ ┌───────────────────────────────────────────────┐ │ │ │ │ │ Player Account (inner execution, │ │ │ │ │ │ signed by player's passkey) │ │ │ │ │ │ │ │ │ │ │ │ 1. request_random(caller, source) │ │ │ │ │ │ → signals VRF intent │ │ │ │ │ │ │ │ │ │ │ │ 2. game_action() on Game Contract │ │ │ │ │ │ → calls consume_random(source) │ │ │ │ │ │ → reads verified random value │ │ │ │ │ │ → uses it in game logic │ │ │ │ │ └───────────────────────────────────────────────┘ │ │ │ │ │ │ │ │ 3. assert_consumed() │ │ │ │ → ensures random value was used │ │ │ │ → clears storage │ │ │ └────────────────────────────────────────────────────────┘ │ └─────────────────────────────────────────────────────────────┘ ``` #### Step by Step 1. **Player signs their game action** — the player's wallet signs a multicall containing `request_random` + the game call (e.g. `roll_dice`). This is the inner SNIP-9 outside execution. 2. **Paymaster intercepts** — the paymaster sees the `request_random` call, computes the seed from the source parameters, and generates the VRF proof using its secret key. 3. **Paymaster wraps with proof injection** — the paymaster constructs an outer SNIP-9 execution signed by the VRF Account that prepends `submit_random(seed, proof)` before executing the player's calls. 4. **Onchain verification** — `submit_random` verifies the EC-VRF proof against the VRF Account's stored public key. If valid, the derived random value is stored for this transaction. 5. **Game consumes randomness** — when the game contract calls `consume_random(source)`, it reads the verified value from the VRF provider's storage. 6. **Cleanup** — `assert_consumed` ensures the random value was actually used and clears storage, preventing stale values from persisting. #### Why Nested Execution? The nesting serves two purposes: * **Proof injection without player awareness** — the player only signs their game action. The VRF proof is added by the paymaster in the outer layer, invisible to the player. * **Atomic execution** — proof verification and consumption happen in the same transaction. There is no window where the random value exists but hasn't been used, and no second transaction to wait for. ### Security Model #### Current (Phase 0) The security assumption is that the **paymaster has not revealed its VRF secret key** and does not collude with players. Given this assumption: * The provider cannot choose a favorable random value — the VRF is deterministic for a given seed, and the seed is derived from onchain state the provider doesn't control. * Players cannot predict the random value — they don't have the provider's secret key. * Anyone can verify after the fact — the proof and public key are onchain. The main trust assumption is that the provider isn't selectively *withholding* unfavorable results (censorship). #### Future (TEE) The planned migration to a Trusted Execution Environment (TEE) will eliminate the trust assumption on the provider operator. The secret key will be generated and held within the TEE, ensuring that even the operator cannot extract it or selectively withhold results. ## vRNG Overview This Cartridge Verifiable Random Number Generator (vRNG) is designed to provide cheap, atomic verifiable randomness for fully onchain games. :::info The vRNG was previously referred to as the Verifiable Random Function, and most code implementations use this terminology. ::: ### Key Features 1. **Atomic Execution**: The vRNG request and response are processed within the same transaction, ensuring synchronous and immediate randomness for games. 2. **Efficient Onchain Verification**: Utilizes the Stark curve and Poseidon hash for optimized verification on Starknet. 3. **Fully Onchain**: The entire vRNG process occurs onchain, maintaining transparency and verifiability. 4. **Improved Player Experience**: The synchronous nature of the vRNG allows for instant resolution of random events in games, enhancing gameplay fluidity. ### How It Works 1. A game calls `request_random(caller, source)` as the first call in their multicall. 2. A game contract calls `consume_random(source)` on the vRNG contract. 3. The vRNG server generates a random value using the vRNG algorithm for the provided entropy source. 4. The [Cartridge Paymaster](/services/paymaster) wraps the players multicall with a `submit_random` and `assert_consumed` call. 5. The `submit_random` call submit a vRNG Proof for the request, the vRNG Proof is verified onchain, ensuring the integrity of the random value which is immediately available and must be used within the same transaction. 6. The `assert_consumed` call ensures that `consume_random(source)` has been called, it also reset the storage used to store the random value during the transaction to 0. ### Benefits for Game Developers * **Simplicity**: Easy integration with existing Starknet smart contracts and Dojo. * **Performance**: Synchronous randomness generation without waiting for multiple transactions. * **Cost-effectiveness**: Potential cost savings through Paymaster integration. * **Security**: Cryptographically secure randomness that's fully verifiable onchain. #### Deployments | Network | Contract Address | Class Hash | | ------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Mainnet | [0x051fea4450da9d6aee758bdeba88b2f665bcbf549d2c61421aa724e9ac0ced8f](https://voyager.online/contract/0x051fea4450da9d6aee758bdeba88b2f665bcbf549d2c61421aa724e9ac0ced8f) | [0x00be3edf412dd5982aa102524c0b8a0bcee584c5a627ed1db6a7c36922047257](https://voyager.online/class/0x00be3edf412dd5982aa102524c0b8a0bcee584c5a627ed1db6a7c36922047257) | | Sepolia | [0x051fea4450da9d6aee758bdeba88b2f665bcbf549d2c61421aa724e9ac0ced8f](https://sepolia.voyager.online/contract/0x051fea4450da9d6aee758bdeba88b2f665bcbf549d2c61421aa724e9ac0ced8f) | [0x00be3edf412dd5982aa102524c0b8a0bcee584c5a627ed1db6a7c36922047257](https://sepolia.voyager.online/class/0x00be3edf412dd5982aa102524c0b8a0bcee584c5a627ed1db6a7c36922047257) | For detailed implementation and usage, refer to the [GitHub repository](https://github.com/cartridge-gg/vrf). ### Using the vRNG Provider To integrate the Verifiable Random Function (vRNG) into your Starknet contract, follow these steps: 1. Define the vRNG Provider interface: ```rust #[starknet::interface] trait IVrfProvider { fn request_random(self: @TContractState, caller: ContractAddress, source: Source); fn consume_random(ref self: TContractState, source: Source) -> felt252; } #[derive(Drop, Copy, Clone, Serde)] pub enum Source { Nonce: ContractAddress, Salt: felt252, } ``` 2. Define the vRNG Provider address in your contract: ```rust const VRF_PROVIDER_ADDRESS: starknet::ContractAddress = starknet::contract_address_const::<0x123>(); ``` 3. Create a dispatcher for the vRNG Provider: ```rust let vrf_provider = IVrfProviderDispatcher { contract_address: VRF_PROVIDER_ADDRESS }; ``` 4. To consume random values, use the following pattern in your contract functions: ```rust fn roll_dice(ref self: ContractState) { // Your game logic here... // Consume random value let player_id = get_caller_address(); let random_value = vrf_provider.consume_random(Source::Nonce(player_id)); // Use the random value in your game logic // ... } ``` 5. You can use either `Source::Nonce(ContractAddress)` or `Source::Salt(felt252)` as the source for randomness: * `Source::Nonce(ContractAddress)`: Uses the provided contract address internal nonce for randomness. \ Each request will generate a different seed ensuring unique random values. * `Source::Salt(felt252)`: Uses a provided salt value for randomness. \ Two requests with same salts will result in same random value. ### Executing vRNG transactions In order to execute a transaction that includes a `consume_random` call, you need to include a `request_random` transaction as the first transaction in the multicall. The `request_random` call allows our server to efficiently parse transactions that include a `consume_random` call internally. ```javascript const call = await account.execute([ // Prefix the multicall with the request_random call { contractAddress: VRF_PROVIDER_ADDRESS, entrypoint: 'request_random', calldata: CallData.compile({ caller: GAME_CONTRACT, // Using Source::Nonce(address) source: {type: 0, address: account.address}, // Using Source::Salt(felt252) // source: {type: 1, salt: 0x123} }), }, { contractAddress: GAME_CONTRACT, entrypoint: 'roll_dice', ... }, ]); ``` **Ensure that you call `consume_random` with the same `Source` as used in `request_random`.** #### Important: Adding vRNG to Policies When using the Cartridge Controller with vRNG, make sure to add the vRNG contract address and the `request_random` method to your session policies. This allows the controller to pre-approve vRNG-related transactions, ensuring a seamless experience for your users. Add the following policy to your existing session policies: ```typescript const policies: Policy[] = [ // ... your existing policies ... { target: VRF_PROVIDER_ADDRESS, method: "request_random", description: "Allows requesting random numbers from the VRF provider", }, ]; ``` This ensures that vRNG-related transactions can be executed without requiring additional user approval each time. By following these steps, you can integrate the vRNG Provider into your Starknet contract and generate verifiable random numbers for your onchain game or application. ### Security Assumptions During the Phase 0 deployment, the construction assumes the Provider has not revealed the private key and does not collude with players. In the future, we plan to move the Provider to a Trusted Execution Environment (TEE) in order to provide a more robust security model without compromising on performance.