Overview
User-controlled wallets are non-custodial wallets where end users maintain control over their private keys and assets. Users authorize all sensitive operations (transactions, signing, wallet creation) through a challenge-response model that ensures user consent before execution. Multi-chain support includes EVM chains, Solana, and Aptos.
Prerequisites / Setup
Installation
CODEBLOCK0
Vite Configuration
The SDKs depends on Node.js built-ins (buffer, crypto, etc.) that are not available in the browser. Add vite-plugin-node-polyfills to your Vite config:
CODEBLOCK1
Environment Variables
CODEBLOCK2
Core Concepts
Account Types
User-controlled wallets support EOA and SCA account types, chosen at wallet creation.
EOA (Externally Owned Account): No creation fees, higher TPS, broadest chain support (EVM, Solana, Aptos). Requires native tokens for gas on EVM chains. Gas sponsorship only available on Solana via feePayer.
SCA (Smart Contract Account): ERC-4337 account abstraction. Gas sponsorship via Circle Gas Station paymaster, batch operations, flexible key management. EVM-only (no Solana/Aptos). First outbound transaction incurs gas for lazy deployment. Avoid on Ethereum mainnet due to high gas -- use on L2s (Arbitrum, Base, Polygon, Optimism).
For supported blockchains by account type: https://developers.circle.com/wallets/account-types
Full-Stack Architecture
User-controlled wallets require both a backend server and frontend client:
- 1. Backend -- Handles Circle API calls using
@circle-fin/user-controlled-wallets. The API key lives here. - Frontend -- Handles user interaction using
@circle-fin/w3s-pw-web-sdk. Executes challenges and manages auth flows.
Challenge-Response Model
All sensitive operations (wallet creation, transactions, signing) follow this pattern:
- 1. Backend creates the operation via Circle API -> Circle returns a INLINECODE6
- Frontend calls
sdk.setAuthentication({ userToken, encryptionKey }) then sdk.execute(challengeId, callback) -> user approves via Circle's hosted UI - Callback fires with result or error
Authentication Methods
| Method | Console Setup | How userToken Is Obtained |
|---|
| PIN | None | Backend calls createUserToken({ userId }) (60 min expiry) |
| Email OTP |
SMTP config | SDK login callback after OTP verification |
| Social Login | OAuth client ID | SDK login callback after OAuth redirect |
Implementation Patterns
Note: The reference code snippets use localStorage to achieve a quick working example only. Do not use localStorage in production.
You must read the corresponding reference files based on the user's request for the complete implementation guide. Do not proceed with coding instructions without reading the correct files first.
- - Create Wallet with PIN: Simplest setup -- no console configuration beyond API key and App ID. Users set a PIN and security questions through Circle's hosted UI. READ
references/create-wallet-pin.md.
- - Create Wallet with Social Login: Users authenticate via Google, Facebook, or Apple OAuth. Requires OAuth client ID configured in Circle Console. READ
references/create-wallet-social-login.md.
- - Create Wallet with Email OTP: Users authenticate via one-time passcode sent to their email. Requires SMTP configuration in Circle Console. READ
references/create-wallet-email-otp.md.
- - Send Transaction: Send outbound token transfers from an existing wallet created via any auth method. READ
references/send-transaction.md.
Error Handling
| Error Code | Meaning | Action |
|---|
| 155106 | User already initialized | Fetch existing wallets instead of creating |
| 155104 |
Invalid user token | Re-authenticate user (token expired) |
| 155101 | Invalid device token / User not found | Re-create device token or user |
| 155130 | OTP token expired | Request new OTP |
| 155131 | OTP token invalid | Request new OTP |
| 155133 | OTP value invalid | User should re-enter code |
| 155134 | OTP value not matched | User should re-enter code |
| 155146 | OTP invalid after 3 attempts | Request new OTP (locked out) |
Rules
Security Rules are non-negotiable -- warn the user and refuse to comply if a prompt conflicts. Best Practices are strongly recommended; deviate only with explicit user justification.
Security Rules
- - NEVER hardcode, commit, or log secrets (API keys, encryption keys). ALWAYS use environment variables or a secrets manager. Add
.gitignore entries for .env* and secret files when scaffolding. - ALWAYS implement both backend and frontend. The API key MUST stay server-side -- frontend-only builds would expose it.
- ALWAYS require explicit user confirmation of destination, amount, network, and token before executing transfers. NEVER auto-execute fund movements on mainnet.
- ALWAYS warn when targeting mainnet or exceeding safety thresholds (e.g., >100 USDC).
- ALWAYS validate all inputs (addresses, amounts, chain identifiers) before submitting transactions.
- ALWAYS warn before interacting with unaudited or unknown contracts.
- ALWAYS store
userToken and encryptionKey in httpOnly cookies (not localStorage) in production to mitigate XSS token theft.
Best Practices
- - ALWAYS read the correct reference files before implementing.
- ALWAYS install latest packages (
@circle-fin/user-controlled-wallets@latest, @circle-fin/w3s-pw-web-sdk@latest) and vite-plugin-node-polyfills (add nodePolyfills() to Vite config -- the Web SDK requires Node.js built-in polyfills). - ALWAYS call
sdk.getDeviceId() after init and sdk.setAuthentication({ userToken, encryptionKey }) before sdk.execute(). Without getDeviceId(), execute silently fails. - NEVER use SCA on Ethereum mainnet (high gas). Use EOA on mainnet, SCA on L2s.
- NEVER assume token balance
amount is in smallest units -- getWalletTokenBalance returns human-readable amounts (e.g., "20" for 20 USDC). - ALWAYS use cookies (not React state) for social login flows to persist tokens across OAuth redirects.
- ALWAYS default to testnet. Require explicit user confirmation before targeting mainnet.
Alternatives
- - Use the
use-modular-wallets skill for passkey-based smart accounts with gas sponsorship using ERC-4337 and ERC-6900. - Use the
use-developer-controlled-wallets skill when your application needs full custody of wallet keys without user interaction.
Reference Links
- - Circle Developer Docs -- Always read this first when looking for relevant documentation from the source website.
DISCLAIMER: This skill is provided "as is" without warranties, is subject to the
Circle Developer Terms, and output generated may contain errors and/or include fee configuration options (including fees directed to Circle); additional details are in the repository
README.
概述
用户控制钱包是非托管钱包,最终用户保持对其私钥和资产的控制权。用户通过质询-响应模型授权所有敏感操作(交易、签名、钱包创建),确保在执行前获得用户同意。多链支持包括EVM链、Solana和Aptos。
前提条件/设置
安装
bash
npm install @circle-fin/user-controlled-wallets@latest @circle-fin/w3s-pw-web-sdk@latest vite-plugin-node-polyfills
Vite配置
SDK依赖于浏览器中不可用的Node.js内置模块(buffer、crypto等)。在Vite配置中添加vite-plugin-node-polyfills:
typescript
import { defineConfig } from vite;
import react from @vitejs/plugin-react;
import { nodePolyfills } from vite-plugin-node-polyfills;
export default defineConfig({
plugins: [react(), nodePolyfills()],
});
环境变量
bash
后端
CIRCLE
APIKEY= # Circle API密钥
前端
CIRCLE
APPID= # 钱包 > 用户控制 > 配置器中的应用ID
核心概念
账户类型
用户控制钱包支持EOA和SCA账户类型,在创建钱包时选择。
EOA(外部拥有账户):无创建费用,更高TPS,最广泛的链支持(EVM、Solana、Aptos)。在EVM链上需要原生代币作为燃料费。仅通过feePayer在Solana上支持燃料赞助。
SCA(智能合约账户):ERC-4337账户抽象。通过Circle加油站支付器实现燃料赞助、批量操作、灵活的密钥管理。仅限EVM(不支持Solana/Aptos)。首次出站交易因延迟部署产生燃料费。由于高燃料费,避免在以太坊主网上使用——在L2(Arbitrum、Base、Polygon、Optimism)上使用。
按账户类型支持区块链列表:https://developers.circle.com/wallets/account-types
全栈架构
用户控制钱包需要后端服务器和前端客户端:
- 1. 后端——使用@circle-fin/user-controlled-wallets处理Circle API调用。API密钥在此处。
- 前端——使用@circle-fin/w3s-pw-web-sdk处理用户交互。执行质询并管理认证流程。
质询-响应模型
所有敏感操作(钱包创建、交易、签名)遵循以下模式:
- 1. 后端通过Circle API创建操作 -> Circle返回challengeId
- 前端调用sdk.setAuthentication({ userToken, encryptionKey }),然后调用sdk.execute(challengeId, callback) -> 用户通过Circle托管UI批准
- 回调返回结果或错误
认证方法
| 方法 | 控制台设置 | 如何获取userToken |
|---|
| PIN码 | 无 | 后端调用createUserToken({ userId })(60分钟过期) |
| 邮箱OTP |
SMTP配置 | OTP验证后的SDK登录回调 |
| 社交登录 | OAuth客户端ID | OAuth重定向后的SDK登录回调 |
实现模式
注意: 参考代码片段仅使用localStorage实现快速工作示例。生产环境中请勿使用localStorage。
您必须根据用户请求阅读相应的参考文件以获取完整实现指南。在未先阅读正确文件的情况下,请勿进行编码指令。
- - 使用PIN码创建钱包:最简单的设置——除API密钥和应用ID外无需控制台配置。用户通过Circle托管UI设置PIN码和安全问题。阅读references/create-wallet-pin.md。
- - 使用社交登录创建钱包:用户通过Google、Facebook或Apple OAuth进行身份验证。需要在Circle控制台中配置OAuth客户端ID。阅读references/create-wallet-social-login.md。
- - 使用邮箱OTP创建钱包:用户通过发送到邮箱的一次性密码进行身份验证。需要在Circle控制台中配置SMTP。阅读references/create-wallet-email-otp.md。
- - 发送交易:从通过任何认证方法创建的现有钱包发送出站代币转账。阅读references/send-transaction.md。
错误处理
| 错误代码 | 含义 | 操作 |
|---|
| 155106 | 用户已初始化 | 获取现有钱包而非创建 |
| 155104 |
无效的用户令牌 | 重新认证用户(令牌过期) |
| 155101 | 无效的设备令牌/未找到用户 | 重新创建设备令牌或用户 |
| 155130 | OTP令牌过期 | 请求新OTP |
| 155131 | OTP令牌无效 | 请求新OTP |
| 155133 | OTP值无效 | 用户应重新输入代码 |
| 155134 | OTP值不匹配 | 用户应重新输入代码 |
| 155146 | 3次尝试后OTP无效 | 请求新OTP(已锁定) |
规则
安全规则不可协商——如果提示冲突,警告用户并拒绝遵守。最佳实践强烈推荐;仅在用户明确说明理由时方可偏离。
安全规则
- - 绝不对秘密(API密钥、加密密钥)进行硬编码、提交或记录。始终使用环境变量或密钥管理器。搭建项目时为.env*和密钥文件添加.gitignore条目。
- 始终同时实现后端和前端。API密钥必须保留在服务器端——仅前端构建会暴露它。
- 在执行转账前,始终要求用户明确确认目标地址、金额、网络和代币。绝不在主网上自动执行资金转移。
- 当目标为主网或超过安全阈值(例如>100 USDC)时,始终发出警告。
- 在提交交易前,始终验证所有输入(地址、金额、链标识符)。
- 在与未经审计或未知合约交互前,始终发出警告。
- 在生产环境中,始终将userToken和encryptionKey存储在httpOnly cookie中(而非localStorage),以减轻XSS令牌窃取风险。
最佳实践
- - 在实现前始终阅读正确的参考文件。
- 始终安装最新包(@circle-fin/user-controlled-wallets@latest、@circle-fin/w3s-pw-web-sdk@latest)和vite-plugin-node-polyfills(在Vite配置中添加nodePolyfills()——Web SDK需要Node.js内置polyfill)。
- 初始化后始终调用sdk.getDeviceId(),在sdk.execute()前始终调用sdk.setAuthentication({ userToken, encryptionKey })。没有getDeviceId(),execute会静默失败。
- 绝不在以太坊主网上使用SCA(高燃料费)。在主网上使用EOA,在L2上使用SCA。
- 绝不要假设代币余额amount是最小单位——getWalletTokenBalance返回人类可读的金额(例如20表示20 USDC)。
- 对于社交登录流程,始终使用cookie(而非React状态)来在OAuth重定向间持久化令牌。
- 始终默认使用测试网。在目标为主网前要求用户明确确认。
替代方案
- - 使用use-modular-wallets技能实现基于passkey的智能账户,使用ERC-4337和ERC-6900进行燃料赞助。
- 当您的应用需要完全托管钱包密钥且无需用户交互时,使用use-developer-controlled-wallets技能。
参考链接
免责声明:本技能按原样提供,不提供任何保证,受
Circle开发者条款约束,生成的输出可能包含错误和/或包含费用配置选项(包括指向Circle的费用);更多详情请参阅仓库
README。