fuels-ts 数值运算模块@fuel-ts/math完全指南:基于 bn.js 的安全大数运算、单位换算与格式化工具
【免费下载链接】fuels-tsFuel Network Typescript SDK项目地址: https://gitcode.com/GitHub_Trending/fu/fuels-ts
@fuel-ts/math是 Fuel TypeScript SDK(fuels-ts)monorepo 中的一个基础子模块,为上层所有涉及数值运算的代码提供统一入口。它围绕decimal(十进制小数)、hex(十六进制字符串)与Uint8Array(字节数组)三类数据形态封装了一整套基于 bn.js 的数学工具,让任意大小的整数计算都足够安全、精确。读完本文,你将掌握该模块的安装方式、核心BN类型与函数式 API 的完整用法,理解formatUnits/parseUnits的单位换算机制、默认精度配置的来源,以及该模块在 SDK 内部(如账户余额、交易金额处理)的真实应用场景。
模块定位:为什么 fuels-ts 需要一个独立的数学包
在区块链场景下,绝大多数数值都远超 JavaScriptNumber的安全整数范围(Number.MAX_SAFE_INTEGER,即2^53 - 1)。无论是 Fuel 网络的资产余额、交易金额,还是燃料价格,都以最小单位(wei 级别)的整数在网络中传输。如果直接使用原生number进行计算,很容易因精度丢失而引入严重 bug。
为此,fuels-ts 单独抽取了 packages/math 作为 SDK 共用的数值底座,其设计要点是:
- 基于 bn.js实现任意精度整数运算,无论数值多大都能安全完成加减乘除、幂、取模等计算;
- 面向 Fuel 的实际开发场景,额外提供
decimal、hex、Uint8Array三种形态之间的互转,以及带小数位的单位格式化能力; - 被 SDK 内其他包大量复用。例如在 account.ts 中,余额、手续费、
CoinQuantity等几乎全部用bn(...)构造、用.toNumber()/.toHex()转换,说明该模块是 SDK 数值处理的公共基础设施。
安装方式
单独使用该模块时,按官方 README 推荐使用pnpm:
pnpm add @fuel-ts/math # 或 npm add @fuel-ts/math根据 package.json,其运行时依赖仅有两个:bn.js@5.2.1及其类型声明@types/bn.js@5.1.6,外加 monorepo 内的@fuel-ts/errors(用于抛出标准化的FuelError)。安装体积与依赖面都很小,可在任意 Node.js 环境(^20 || ^22 || ^24)中使用,同时打包产物支持require/import/类型三种入口。
如果你正在开发完整的 Fuel 应用而非底层库,官方推荐直接安装聚合包fuels(SDK 的 umbrella package),它将@fuel-ts/math与地址、账户、合约、脚本、钱包等能力一并带入:
pnpm add fuels # 或 npm add fuels导出结构总览
模块的统一出口是 index.ts,仅用五条 re-export 就组织好了全部能力:
export * from './bn'; export * from './decimal'; export * from './functional'; export * from './math'; export * from './types';各文件的职责划分非常清晰:
| 源文件 | 职责 |
|---|---|
| bn.ts | 核心BN类(继承 bn.js 的BN)与函数式工厂bn()、bn.parseUnits() |
| decimal.ts | 面向展示场景的toFixed小数格式化函数 |
| functional.ts | toNumber/toHex/toBytes/formatUnits/format五个函数式快捷调用 |
| math.ts | max、multiply等聚合数学函数 |
| types.ts | BigNumberish、FormatConfig、ToFixedConfig等共享类型 |
核心:BN类与bn()工厂函数
统一入参类型BNInput
BN的构造与所有算术方法都接受一个宽松的入参联合类型BNInput(定义于 bn.ts):
type BNInput = number | string | number[] | Uint8Array | Buffer | BnJs;也就是说,无论你手头是普通数字、"1000000000000000000"这样的十进制字符串、"0x1ff"十六进制字符串、字节数组,还是另一个bn.js实例,都可以直接传入参与运算,无需手动统一。
构造与输入归一化规则
BN构造函数(bn.ts)做了几件关键的输入处理:
- 传入
BN实例时,先转成字节数组再重新构造,保证返回的是本模块的BN类型; - 十六进制字符串会自动剔除
0x前缀(bn.js 本身不接受该前缀),并把进制默认设为'hex'; - 空值保护:
null/undefined/缺省参数都会被当作0; - 安全整数校验:当入参是
number且超过Number.MAX_SAFE_INTEGER时,直接抛出携带ErrorCode.NUMBER_TOO_BIG的FuelError,提示“数值过大,请改用字符串”。测试 bn.test.ts 明确验证了Number.MAX_SAFE_INTEGER + 1这种不安全的 number 会被拒绝。
函数式工厂bn(value?, base?, endian?)只是new BN(...)的语法糖(bn.ts),在 SDK 内部的使用频率远高于new BN(...),例如 account.ts 中的amount: bn(fee)。
核心方法速查表
| 方法 | 签名 | 行为要点 |
|---|---|---|
toString | (base?, length?) | 重写后,base === 16或'hex'时强制带0x前缀 |
toHex | (bytesPadding?: number) | 输出0x...;负数与超出 padding 长度都会抛CONVERTING_FAILED |
toBytes | (bytesPadding?: number) | 输出Uint8Array;负数抛错,支持前置补零 |
toJSON | () | 输出十六进制字符串 |
valueOf | () | 输出十进制字符串(与无参toString()一致) |
add/sub/mul/div/pow/mod/divRound | (v: BNInput) | 运算后返回本模块的BN,永不丢失类型引用 |
lt/lte/gt/gte/eq | (v: BNInput) | 返回布尔值的比较 |
cmp | (v: BNInput) | 返回-1 \| 0 \| 1 |
sqr/neg/abs/toTwos/fromTwos | (width?) | 返回BN(覆盖 bn.js 原方法) |
clone | () | 深拷贝为新的BN |
maxU64 | () | 若当前值超过0xFFFFFFFFFFFFFFFF,钳制到该上限 |
max | (v: BNInput) | 返回两者中的较小值(语义为上限保护) |
normalizeZeroToOne | () | 若为0则返回1,否则不变 |
format/formatUnits | 见下文 | 单位换算与展示格式化 |
为什么重写这么多方法:避免丢失BN引用
阅读 bn.ts 可以发现一个精心设计:add、pow、sub等所有算术与比较方法都经由内部caller(v, methodName)转发(bn.ts),其结果若仍是 bn.js 的BN,会立即new BN(output.toArray())重新包装成本模块的BN。sqr、neg、abs、toTwos、fromTwos、mulTo、egcd、divmod也如法炮制。
这意味着你可以像下面这样无限链式调用,且每一步返回值都是本模块的BN,链式表达式的类型与行为保持一致:
bn(2).add(2).sub(2).pow('0x3').mul(2).div(2).sqr().abs().mod(2).divRound(2);对应测试见 bn.test.ts,它逐个断言了链式中每步调用结果仍能以toString(16)得到0x前缀的十六进制串。
hex 与 Uint8Array 互转及字节对齐
Fuel 链上与交易、签名相关的很多字段都是定长字节,因此toHex(bytesPadding)/toBytes(bytesPadding)的补零对齐能力至关重要。bytesPadding以字节为单位:调用toHex(8)会把值补足为 8 字节(16 个十六进制字符)宽度的0x0000000000000000形式;toBytes(8)同理返回 8 个字节的Uint8Array。若值本身超出声明的字节宽度,则抛出转换失败错误。完整边界用例可参考 bn.test.ts。
import { bn } from '@fuel-ts/math'; bn(1).toHex(); // '0x1' bn(1).toHex(2); // '0x0001' bn('0x1ff').toBytes(); // Uint8Array [1, 255] bn(255).toBytes(4); // Uint8Array [0, 0, 0, 255]单位换算与展示格式化:format/formatUnits/parseUnits
区块链应用最常见的两类需求是:把链上的最小单位整数还原成带小数的资产展示值,以及把用户输入的带小数金额转换回最小单位整数提交上链。@fuel-ts/math为此提供了镜像的两个 API。
formatUnits(units?):整数 → 带小数位字符串
formatUnits按指定小数位在整数值上打小数点(bn.ts),默认units = 9(Fuel 网络默认 9 位小数):
bn('1000000000').formatUnits(); // '1.000000000' bn('2').formatUnits(); // '0.000000002' bn('100000020000').formatUnits(); // '100.000020000' bn('1000000000').formatUnits(7); // '100.0000000'format(options?):带千分位、去尾零的展示格式化
format在formatUnits基础上叠加precision(最大小数位数)与minPrecision(最小保留位数),并自动添加千分位分隔符与去除尾部多余的零,是面向UI 展示的高阶封装(bn.ts):
bn('1000000000').format(); // '1.000' (默认去零后保留 3 位最小精度) bn('2').format(); // '0.000000002' bn('100000020000').format(); // '100.00002' bn('100100000020000').format(); // '100,100.00002' bn('1000000000').format({ minPrecision: 2, units: 8 }); // '10.00'其中配置项来自 types.ts 定义的FormatConfig:
type FormatConfig = { units?: number } & ToFixedConfig; type ToFixedConfig = { minPrecision?: number; precision?: number };bn.parseUnits(value, units?):展示值 → 最小单位整数
parseUnits是formatUnits的逆操作(bn.ts),它把形如'100.00002'、甚至带千分位'100,100.00002'或.的字符串解析回最小单位整数,位数不足则自动补零,小数位超过units时抛出CONVERTING_FAILED:
bn.parseUnits('1'); // BN: 1000000000 bn.parseUnits('100.00002'); // BN: 100000020000 bn.parseUnits('100,100.00002', 5); // BN: 10010000002 bn.parseUnits('.'); // BN: 0 bn.parseUnits('0.000000002'); // BN: 2默认配置的出处
以上默认值全部定义在 configs.ts,并由测试 configs.test.ts 锁定不可随意变更:
| 常量 | 值 | 含义 |
|---|---|---|
DEFAULT_DECIMAL_UNITS | 9 | 默认小数位(formatUnits/parseUnits的默认units) |
DEFAULT_PRECISION | 9 | 默认最大展示精度 |
DEFAULT_MIN_PRECISION | 3 | 默认最小展示精度(防止整块金额被显示成1而丢失小数视觉信息) |
DECIMAL_FUEL | 9 | Fuel 网络的原生小数位数 |
DECIMAL_WEI/DECIMAL_KWEI/DECIMAL_MWEI/DECIMAL_GWEI | 18 / 15 / 12 / 9 | Ethereum 生态常见单位换算常数,供需要换算 ERC-20 金额的场景参考 |
展示辅助函数:toFixed
decimal.ts 提供的toFixed(value, options?)面向纯粹的展示场景,接受字符串或数字输入,返回带千分位分隔且小数位数被precision/minPrecision约束的字符串。它同样默认使用DEFAULT_PRECISION = 9与DEFAULT_MIN_PRECISION = 3,并在minPrecision < precision时先去除尾部零再按最小精度补齐:
toFixed('100000020000'); // '100,000,020,000'(无小数输入时的整数分组) toFixed('1234567.89123'); // 千分位分组 + 精度约束 toFixed('1234.5', { precision: 4, minPrecision: 2 }); // '1,234.5'若你只处理纯整数且不需要单位换算,用toFixed即可完成分组展示,无需构造BN实例。
函数式快捷 API 与聚合函数
为了让不持有BN实例的代码也能方便使用,functional.ts 暴露了五个一等函数,它们内部都是bn(value)的薄封装:
toNumber('0x1ff'); // 511 toHex(1, 2); // '0x0001' toBytes('0x1ff'); // Uint8Array [1, 255] formatUnits('1000000000'); // '1.000000000' format('100000020000'); // '100.00002'math.ts 则提供两个聚合运算:
max(...numbers):返回一组BigNumberish中的最大值(以bn(0)为起点归约);multiply(...numbers):对一组值连续相乘后向上取整。
这里的BigNumberish = string | number | BN同样来自 types.ts,是 SDK 其他模块引用本包时最常用的类型别名。
在 SDK 中的真实应用
从源码检索可以看到,@fuel-ts/math并不是孤立的工具包,而是被大量 SDK 内部代码直接依赖。以 account.ts 为例:
- 余额与
CoinQuantity的组装使用bn(0)、bn(fee)、bn(transferParam.amount); - 在计算手续费、合并输入时大量使用
acc.add(input.amount)、bn(0)这类链式与归约运算; - 十六进制编码输出时使用
'0x'.concat(bn(amount).toHex()...)或直接依赖toHex()自带的0x前缀。
换言之,你在使用fuels聚合包开发 DApp 时,凡是经手余额、金额、gas 等数值的场景,底层都在使用本文介绍的BN、parseUnits、format等能力。理解了@fuel-ts/math,就等于理解了 fuels-ts 全部数值处理行为的地基。
生态与配套信息
- 版本与依赖:包版本号见 packages/math/package.json,构建脚本为
tsup,产物输出到dist; - 变更记录:完整的版本变更历史见 packages/math/CHANGELOG.md;
- 许可证:该子模块采用
Apache 2.0,详见 packages/math/LICENSE; - 参与贡献:本包是 fuels-ts 单一 monorepo 的一部分,贡献指引请参见仓库根目录的 CONTRIBUTING.md,其余包对
@fuel-ts/math的调用方式也可直接查阅上文提到的 account.ts 等源码作为最佳实践参考。
【免费下载链接】fuels-tsFuel Network Typescript SDK项目地址: https://gitcode.com/GitHub_Trending/fu/fuels-ts
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考