controller-react

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Controller React Integration

Controller React集成

Integrate Cartridge Controller with React using
starknet-react
.
使用
starknet-react
将Cartridge Controller与React集成。

Installation

安装

bash
pnpm add @cartridge/connector @cartridge/controller @starknet-react/core @starknet-react/chains starknet
pnpm add -D vite-plugin-mkcert
bash
pnpm add @cartridge/connector @cartridge/controller @starknet-react/core @starknet-react/chains starknet
pnpm add -D vite-plugin-mkcert

Provider Setup

Provider配置

Important: Create connector outside React components.
typescript
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 });

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" };
    }
  },
});

export function StarknetProvider({ children }: { children: React.ReactNode }) {
  return (
    <StarknetConfig
      autoConnect
      defaultChainId={mainnet.id}
      chains={[mainnet, sepolia]}
      provider={provider}
      connectors={[connector]}
      explorer={cartridge}
    >
      {children}
    </StarknetConfig>
  );
}
重要提示:在React组件外部创建connector。
typescript
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 });

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" };
    }
  },
});

export function StarknetProvider({ children }: { children: React.ReactNode }) {
  return (
    <StarknetConfig
      autoConnect
      defaultChainId={mainnet.id}
      chains={[mainnet, sepolia]}
      provider={provider}
      connectors={[connector]}
      explorer={cartridge}
    >
      {children}
    </StarknetConfig>
  );
}

Connect Wallet Component

钱包连接组件

typescript
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>
  );
}
typescript
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

动态授权按钮

typescript
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>
typescript
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

执行交易

typescript
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>
  );
}
typescript
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

外部钱包方法

typescript
// 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
.
typescript
// 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);
支持的钱包类型:
metamask
,
rabby
,
phantom
,
argent
,
walletconnect

Vite Configuration

Vite配置

Enable HTTPS for local development:
typescript
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";
import mkcert from "vite-plugin-mkcert";

export default defineConfig({
  plugins: [react(), mkcert()],
});
为本地开发启用HTTPS:
typescript
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";
import mkcert from "vite-plugin-mkcert";

export default defineConfig({
  plugins: [react(), mkcert()],
});

Development Modes

开发模式

bash
undefined
bash
undefined

Local development with local APIs

Local development with local APIs

pnpm dev
pnpm dev

Testing with production APIs (hybrid mode)

Testing with production APIs (hybrid mode)

pnpm dev:live

The `dev:live` mode runs keychain locally while connecting to production APIs.
pnpm dev:live

`dev:live`模式会在本地运行keychain,同时连接到生产环境API。

App Structure

应用结构

typescript
import { StarknetProvider } from "./StarknetProvider";
import { ConnectWallet } from "./ConnectWallet";
import { TransferEth } from "./TransferEth";

function App() {
  return (
    <StarknetProvider>
      <ConnectWallet />
      <TransferEth />
    </StarknetProvider>
  );
}
typescript
import { StarknetProvider } from "./StarknetProvider";
import { ConnectWallet } from "./ConnectWallet";
import { TransferEth } from "./TransferEth";

function App() {
  return (
    <StarknetProvider>
      <ConnectWallet />
      <TransferEth />
    </StarknetProvider>
  );
}