controller-react
Controller React Integration
Integrate Cartridge Controller with React using starknet-react.
Installation
pnpm add @cartridge/connector @cartridge/controller @starknet-react/core @starknet-react/chains starknet
pnpm add -D vite-plugin-mkcert
Provider Setup
Important: Create connector outside React components.
import { sepolia, mainnet, Chain } from "@starknet-react/chains";
import { StarknetConfig, jsonRpcProvider, cartridge } from "@starknet-react/core";
import { ControllerConnector } from "@cartridge/connector";
import { SessionPolicies } from "@cartridge/controller";
// Define contract addresses
const ETH_TOKEN_ADDRESS =
"0x049d36570d4e46f48e99674bd3fcc84644ddd6b96f7c741b1562b82f9e004dc7";
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" },
],
},
},
};
// Create OUTSIDE component
const connector = new ControllerConnector({ policies });
// Katana chain definition for local development
// Requires katana.toml with [cartridge] paymaster = true
const KATANA_CHAIN_ID = "0x4b4154414e41"; // "KATANA" hex-encoded ASCII
const KATANA_URL = "http://localhost:5050";
const katana: Chain = {
id: BigInt(KATANA_CHAIN_ID),
name: "Katana",
network: "katana",
testnet: true,
nativeCurrency: {
address: "0x04718f5a0fc34cc1af16a1cdee98ffb20c31f5cd61d6ab07201858f4287c938d",
name: "Stark",
symbol: "STRK",
decimals: 18,
},
rpcUrls: {
default: { http: [KATANA_URL] },
public: { http: [KATANA_URL] },
},
// Required for Controller account auto-deployment on Katana
paymasterRpcUrls: {
avnu: { http: [KATANA_URL] },
},
};
const provider = jsonRpcProvider({
rpc: (chain: Chain) => {
switch (chain) {
case mainnet:
return { nodeUrl: "https://api.cartridge.gg/x/starknet/mainnet" };
case sepolia:
return { nodeUrl: "https://api.cartridge.gg/x/starknet/sepolia" };
default:
return { nodeUrl: KATANA_URL };
}
},
});
export function StarknetProvider({ children }: { children: React.ReactNode }) {
return (
<StarknetConfig
autoConnect
defaultChainId={katana.id}
chains={[katana, mainnet, sepolia]}
provider={provider}
connectors={[connector]}
explorer={cartridge}
>
{children}
</StarknetConfig>
);
}
Connect Wallet Component
import { useAccount, useConnect, useDisconnect } from "@starknet-react/core";
import { ControllerConnector } from "@cartridge/connector";
import { useEffect, useState } from "react";
export function ConnectWallet() {
const { connect, connectors } = useConnect();
const { disconnect } = useDisconnect();
const { address } = useAccount();
const controller = connectors[0] as ControllerConnector;
const [username, setUsername] = useState<string>();
useEffect(() => {
if (!address) return;
controller.username()?.then(setUsername);
}, [address, controller]);
if (address) {
return (
<div>
<p>Account: {address}</p>
{username && <p>Username: {username}</p>}
<button onClick={() => disconnect()}>Disconnect</button>
</div>
);
}
return (
<div>
<button onClick={() => connect({ connector: controller })}>
Connect
</button>
</div>
);
}
Dynamic Auth Buttons
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);
}
};
<button onClick={() => handleSpecificAuth(["phantom-evm"])}>
Continue with Phantom
</button>
<button onClick={() => handleSpecificAuth(["google"])}>
Continue with Google
</button>
Execute Transactions
import { useAccount, useExplorer } from "@starknet-react/core";
import { useCallback, useState } from "react";
const ETH = "0x049d36570d4e46f48e99674bd3fcc84644ddd6b96f7c741b1562b82f9e004dc7";
export function TransferEth() {
const [submitted, setSubmitted] = useState<boolean>(false);
const { account } = useAccount();
const explorer = useExplorer();
const [txnHash, setTxnHash] = useState<string>();
const execute = useCallback(
async (amount: string) => {
if (!account) return;
setSubmitted(true);
setTxnHash(undefined);
try {
const result = await account.execute([
{
contractAddress: ETH,
entrypoint: "approve",
calldata: [account.address, amount, "0x0"],
},
{
contractAddress: ETH,
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 (
<div>
<button onClick={() => execute("0x1C6BF52634000")} disabled={submitted}>
Transfer 0.005 ETH
</button>
{txnHash && (
<a href={explorer.transaction(txnHash)} target="_blank" rel="noreferrer">
View Transaction
</a>
)}
</div>
);
}
External Wallet Methods
// Wait for transaction confirmation
const response = await controller.externalWaitForTransaction(
"metamask",
txHash,
30000 // timeout ms
);
if (response.success) {
console.log("Receipt:", response.result);
} else {
console.error("Error:", response.error);
}
// Switch chains
const success = await controller.externalSwitchChain("metamask", chainId);
Supported wallet types: metamask, rabby, phantom, argent, walletconnect.
Vite Configuration
Enable HTTPS for local development:
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";
import mkcert from "vite-plugin-mkcert";
export default defineConfig({
plugins: [react(), mkcert()],
});
Development Modes
# Local development with local APIs
pnpm dev
# Testing with production APIs (hybrid mode)
pnpm dev:live
The dev:live mode runs keychain locally while connecting to production APIs.
App Structure
import { StarknetProvider } from "./StarknetProvider";
import { ConnectWallet } from "./ConnectWallet";
import { TransferEth } from "./TransferEth";
function App() {
return (
<StarknetProvider>
<ConnectWallet />
<TransferEth />
</StarknetProvider>
);
}
More from cartridge-gg/docs
controller-setup
Integrate Cartridge Controller wallet into Starknet applications. Use when setting up Controller for the first time, installing packages, configuring chains/RPC endpoints, or troubleshooting basic integration issues. Covers installation, Controller instantiation, ControllerConnector vs SessionConnector choice, chain configuration, and package compatibility.
69controller-sessions
Configure session keys and policies for Cartridge Controller to enable gasless, pre-approved transactions. Use when defining contract interaction policies, setting spending limits, configuring signed message policies, or implementing error handling for session-based transactions. Covers SessionPolicies type, policy definitions, verified sessions, and error display modes.
68controller-signers
Configure authentication methods for Cartridge Controller including passkeys, social login, and external wallets. Use when implementing user authentication, adding multiple signers for account recovery, customizing signup options, or integrating external wallets like MetaMask or Phantom. Covers WebAuthn passkeys, Google/Discord/Twitter OAuth, wallet connections, and dynamic authentication flows.
65controller-backend
Integrate Cartridge Controller into backend services using Node.js, Rust, or headless mode. Use when building server-side applications, game backends, automated bots, or any non-browser environment that needs to execute Starknet transactions. Covers SessionProvider for Node.js, Rust SDK setup, and headless Controller with custom signing keys.
61slot-vrng
Integrate Cartridge's verifiable random number generator (vRNG) into onchain games.
59slot-rpc
Configure Cartridge RPC endpoints with API token authentication and CORS whitelisting.
58