使用 create fuels 从零搭建全栈 Fuel dApp(Counter 合约实战指南)
本指南基于 fuels-ts 官方文档「Creating a Fuel dApp」整理,讲解如何通过 npm create fuels 脚手架一条龙生成包含 Sway 合约、类型生成与前端界面的全栈 Fuel dApp,并在生成的 Counter 计数器应用上逐步加入 decrement(递减)功能。读完本文,你将掌握 Fuel dApp 的标准目录结构、fuels.config.ts 配置、fuels dev 本地热更新工作流,以及从 Sway 合约、TypeScript 集成测试到 Playwright UI 测试的完整验证方法。
认识 npm create fuels:全栈脚手架工具
npm create fuels 是 Fuel 官方提供的 CLI 脚手架工具,用于快速生成一个可运行的全栈 Fuel dApp。所谓「全栈」是指它一次性为你准备好了三层内容:
- Sway 智能合约:默认内置一个只支持递增(increment)的简单 Counter 合约;
- 类型安全前端:基于 Vite 与 React 的应用,配合 @fuels/react 等钱包连接组件,直接读取已部署合约的 ABI 类型;
- 本地开发链路:
fuelsCLI 负责编译 Sway 程序并持续部署到本地 Fuel 节点,前端改动与合约改动可以并行热更新。
本仓库中用于承载该示例的完整源码位于 apps/create-fuels-counter-guide,文档原始说明见 index.md。
初始化项目
初始化项目只需执行一条命令(按你的包管理器选择其一),工具会自动拉取对应版本的脚手架模板:
npm create fuels@latest
pnpm create fuels@latest
bun create fuels@latest
版本说明:官方文档中以
fuels@{{fuels}}形式注入当前发布版本,实际使用时可省略@latest让工具解析最新版本,也可以显式固定到你需要的版本号。
命令运行后,CLI 会通过交互式提示询问项目名称,例如输入 my-fuel-project:
◇ What is the name of your project?
│ my-fuel-project
└
随后脚手架会自动创建项目目录并安装依赖,完成后你会看到如下成功提示:
⚡️ Success! Created a fullstack Fuel dapp at my-fuel-project
To get started:
- cd into the project directory: cd my-fuel-project
- Start a local Fuel dev server: pnpm fuels:dev
- Run the frontend: pnpm dev
理解生成的目录结构
create fuels 生成的是一个标准的 Vite 工程,外加若干与 Fuel 相关的目录和文件,其结构大致如下:
my-fuel-project
├── src
│ ├── components
│ │ └── ...
│ ├── hooks
│ │ └── ...
│ ├── lib.tsx
│ ├── App.tsx
│ └── ...
├── sway-programs
│ ├── contract
│ │ └── ...
│ └── ...
├── public
│ └── ...
├── fuels.config.ts
├── package.json
└── ...
仓库中的真实示例 apps/create-fuels-counter-guide 严格遵循该结构,下面逐个讲解其中关键文件的作用。
fuels.config.ts:连接 Sway 与 TS 的枢纽
这是 fuels CLI 的配置文件,也是驱动整个项目「持续编译 + 持续部署」的核心。示例项目的配置内容如下(见 fuels.config.ts):
import { createConfig } from 'fuels';
import dotenv from 'dotenv';
import { providerUrl } from './src/lib';
dotenv.config({
path: ['.env.local', '.env'],
});
// If your node is running on a port other than 4000, you can set it here
const fuelCorePort = +(process.env.VITE_FUEL_NODE_PORT as string) || 4000;
export default createConfig({
workspace: './sway-programs', // Path to your Sway workspace
output: './src/sway-api', // Where your generated types will be saved
fuelCorePort,
providerUrl,
forcPath: 'fuels-forc',
fuelCorePath: 'fuels-core',
});
各配置项含义与实现要点:
workspace:Sway workspace 路径,指向sway-programs目录,其中的 contract / predicate / script 子目录都会被自动发现;output:TypeScript 类型与 ABI 文件的输出目录。示例输出到./src/sway-api,里面会自动生成contract-ids.json(已部署合约 ID)等产物;providerUrl:Provider 连接地址,前端通过它连接本地节点或测试网节点。providerUrl的值在 lib.tsx 中根据环境变量计算,本地默认是http://127.0.0.1:4000/v1/graphql,测试网为https://testnet.fuel.network/v1/graphql;fuelCorePort:本地 Fuel 节点端口,默认 4000,可通过VITE_FUEL_NODE_PORT环境变量覆盖;forcPath/fuelCorePath:指向脚手架随附的fuels-forc、fuels-core工具,避免要求开发者手动安装 Sway 工具链。
关于该配置文件的更多参数说明,可查阅 fuels CLI 配置文档。
sway-programs/contract/src/main.sw:智能合约
Counter 合约的源码位于 sway-programs/contract/src/main.sw。开箱即用它只支持递增,下面的实战部分我们会为其加上递减功能。仓库中的完整实现见 main.sw。
src/App.tsx 与 src/components/Contract.tsx:前端
- src/App.tsx 是前端入口组件,负责钱包连接状态判断与各标签页(Wallet / Contract / Predicate / Script / Faucet)的切换;
- src/components/Contract.tsx 实现「Contract」标签页,内部封装了合约实例的创建与所有合约方法调用逻辑,是后面改造的重点文件。
搭建开发环境:启动本地节点与前端
项目脚手架完成后,先启动 Fuel Dev 服务器。该命令会同时做两件事:拉起一个本地 Fuel 节点,并把 sway-programs 下的 Sway 程序持续编译、部署到该节点:
pnpm fuels:dev
npm run fuels:dev
bun run fuels:dev
脚本对应关系见 package.json:
fuels:dev实际执行fuels dev,fuels:build执行fuels build。
服务器就绪后,在另一个终端启动前端开发服务器:
pnpm dev
npm run dev
bun run dev
现在打开 http://localhost:5173,即可看到 dApp 界面。点击「Connect Wallet」连接钱包——若不想连接真实钱包,可以直接在连接器列表中选择 Burner Wallet(Fuel 生态提供的临时/一次性钱包,便于本地开发测试)。
连接钱包后,可以尝试修改 ./sway-programs/contract/src/main.sw 中的合约代码并保存,改动会实时反映在 UI 的「Contract」标签页中,无需重启前端服务器——这正是 fuels dev 提供的全栈开发体验:左侧前端页面与右侧节点/编译日志分屏运行,合约改动即时编译部署、前端类型即时重新生成。
提示:如果需要构建使用 Predicate(谓词)的 Fuel dApp,可参考 Working with Predicates 指南。
实战:为 Counter 添加 Decrement 功能
我们的目标是让计数器既能递增也能递减,这需要在两处协作修改:Sway 合约(新增 decrement_counter 函数)与前端(新增触发该函数的按钮)。
第 1 步:修改 Sway 合约
Sway 合约位于 ./sway-programs/contract/src/main.sw。给 Sway 程序新增函数分两个阶段:先声明 ABI,再实现函数。
首先在文件顶部的 ABI 区新增函数签名。ABI 定义了合约的对外接口蓝图,同时通过属性注解声明存储访问权限:
// The abi defines the blueprint for the contract.
abi Counter {
#[storage(read)]
fn get_count() -> u64;
#[storage(write, read)]
fn increment_counter(amount: u64) -> u64;
#[storage(write, read)]
fn decrement_counter(amount: u64) -> u64;
}
然后,在 increment_counter 实现的下方补上 decrement_counter 的函数体实现(完整上下文见 main.sw)。合约通过 storage { counter: u64 = 0 } 声明存储变量,#[storage(write, read)] 注解表示该函数需要读写存储:
impl Counter for Contract {
// The `get_count` function returns the current value of the counter.
#[storage(read)]
fn get_count() -> u64 {
storage.counter.read()
}
// The `increment_counter` function increments the counter by the given amount.
#[storage(write, read)]
fn increment_counter(amount: u64) -> u64 {
let current = storage.counter.read();
storage.counter.write(current + amount);
storage.counter.read()
}
#[storage(write, read)]
fn decrement_counter(amount: u64) -> u64 {
let current = storage.counter.read();
storage.counter.write(current - amount);
storage.counter.read()
}
}
递减逻辑与递增对称:读取当前计数 → 减去 amount → 写回存储 → 返回最新值。u64 为无符号类型,实际使用时需自行保证 amount 不超过当前值,否则会触发下溢(本示例未做边界处理,可视为刻意保持简洁)。
第 2 步:修改前端
前端合约调用逻辑集中在 src/components/Contract.tsx。参照已有的 incrementCounter,新增一个调用 decrement_counter 的异步函数:
async function decrementCounter() {
if (!wallet || !contract) return;
setIsLoading(true);
try {
const call = await contract.functions.decrement_counter(1).call();
transactionSubmitNotification(call.transactionId);
const result = await call.waitForResult();
transactionSuccessNotification(result.transactionId);
setCounter(result.value.toNumber());
} catch (error) {
console.error(error);
errorNotification("Error decrementing counter");
}
setIsLoading(false);
}
这段代码的关键点在于 Fuel SDK 的类型安全合约调用链:
contract实例类型为脚手架生成的TestContract(从./sway-api自动导入),因此contract.functions.decrement_counter具有完整的入参/返回值类型推导;.call()提交交易并返回一个包含transactionId与waitForResult()的调用句柄;call.waitForResult()等待交易上链并返回结果,result.value即合约返回的新计数值;- 前端通过
setCounter(result.value.toNumber())更新 UI,useNotificationHook(见 useNotification.tsx)负责提交/成功/失败三类状态提示。
接着在返回的 JSX 中加入调用按钮。仓库真实实现(Contract.tsx)在「Increment」按钮旁并列放置了:
<Button onClick={decrementCounter} className="w-1/3" disabled={isLoading}>
Decrement
</Button>
按钮会在点击后调用 decrementCounter,isLoading 状态用于在交易处理期间禁用按钮,防止重复提交。
保存修改后回到 http://localhost:5173,你会发现「Contract」标签页同时出现 Increment 与 Decrement 按钮,此时计数器 dApp 已具备双向增减能力:
当你后续想快速为 dApp 添加新功能做原型验证时,都可以沿用「改 Sway 合约 → 等类型重新生成 → 改前端组件调用」这套三步循环。
第 3 步(可选):扩展合约测试
为智能合约编写测试是保证实现正确性的良好实践,也能在后续修改实现时提供回归保障。Sway 内置测试通过 #[test] 宏编写,既可以内联在合约文件里,也可以放在单独文件中。
在本指南中,我们将针对新增的 decrement_counter 在 ./sway-programs/contract/src/main.sw 中添加测试。Sway 测试可以在链上部署合约实例(abi(Counter, CONTRACT_ID))并断言返回值,仓库中的完整示例为:
#[test]
fn test_decrement_counter() {
let contract_instance = abi(Counter, CONTRACT_ID);
let _ = contract_instance.increment_counter(5);
let count_before = contract_instance.get_count();
let count_after = contract_instance.decrement_counter(1);
assert(count_after == count_before - 1);
}
测试思路:先用 increment_counter(5) 把计数设到 5,读取递减前的值,再调用 decrement_counter(1),断言新值等于旧值减 1,从而验证函数读写存储的逻辑正确。
写好后通过 forc test 直接运行,或使用项目脚本 pnpm test:forc(在 package.json 中对应 forc test --path ./sway-programs):
forc test --path ./sway-programs
第 4 步(可选):扩展集成测试套件
合约单测之外,集成测试能在受控的本地节点环境中验证整个应用的端到端行为。模板在每个程序类型对应的 ./test 目录下都提供了示例,下面为我们的递减功能补两类测试。
TypeScript 集成测试
在 ./test/contract.test.ts 中新增针对 decrement_counter 的测试。指南文档中的示例代码(见 snippets/decrement-counter.ts)展示了完整模式:创建 Provider 与钱包 → 用工厂部署合约 → 依次调用并断言:
import { Wallet, Provider } from 'fuels';
import { LOCAL_NETWORK_URL, WALLET_PVT_KEY } from '../../../env';
import { CounterFactory } from '../../../typegend/contracts';
const provider = new Provider(LOCAL_NETWORK_URL);
const wallet = Wallet.fromPrivateKey(WALLET_PVT_KEY, provider);
// Deploy the contract using the generated factory.
const { waitForResult } = await CounterFactory.deploy(wallet);
const { contract } = await waitForResult();
// Read the initial value.
const { waitForResult: getCountWaitForResult } = await contract.functions
.get_count()
.call();
const { value: initialGetCountValue } = await getCountWaitForResult();
// Increment, then decrement, asserting each intermediate value.
const { waitForResult: incWaitForResult } = await contract.functions
.increment_count(5)
.call();
const { value: incValue } = await incWaitForResult();
const { waitForResult: decWaitForResult } = await contract.functions
.decrement_count(3)
.call();
const { value: decValue } = await decWaitForResult();
// Finally read back the value to ensure parity.
const { waitForResult: finalWaitForResult } = await contract.functions
.get_count()
.call();
const { value: finalValue } = await finalWaitForResult();
需要注意:示例中
contract.functions.<method>的方法名来自 typegen 根据 ABI 生成的类型。在本指南的 Counter 合约(ABI 中函数名为increment_counter/decrement_counter)上,实际调用名应与 Contract.tsx 中一致,示例代码源自文档工程里的演示片段,落地到你自己的合约时请以 typegen 实际生成的类型为准。
Playwright UI 测试
模板还预置了基于 Playwright 的 UI 测试框架(配置文件见 playwright.config.ts)。对递减功能,可参照仓库中的 test/ui/ui.test.ts 编写 UI 测试:
test('counter contract - decrement function call works properly', async ({ page }) => {
await setup({ page });
const topUpWalletButton = page.getByText('Transfer 5 ETH', { exact: true });
await topUpWalletButton.click();
await page.waitForTimeout(2000); // ensure transactions are mined
const contractTab = page.getByText('Contract');
await contractTab.click();
const initialCounterValue = +page.getByTestId('counter').textContent;
const decrementButton = page.getByText('Decrement', { exact: true });
await decrementButton.click();
const counterValueAfterDecrement = +page.getByTestId('counter').textContent;
expect(counterValueAfterDecrement).toEqual(initialCounterValue - 1);
});
该测试复用了 setup 辅助函数:打开 http://localhost:5173 → 点击「Connect Wallet」→ 选择「Burner Wallet」建立临时钱包会话;随后给钱包转入测试 ETH、切换到 Contract 标签页,点击 Decrement 按钮并断言界面上的计数值减 1。注意测试通过 data-testid="counter" 定位计数显示框、用 exact: true 精确匹配按钮文本,这些选择器在 Contract.tsx 中均有对应实现。
模板同时提供 test:ui 脚本(sh ./test/ui/test-ui.sh),可一键拉起节点与前端后执行全部 UI 测试。
后续方向
- 在掌握
create fuels工作流与基本 Counter dApp 后,可以基于 Fuel Stack 构建更复杂的应用,Predicate 与 Script 是 Fuel 体系中值得重点探索的程序类型; - 若希望把 dApp 部署到 Testnet,可参考 Deploying a dApp to Testnet 指南(示例项目已内置
VITE_DAPP_ENVIRONMENT=testnet、testnetContractId等切换逻辑,见 lib.tsx); - 如需进一步验证 dApp 与各类程序的功能,可结合
create fuels项目中的test目录深入阅读测试方法论; - 想查看本文所述 dApp 的完整源码与 Sway 测试,可直接研读仓库内的 apps/create-fuels-counter-guide 目录。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0631
MiniCPM5-2BMiniCPM5-2B 是一款面向端侧、本地部署和资源受限场景的 2B 稠密 Transformer,能够达到同尺寸开源模型 SOTA 水平。Markdown00
video-shotcraftAI宣传片skill,使用 Remotion 制作电影级产品视频:提供106 张镜头配方卡和可复用的视频魔板。适用于 Claude Code 与 Codex以及所有其他智能体Markdown00
HivisionIDPhotos⚡️HivisionIDPhotos: a lightweight and efficient AI ID photos tools. 一个轻量级的AI证件照制作算法。Python09
DragonOSDragonOS is an operating system developed from scratch using Rust, with Linux compatibility. It is designed for **Serverless** scenarios. 使用Rust从0自研内核,具有Linux兼容性的操作系统,面向云计算Serverless场景而设计。Rust00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00


