controller-backend

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Controller Backend Integration

Controller后端集成

Integrate Controller into server-side applications and automated systems.
Sessions enable pre-approved transactions without manual user approval, making them ideal for automated backends.
将Controller集成到服务器端应用和自动化系统中。
会话支持无需用户手动批准的预授权交易,非常适合自动化后端场景。

Node.js

Node.js

Uses
SessionProvider
with file-based session storage.
结合基于文件的会话存储使用
SessionProvider

Installation

安装

bash
pnpm add @cartridge/controller starknet
bash
pnpm add @cartridge/controller starknet

Setup

设置

typescript
import SessionProvider, {
  ControllerError,
} from "@cartridge/controller/session/node";
import { constants } from "starknet";
import path from "path";

const ETH_CONTRACT =
  "0x049d36570d4e46f48e99674bd3fcc84644ddd6b96f7c741b1562b82f9e004dc7";

async function main() {
  const storagePath =
    process.env.CARTRIDGE_STORAGE_PATH ||
    path.join(process.cwd(), ".cartridge");

  const provider = new SessionProvider({
    rpc: "https://api.cartridge.gg/x/starknet/mainnet",
    chainId: constants.StarknetChainId.SN_MAIN,
    policies: {
      contracts: {
        [ETH_CONTRACT]: {
          methods: [
            {
              name: "approve",
              entrypoint: "approve",
              spender: "0x1234567890abcdef1234567890abcdef12345678",
              amount: "0xffffffffffffffffffffffffffffffff",
              description: "Approve spending of tokens",
            },
            { name: "transfer", entrypoint: "transfer" },
          ],
        },
      },
    },
    basePath: storagePath,
  });

  try {
    const account = await provider.connect();

    if (account) {
      console.log("Account address:", account.address);

      // Example: Transfer ETH
      const amount = "0x0";
      const recipient = account.address; // Replace with actual recipient

      const result = await account.execute([
        {
          contractAddress: ETH_CONTRACT,
          entrypoint: "transfer",
          calldata: [recipient, amount, "0x0"],
        },
      ]);

      console.log("Transaction hash:", result.transaction_hash);
    } else {
      console.log("Please complete the session creation in your browser");
    }
  } catch (error: unknown) {
    const controllerError = error as ControllerError;
    if (controllerError.code) {
      console.error("Session error:", {
        code: controllerError.code,
        message: controllerError.message,
        data: controllerError.data,
      });
    } else {
      console.error("Session error:", error);
    }
  }
}

main().catch(console.error);
typescript
import SessionProvider, {
  ControllerError,
} from "@cartridge/controller/session/node";
import { constants } from "starknet";
import path from "path";

const ETH_CONTRACT =
  "0x049d36570d4e46f48e99674bd3fcc84644ddd6b96f7c741b1562b82f9e004dc7";

async function main() {
  const storagePath =
    process.env.CARTRIDGE_STORAGE_PATH ||
    path.join(process.cwd(), ".cartridge");

  const provider = new SessionProvider({
    rpc: "https://api.cartridge.gg/x/starknet/mainnet",
    chainId: constants.StarknetChainId.SN_MAIN,
    policies: {
      contracts: {
        [ETH_CONTRACT]: {
          methods: [
            {
              name: "approve",
              entrypoint: "approve",
              spender: "0x1234567890abcdef1234567890abcdef12345678",
              amount: "0xffffffffffffffffffffffffffffffff",
              description: "Approve spending of tokens",
            },
            { name: "transfer", entrypoint: "transfer" },
          ],
        },
      },
    },
    basePath: storagePath,
  });

  try {
    const account = await provider.connect();

    if (account) {
      console.log("Account address:", account.address);

      // Example: Transfer ETH
      const amount = "0x0";
      const recipient = account.address; // Replace with actual recipient

      const result = await account.execute([
        {
          contractAddress: ETH_CONTRACT,
          entrypoint: "transfer",
          calldata: [recipient, amount, "0x0"],
        },
      ]);

      console.log("Transaction hash:", result.transaction_hash);
    } else {
      console.log("Please complete the session creation in your browser");
    }
  } catch (error: unknown) {
    const controllerError = error as ControllerError;
    if (controllerError.code) {
      console.error("Session error:", {
        code: controllerError.code,
        message: controllerError.message,
        data: controllerError.data,
      });
    } else {
      console.error("Session error:", error);
    }
  }
}

main().catch(console.error);

Notes

注意事项

  • basePath
    specifies session storage directory (must be writable)
  • First run requires browser-based session creation
  • Session persists between runs
  • basePath
    指定会话存储目录(必须可写)
  • 首次运行需要通过浏览器创建会话
  • 会话在多次运行之间保持持久化

Rust

Rust

Uses
account_sdk
crate for native Rust integration.
使用
account_sdk
crate进行原生Rust集成。

Installation

安装

toml
[dependencies]
account_sdk = { git = "https://github.com/cartridge-gg/controller-rs.git", package = "account_sdk" }
starknet = "0.10"
tokio = { version = "1", features = ["full"] }
toml
[dependencies]
account_sdk = { git = "https://github.com/cartridge-gg/controller-rs.git", package = "account_sdk" }
starknet = "0.10"
tokio = { version = "1", features = ["full"] }

Setup

设置

rust
use account_sdk::{controller::Controller, signers::Signer};
use starknet::{
    accounts::Account,
    providers::Provider,
    signers::SigningKey,
    core::types::FieldElement,
};
use std::env;

#[tokio::main]
async fn main() {
    // Load private key from environment
    let private_key = env::var("PRIVATE_KEY")
        .expect("PRIVATE_KEY must be set");

    let owner = Signer::Starknet(
        SigningKey::from_secret_scalar(
            FieldElement::from_hex_be(&private_key).unwrap()
        )
    );

    let rpc_url = "https://api.cartridge.gg/x/starknet/mainnet";
    let provider = Provider::try_from(rpc_url).unwrap();
    let chain_id = provider.chain_id().await.unwrap();

    let controller = Controller::new(
        "my_app".to_string(),
        "username".to_string(),
        FieldElement::from_hex_be("0xCLASS_HASH").unwrap(),
        rpc_url.parse().unwrap(),
        owner,
        FieldElement::from_hex_be("0xCONTROLLER_ADDRESS").unwrap(),
        chain_id,
    );

    // Deploy if needed
    controller.deploy().await.unwrap();

    // Execute transaction
    let call = starknet::core::types::FunctionCall {
        contract_address: FieldElement::from_hex_be("0xCONTRACT").unwrap(),
        entry_point_selector: starknet::core::utils::get_selector_from_name("transfer").unwrap(),
        calldata: vec![FieldElement::from(123)],
    };

    controller.execute(vec![call], None).await.unwrap();
}
rust
use account_sdk::{controller::Controller, signers::Signer};
use starknet::{
    accounts::Account,
    providers::Provider,
    signers::SigningKey,
    core::types::FieldElement,
};
use std::env;

#[tokio::main]
async fn main() {
    // Load private key from environment
    let private_key = env::var("PRIVATE_KEY")
        .expect("PRIVATE_KEY must be set");

    let owner = Signer::Starknet(
        SigningKey::from_secret_scalar(
            FieldElement::from_hex_be(&private_key).unwrap()
        )
    );

    let rpc_url = "https://api.cartridge.gg/x/starknet/mainnet";
    let provider = Provider::try_from(rpc_url).unwrap();
    let chain_id = provider.chain_id().await.unwrap();

    let controller = Controller::new(
        "my_app".to_string(),
        "username".to_string(),
        FieldElement::from_hex_be("0xCLASS_HASH").unwrap(),
        rpc_url.parse().unwrap(),
        owner,
        FieldElement::from_hex_be("0xCONTROLLER_ADDRESS").unwrap(),
        chain_id,
    );

    // Deploy if needed
    controller.deploy().await.unwrap();

    // Execute transaction
    let call = starknet::core::types::FunctionCall {
        contract_address: FieldElement::from_hex_be("0xCONTRACT").unwrap(),
        entry_point_selector: starknet::core::utils::get_selector_from_name("transfer").unwrap(),
        calldata: vec![FieldElement::from(123)],
    };

    controller.execute(vec![call], None).await.unwrap();
}

Headless Controller

无头Controller

For server-side execution with custom signing keys.
适用于使用自定义签名密钥的服务器端执行场景。

Use Cases

适用场景

  • Single owner, multiple accounts
  • Automated game backends (bots, NPCs)
  • Custom key management requirements
  • 单所有者多账户
  • 自动化游戏后端(机器人、NPC)
  • 自定义密钥管理需求

C++ Example

C++示例

Warning: Never commit private keys. Use environment variables or secret managers.
cpp
#include "controller.hpp"
#include <cstdlib>

int main() {
    // Load from environment - NEVER hardcode!
    const char* pk = std::getenv("PRIVATE_KEY");
    if (!pk) {
        std::cerr << "PRIVATE_KEY not set" << std::endl;
        return 1;
    }
    std::string private_key(pk);

    auto owner = controller::Owner::init(private_key);

    auto ctrl = controller::Controller::new_headless(
        "my_app",
        "bot_account",
        controller::get_controller_class_hash(controller::Version::kLatest),
        "https://api.cartridge.gg/x/starknet/mainnet",
        owner,
        "0x534e5f4d41494e"  // SN_MAIN
    );

    // Register new account onchain
    ctrl->signup(
        controller::SignerType::kStarknet,
        std::nullopt,
        std::nullopt
    );

    // Execute transaction
    controller::Call call{
        "0x049d36570d4e46f48e99674bd3fcc84644ddd6b96f7c741b1562b82f9e004dc7",
        "transfer",
        {"0xRECIPIENT", "0x100", "0x0"}
    };

    std::string tx_hash = ctrl->execute({call});
    std::cout << "Transaction: " << tx_hash << std::endl;

    return 0;
}
警告:绝对不要提交私钥。请使用环境变量或密钥管理器。
cpp
#include "controller.hpp"
#include <cstdlib>

int main() {
    // Load from environment - NEVER hardcode!
    const char* pk = std::getenv("PRIVATE_KEY");
    if (!pk) {
        std::cerr << "PRIVATE_KEY not set" << std::endl;
        return 1;
    }
    std::string private_key(pk);

    auto owner = controller::Owner::init(private_key);

    auto ctrl = controller::Controller::new_headless(
        "my_app",
        "bot_account",
        controller::get_controller_class_hash(controller::Version::kLatest),
        "https://api.cartridge.gg/x/starknet/mainnet",
        owner,
        "0x534e5f4d41494e"  // SN_MAIN
    );

    // Register new account onchain
    ctrl->signup(
        controller::SignerType::kStarknet,
        std::nullopt,
        std::nullopt
    );

    // Execute transaction
    controller::Call call{
        "0x049d36570d4e46f48e99674bd3fcc84644ddd6b96f7c741b1562b82f9e004dc7",
        "transfer",
        {"0xRECIPIENT", "0x100", "0x0"}
    };

    std::string tx_hash = ctrl->execute({call});
    std::cout << "Transaction: " << tx_hash << std::endl;

    return 0;
}

Security Best Practices

安全最佳实践

Never commit private keys. Use:
  • Environment variables
  • Secret managers (AWS Secrets Manager, HashiCorp Vault)
  • Hardware security modules (HSM)
typescript
// Good
const privateKey = process.env.PRIVATE_KEY;

// Bad - NEVER do this
const privateKey = "0x1234...";
绝对不要提交私钥。请使用:
  • 环境变量
  • 密钥管理器(AWS Secrets Manager、HashiCorp Vault)
  • 硬件安全模块(HSM)
typescript
// 正确做法
const privateKey = process.env.PRIVATE_KEY;

// 错误做法 - 绝对不要这样做
const privateKey = "0x1234...";

Telegram Mini-Apps

Telegram迷你应用

Use
SessionConnector
with Telegram WebApp context.
结合Telegram WebApp上下文使用
SessionConnector

Integration Flow

集成流程

  1. Configure session policies
  2. Set up SessionProvider with cloud storage
  3. Handle authentication redirects
  4. Execute transactions via session
  1. 配置会话策略
  2. 设置带有云存储的SessionProvider
  3. 处理认证重定向
  4. 通过会话执行交易

Setup

设置

typescript
import { SessionConnector } from "@cartridge/connector";
import { constants } from "starknet";

const policies = {
  contracts: {
    "0x...": {
      methods: [{ name: "action", entrypoint: "action" }],
    },
  },
};

const connector = new SessionConnector({
  policies,
  rpc: "https://api.cartridge.gg/x/starknet/mainnet",
  chainId: constants.StarknetChainId.SN_MAIN,
  redirectUrl: window.Telegram?.WebApp?.initData
    ? "https://t.me/MyBot/app"
    : "https://myapp.com/callback",
});
typescript
import { SessionConnector } from "@cartridge/connector";
import { constants } from "starknet";

const policies = {
  contracts: {
    "0x...": {
      methods: [{ name: "action", entrypoint: "action" }],
    },
  },
};

const connector = new SessionConnector({
  policies,
  rpc: "https://api.cartridge.gg/x/starknet/mainnet",
  chainId: constants.StarknetChainId.SN_MAIN,
  redirectUrl: window.Telegram?.WebApp?.initData
    ? "https://t.me/MyBot/app"
    : "https://myapp.com/callback",
});

Next.js WebAssembly Configuration

Next.js WebAssembly配置

For Telegram mini-apps using Next.js:
typescript
// next.config.js
module.exports = {
  webpack: (config) => {
    config.experiments = {
      ...config.experiments,
      asyncWebAssembly: true,
    };
    return config;
  },
};
适用于使用Next.js的Telegram迷你应用:
typescript
// next.config.js
module.exports = {
  webpack: (config) => {
    config.experiments = {
      ...config.experiments,
      asyncWebAssembly: true,
    };
    return config;
  },
};