首页
/ 使用 create fuels 从零搭建全栈 Fuel dApp(Counter 合约实战指南)

使用 create fuels 从零搭建全栈 Fuel dApp(Counter 合约实战指南)

2026-09-08 20:29:15作者:秋阔奎Evelyn

本指南基于 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。所谓「全栈」是指它一次性为你准备好了三层内容:

  1. Sway 智能合约:默认内置一个只支持递增(increment)的简单 Counter 合约;
  2. 类型安全前端:基于 Vite 与 React 的应用,配合 @fuels/react 等钱包连接组件,直接读取已部署合约的 ABI 类型;
  3. 本地开发链路fuels CLI 负责编译 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-forcfuels-core 工具,避免要求开发者手动安装 Sway 工具链。

关于该配置文件的更多参数说明,可查阅 fuels CLI 配置文档

sway-programs/contract/src/main.sw:智能合约

Counter 合约的源码位于 sway-programs/contract/src/main.sw。开箱即用它只支持递增,下面的实战部分我们会为其加上递减功能。仓库中的完整实现见 main.sw

src/App.tsxsrc/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.jsonfuels:dev 实际执行 fuels devfuels:build 执行 fuels build

服务器就绪后,在另一个终端启动前端开发服务器:

pnpm dev
npm run dev
bun run dev

现在打开 http://localhost:5173,即可看到 dApp 界面。点击「Connect Wallet」连接钱包——若不想连接真实钱包,可以直接在连接器列表中选择 Burner Wallet(Fuel 生态提供的临时/一次性钱包,便于本地开发测试)。

Fuel dApp 连接钱包时的连接器选择列表

连接钱包后,可以尝试修改 ./sway-programs/contract/src/main.sw 中的合约代码并保存,改动会实时反映在 UI 的「Contract」标签页中,无需重启前端服务器——这正是 fuels dev 提供的全栈开发体验:左侧前端页面与右侧节点/编译日志分屏运行,合约改动即时编译部署、前端类型即时重新生成。

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() 提交交易并返回一个包含 transactionIdwaitForResult() 的调用句柄;
  • call.waitForResult() 等待交易上链并返回结果,result.value 即合约返回的新计数值;
  • 前端通过 setCounter(result.value.toNumber()) 更新 UI,useNotification Hook(见 useNotification.tsx)负责提交/成功/失败三类状态提示。

接着在返回的 JSX 中加入调用按钮。仓库真实实现(Contract.tsx)在「Increment」按钮旁并列放置了:

<Button onClick={decrementCounter} className="w-1/3" disabled={isLoading}>
  Decrement
</Button>

按钮会在点击后调用 decrementCounterisLoading 状态用于在交易处理期间禁用按钮,防止重复提交。

保存修改后回到 http://localhost:5173,你会发现「Contract」标签页同时出现 Increment 与 Decrement 按钮,此时计数器 dApp 已具备双向增减能力:

为 Counter dApp 添加 decrement 功能后的最终界面

当你后续想快速为 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=testnettestnetContractId 等切换逻辑,见 lib.tsx);
  • 如需进一步验证 dApp 与各类程序的功能,可结合 create fuels 项目中的 test 目录深入阅读测试方法论;
  • 想查看本文所述 dApp 的完整源码与 Sway 测试,可直接研读仓库内的 apps/create-fuels-counter-guide 目录。
登录后查看全文
热门项目推荐
相关项目推荐

项目优选

收起
kernelkernel
deepin linux kernel
C
33
18
ops-transformerops-transformer
本项目是CANN提供的transformer类大模型算子库,实现网络在NPU上加速计算。
C++
1.14 K
2.75 K
pytorchpytorch
作为 Ascend for PyTorch 社区的核心组件,TorchNPU 是昇腾专为 PyTorch 打造的深度学习适配插件,使 PyTorch 框架能够直接调用昇腾 NPU,为开发者提供昇腾 AI 处理器的超强算力。
Python
860
1.35 K
docsdocs
暂无描述
Markdown
899
5.83 K
ops-nnops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
C++
924
1.85 K
jiuwenswarmjiuwenswarm
JiuwenSwarm 是一款基于openJiuwen开发的智能AI Agent,它能够将大语言模型的强大能力,通过你日常使用的各类通讯应用,直接延伸至你的指尖。
Python
3.84 K
1.02 K
kernelkernel
openEuler内核是openEuler操作系统的核心,既是系统性能与稳定性的基石,也是连接处理器、设备与服务的桥梁。
C
533
599
cann-learning-hubcann-learning-hub
CANN 学习中心仓,支持在线互动运行、边学边练,提供教程、示例与优化方案,一站式助力昇腾开发者快速上手。
Jupyter Notebook
1.03 K
525
ops-mathops-math
本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。
C++
1.37 K
1.46 K
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
548
394