skills/controller-react/SKILL.md
Integrate Cartridge Controller into React applications using starknet-react. Use when building React/Next.js web apps with Controller, setting up StarknetConfig provider, using hooks like useConnect/useAccount, or implementing wallet connection components. Covers ControllerConnector setup, provider configuration, and transaction execution patterns.
npx skillsauth add cartridge-gg/docs controller-reactInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Integrate Cartridge Controller with React using starknet-react.
pnpm add @cartridge/connector @cartridge/controller @starknet-react/core @starknet-react/chains starknet
pnpm add -D vite-plugin-mkcert
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>
);
}
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>
);
}
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>
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>
);
}
// 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.
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()],
});
# 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.
import { StarknetProvider } from "./StarknetProvider";
import { ConnectWallet } from "./ConnectWallet";
import { TransferEth } from "./TransferEth";
function App() {
return (
<StarknetProvider>
<ConnectWallet />
<TransferEth />
</StarknetProvider>
);
}
development
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.
data-ai
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.
testing
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.
development
Integrate Cartridge Controller into native mobile applications (iOS, Android, React Native) and web wrappers (Capacitor). Use when building mobile games or apps that need Controller wallet functionality with local keypair signing. Covers SessionConnector for native auth flows, Controller.c FFI bindings, passkey configuration with Apple App Site Association, and platform-specific setup.