fuels-ts 中的 Asset ID 机制:getMintedAssetId 与 createAssetId 原理及实战
在 Fuel 网络中,每种资产都由一个唯一的 Asset ID 标识,而资产发行(Mint)合约在铸造代币时必须先搞清楚:这个新资产的 ID 到底是什么?本文基于 fuels-ts 官方文档 Minted Token Asset ID,完整讲解 Asset ID 的推导规则、getMintedAssetId 与 createAssetId 两个核心工具函数,并结合仓库源码剖析其底层哈希实现与在交易回执(Receipt)处理中的实际调用位置,帮助你在部署代币合约后正确计算、使用铸造出的资产 ID。
Asset ID 由什么决定
Fuel 网络上一个代币的 Asset ID 由两个因素共同决定:
- 铸造该代币的合约 ID(Contract ID);
- 子标识符(Sub ID)。
两者都是 B256 字符串(32 字节,64 个十六进制字符加 0x 前缀)。
推导过程是把 Contract ID 与 Sub ID 拼接后对其应用 SHA-256 哈希算法,得到的哈希值即为该资产的 Asset ID。这意味着:
- Contract ID 是动态的——每次部署合约,生成的合约 ID 都不同,因此同一份合约代码部署两次会产生两种不同的资产;
- Sub ID 可以是固定的——它只是合约 ABI 方法里的一个普通
b256参数,开发者可以约定一个常量值。
这一特性让合约天然支持"多资产发行":同一个合约用不同的 Sub ID 铸造,就会得到互不冲突的多个 Asset ID。
一个简化的 Token 合约
先看文档中给出的这个简化版代币合约(token/src/main.sw),它是理解后续操作的上下文:
contract;
use std::asset::{burn, mint, transfer};
abi Token {
fn transfer_to_address(target: Address, asset_id: AssetId, coins: u64);
fn transfer_to_contract(recipient: ContractId, asset_id: AssetId, coins: u64);
fn mint_coins(sub_id: b256, mint_amount: u64);
fn burn_coins(sub_id: b256, burn_amount: u64);
}
impl Token for Contract {
fn transfer_to_address(recipient: Address, asset_id: AssetId, amount: u64) {
transfer(Identity::Address(recipient), asset_id, amount);
}
fn transfer_to_contract(target: ContractId, asset_id: AssetId, amount: u64) {
transfer(Identity::ContractId(target), asset_id, amount);
}
fn mint_coins(sub_id: b256, mint_amount: u64) {
mint(sub_id, mint_amount);
}
fn burn_coins(sub_id: b256, burn_amount: u64) {
burn(sub_id, burn_amount);
}
}
合约中真正执行铸造的是 Sway 标准库的 mint(sub_id, amount) 内建函数。注意 sub_id 是每次调用时传入的参数,而不是合约常量——这决定了 Asset ID 只有在合约部署完成后、选定 Sub ID 之后才能被计算出来。
部署合约并铸造代币
假设上述合约已经通过 typegen 生成了 TokenFactory,文档给出的铸造流程如下(minted-token-asset-id.ts):
import { bn, getMintedAssetId, Provider, Wallet } from 'fuels';
import { LOCAL_NETWORK_URL, WALLET_PVT_KEY } from '../../../../env';
import { TokenFactory } from '../../../../typegend';
const provider = new Provider(LOCAL_NETWORK_URL);
const deployer = Wallet.fromPrivateKey(WALLET_PVT_KEY, provider);
const deployContract = await TokenFactory.deploy(deployer);
const { contract } = await deployContract.waitForResult();
// Any valid B256 string can be used as a sub ID
const subID =
'0xc7fd1d987ada439fc085cfa3c49416cf2b504ac50151e3c2335d60595cb90745';
const mintAmount = bn(1000);
const { waitForResult } = await contract.functions
.mint_coins(subID, mintAmount)
.call();
await waitForResult();
// Get the minted
const mintedAssetId = getMintedAssetId(contract.id.toB256(), subID);
console.log('Minted asset ID should be defined', mintedAssetId);
const { transactionResult } = await waitForResult();
console.log(
'Transaction should be successful',
transactionResult.isStatusSuccess
);
几个关键点:
- Sub ID 可以是任意合法的 B256 字符串。示例中用一个写死的 B256 值演示;生产中常见做法是把 Sub ID 设为合约代码中约定的常量,便于团队统一。
contract.id.toB256()返回的是部署后动态生成的合约 ID 的 B256 字符串形式,这正是 Asset ID 推导中"动态"的那一半。getMintedAssetId是一个纯本地计算:它不访问节点、不消耗 gas,任何时候只要知道 Contract ID 和 Sub ID,就能在客户端直接算出 Asset ID。这一点在"部署后立即转账"或"预估手续费"等场景中非常有用——你甚至可以在 mint 交易上链之前就拿到未来的 Asset ID。
getMintedAssetId 的源码实现
getMintedAssetId 定义在 receipt.ts(@fuel-ts/transactions 包,经由 fuels 聚合包导出):
export const getMintedAssetId = (contractId: string, subId: string): string => {
const contractIdBytes = arrayify(contractId);
const subIdBytes = arrayify(subId);
return sha256(concat([contractIdBytes, subIdBytes]));
};
实现非常直白:
arrayify(来自@fuel-ts/utils)把两个 B256 十六进制字符串各自转成 32 字节的Uint8Array;concat将 Contract ID 的 32 字节拼在 Sub ID 的 32 字节前面,得到 64 字节输入;- 最后用
@noble/hashes的sha256求哈希,返回 32 字节结果的十六进制字符串。
字节拼接顺序(Contract ID 在前、Sub ID 在后)是协议层面的规定,顺序颠倒会得到完全不同的 Asset ID。从源码结构看,这个函数没有任何输入校验,因此调用方有责任保证传入的是合法的 B256 字符串(否则 arrayify 会因非法十六进制而抛出异常)。
由于 Asset ID 取决于动态的 Contract ID,而 Sub ID 可以固定,这个辅助函数就是文档推荐用来"给定合约 ID 和 Sub ID 快速求得 Asset ID"的官方入口。
createAssetId:直接获得 Sway 原生的 AssetId 参数
如果你要在调用 Sway 方法时把 Asset ID 作为参数传入(例如 transfer_to_address(recipient, asset_id, amount) 中的 asset_id),需要的是 Sway 原生类型 AssetId 对应的 JS 对象,而不是普通字符串。SDK 提供的 createAssetId 就是为此设计的,它内部直接复用 getMintedAssetId(receipt.ts):
export const createAssetId = (contractId: string, subId: string): AssetId => ({
bits: getMintedAssetId(contractId, subId),
});
AssetId 是 @fuel-ts/address 包中的类型别名,本质是形如 { bits: string } 的 B256 包装对象,与 Sway 端的 AssetId 结构体一一对应,可直接作为合约调用参数。文档中的使用示例(create-asset-id.ts):
import type { AssetId, B256Address } from 'fuels';
import { createAssetId } from 'fuels';
const contractId: B256Address =
'0x67eb6a384151a30e162c26d2f3e81ca2023dfa1041000210caed42ead32d63c0';
const subID: B256Address =
'0xc7fd1d987ada439fc085cfa3c49416cf2b504ac50151e3c2335d60595cb90745';
const assetId: AssetId = createAssetId(contractId, subID);
// {
// bits: '0x16c1cb95e999d0c74806f97643af158e821a0063a0c8ea61183bad2497b57478'
// }
对同一对 (contractId, subID),createAssetId 与 getMintedAssetId 的哈希结果完全一致,区别只在于返回值类型:前者返回可直接喂给合约调用的 AssetId 对象,后者返回裸的 B256 字符串,适合日志输出、比较或存储。
它不只是工具函数:SDK 内部也在用它
从源码结构看,getMintedAssetId 除了供开发者手动计算 Asset ID 外,还被 SDK 内部用于解析交易回执。在 serialization.ts(@fuel-ts/account 包的 providers 工具模块)中,SDK 在把节点返回的 ReceiptMint / ReceiptBurn 回执序列化为强类型对象时,会用 getMintedAssetId(contractId, subId) 由回执中携带的合约 ID 和 Sub ID 还原出对应的 Asset ID。这解释了为什么文档示例里 mint 交易完成后、无需额外查询即可拿到"本次铸造的 Asset ID"——它与节点侧的推导逻辑是同一个公式,天然一致。
小结与适用边界
- 公式:
Asset ID = SHA256(Contract ID ‖ Sub ID),两个输入均为 B256 字符串,Contract ID 在前。 getMintedAssetId(contractId, subId):返回 B256 字符串,纯本地计算,适合任何需要"提前知道"资产 ID 的场合。createAssetId(contractId, subId):返回 Sway 原生AssetId对象({ bits }),用于合约方法调用传参。- 两个函数均要求传入合法 B256 字符串,不做格式校验;Contract ID 必须由实际部署结果取得,不能凭空假设。
- 参考实现位置:packages/transactions/src/receipt.ts、packages/account/src/providers/utils/serialization.ts;文档原文见 apps/docs/src/guide/contracts/minted-token-asset-id.md。
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 StartedRust0623
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00