fuels-ts 中的 Asset ID 机制:getMintedAssetId 与 createAssetId 原理及实战
【免费下载链接】fuels-tsFuel Network Typescript SDK项目地址: https://gitcode.com/GitHub_Trending/fu/fuels-ts
在 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。
【免费下载链接】fuels-tsFuel Network Typescript SDK项目地址: https://gitcode.com/GitHub_Trending/fu/fuels-ts
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考