Skip to content

Commit c0211ee

Browse files
authored
feat(ecosystem): enforce raw amount semantics across miniapp transactions (#442)
1 parent 3e3a190 commit c0211ee

16 files changed

Lines changed: 487 additions & 25 deletions

File tree

docs/white-book/11-DApp-Guide/02-Connectivity/01-Bio-SDK-Communication.md

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -177,10 +177,16 @@ import { bio } from '@bioforest/bio-sdk';
177177
// 请求钱包地址
178178
const accounts = await bio.wallet.requestAccounts();
179179

180-
// 签名交易
181-
const txHash = await bio.wallet.sendTransaction({
182-
to: '0x...',
183-
value: '1000000000000000000',
180+
// 发送交易(amount 使用 raw 最小单位整数字符串)
181+
const txResult = await bio.request({
182+
method: 'bio_sendTransaction',
183+
params: [{
184+
from: 'bFxxxxxxxxxxxxxxxxxxxx',
185+
to: 'bNxxxxxxxxxxxxxxxxxxxx',
186+
amount: '1000000000', // raw(例如 decimals=8 => 10.00000000)
187+
chain: 'BFMetaV2',
188+
asset: 'USDT',
189+
}],
184190
});
185191

186192
// 键值存储
@@ -259,6 +265,12 @@ class BioSDK {
259265
}
260266
```
261267

268+
## 金额语义(重要)
269+
270+
- 所有交易相关接口(`bio_sendTransaction` / `bio_createTransaction` / `bio_destroyAsset`)的 `amount` 均使用 **raw 最小单位整数字符串**
271+
- 禁止传入格式化小数字符串(例如 `10.00000000`)。
272+
- 详见:[`02-Amount-Semantics-Standard.md`](./02-Amount-Semantics-Standard.md)
273+
262274
## 安全考虑
263275

264276
1. **来源验证**: Host 端应验证 `event.source` 确保消息来自预期的 iframe
Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
# 02. MiniApp 交易金额语义标准(Raw Amount Standard)
2+
3+
> Last Updated: 2026-02-12
4+
> Status: Active(生态升级基线)
5+
6+
本文定义 MiniApp 与 KeyApp 交互中所有“链上金额字段”的统一语义,避免“展示金额与真实广播金额不一致”的高风险问题。
7+
8+
---
9+
10+
## 1. 统一规则(必须遵守)
11+
12+
### 1.1 金额字段一律使用 raw(最小单位整数)
13+
14+
- `amount` 必须是 **十进制整数字符串**`^\d+$`
15+
- 表示链最小单位(如 USDT 8 位精度时,`1000000000` 表示 `10.00000000`
16+
- 禁止传入格式化小数字符串(例如 `10.00000000`
17+
18+
### 1.2 展示与签名/广播职责分离
19+
20+
- 展示层:根据 `decimals` 把 raw 转为人类可读金额
21+
- 交易层:签名与广播始终使用 raw
22+
- 严禁在显示修复时改动实际广播语义
23+
24+
### 1.3 适用接口(第一批)
25+
26+
- `bio_sendTransaction.params.amount`
27+
- `bio_createTransaction.params.amount`
28+
- `bio_destroyAsset.params.amount`
29+
30+
---
31+
32+
## 2. 风险背景(为什么必须统一)
33+
34+
当调用方把 raw 当 formatted,或 Host 把 formatted 当 raw,会导致:
35+
36+
- 面板显示金额被放大/缩小(误导用户)
37+
- 签名/广播金额与用户预期不一致
38+
- 后端入库与链上数据出现语义冲突(含重复提交与对账困难)
39+
40+
这是高风险交易语义问题,不是纯 UI 问题。
41+
42+
---
43+
44+
## 3. 当前审计结论(KeyApp 内置应用)
45+
46+
### 3.1 已符合或基本符合
47+
48+
- `xin.dweb.rwahub`:调用 `bio_sendTransaction` 前使用整数运算构造 raw(BigInt 路径)
49+
- `xin.dweb.biobridge`(forge/redemption 主路径):在有精度上下文时将用户输入转换为 raw 再调用
50+
51+
### 3.2 需升级
52+
53+
- `xin.dweb.teleport`:当前 `bio_createTransaction` 调用路径仍可能传格式化小数字符串(示例:补 `.0`
54+
55+
### 3.3 Host 侧需对齐项
56+
57+
- `bio_sendTransaction` 已按 raw 语义处理(现状)
58+
- `bio_destroyAsset` 对话框仍存在 formatted 假设(需要改为 raw 语义)
59+
- `bio_createTransaction` 当前存在“自动识别 raw/formatted”路径(应收敛为单一 raw 语义)
60+
61+
---
62+
63+
## 4. 升级计划(执行顺序)
64+
65+
### Phase 0:标准冻结(立即)
66+
67+
- 白皮书明确 raw-only 规则(本文)
68+
- 对外同步“禁止 formatted amount”
69+
70+
### Phase 1:Host 收敛(优先)
71+
72+
- `bio_destroyAsset` 切换为 raw 解析与展示
73+
- `bio_createTransaction` 去除双语义自动判断,统一 raw-only
74+
- 参数不合规时统一返回 `INVALID_PARAMS`
75+
76+
### Phase 2:内置应用对齐
77+
78+
- 升级 `xin.dweb.teleport`,确保传入 raw
79+
- 回归验证 `biobridge``rwahub` 多笔交易流程
80+
81+
### Phase 3:生态外部应用迁移
82+
83+
- 发布迁移窗口和截止版本
84+
- 要求每个应用提交“amount 字段转换点”自检清单
85+
- 逐步开启严格校验(不再接收 formatted)
86+
87+
---
88+
89+
## 5. 测试与验收基线
90+
91+
### 5.1 最小验收样例
92+
93+
- 输入 `amount="1000000000"`, `decimals=8`:展示 `10.00000000`
94+
- 输入 `amount="10.00000000"`:直接 `INVALID_PARAMS`(严格模式)
95+
- 广播上链金额必须保持 `raw=1000000000`
96+
97+
### 5.2 回归重点
98+
99+
- 多笔交易队列(FIFO,不互相覆盖)
100+
- 手势/支付密码步骤下金额不漂移
101+
- 广播失败时状态文案与实际阶段一致
102+
103+
---
104+
105+
## 6. 迁移沟通模板(摘要)
106+
107+
对生态方统一口径:
108+
109+
1. `amount` 改为 raw 整数字符串
110+
2. UI 层自行处理 formatted ↔ raw
111+
3. 不再依赖 Host 自动兼容 formatted
112+
113+
完整可执行提示词见:`/.chat/2026-02-12-miniapp-amount-raw-upgrade-plan.md`
114+

docs/white-book/11-DApp-Guide/02-Connectivity/README.md

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -13,19 +13,24 @@ The BioBridge Protocol is the communication layer between the Miniapp (running i
1313
```json
1414
{
1515
"id": "uuid-v4",
16-
"method": "wallet_requestAccounts",
16+
"method": "bio_requestAccounts",
1717
"params": [],
1818
"jsonrpc": "2.0"
1919
}
2020
```
2121

22-
### Supported Methods
22+
### Supported Methods (Core)
2323

24-
- `wallet_requestAccounts`: Request user address.
25-
- `wallet_sendTransaction`: Request transaction signing.
26-
- `wallet_signMessage`: Request message signing.
27-
- `kv_get`: Read from isolated storage.
28-
- `kv_set`: Write to isolated storage.
24+
- `bio_requestAccounts`: Request user address list.
25+
- `bio_createTransaction`: Build unsigned transaction.
26+
- `bio_signTransaction`: Sign unsigned transaction.
27+
- `bio_sendTransaction`: Request transfer authorization and broadcast.
28+
- `bio_destroyAsset`: Request destroy authorization.
29+
30+
### Amount Semantics Standard
31+
32+
- See: [`02-Amount-Semantics-Standard.md`](./02-Amount-Semantics-Standard.md)
33+
- Key rule: all transaction `amount` fields use raw integer string (minimum unit).
2934

3035
## Network Access
3136

docs/white-book/11-DApp-Guide/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,3 +13,4 @@
1313
- **02-Connectivity (连接性)**
1414
- [README.md](./02-Connectivity/README.md) - 连接性概述
1515
- [01-Bio-SDK-Communication.md](./02-Connectivity/01-Bio-SDK-Communication.md) - Bio-SDK 通讯机制
16+
- [02-Amount-Semantics-Standard.md](./02-Connectivity/02-Amount-Semantics-Standard.md) - 交易金额语义标准(Raw Amount)

miniapps/teleport/src/App.tsx

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -159,8 +159,20 @@ const CHAIN_COLORS: Record<string, string> = {
159159
const normalizeInternalChainName = (value: string): InternalChainName =>
160160
value.toUpperCase() as InternalChainName;
161161

162-
const normalizeInputAmount = (value: string) =>
163-
value.includes('.') ? value : `${value}.0`;
162+
const normalizeInputAmount = (value: string, decimals: number): string => {
163+
const normalized = value.trim();
164+
if (!/^\d+(\.\d+)?$/.test(normalized)) {
165+
throw new Error('Invalid amount format');
166+
}
167+
168+
const [intPart, fractionalPart = ''] = normalized.split('.');
169+
if (fractionalPart.length > decimals) {
170+
throw new Error('Amount precision exceeds decimals');
171+
}
172+
173+
const raw = `${intPart}${fractionalPart.padEnd(decimals, '0')}`.replace(/^0+(?=\d)/, '');
174+
return raw.length > 0 ? raw : '0';
175+
};
164176

165177
const formatMinAmount = (decimals: number) => {
166178
if (decimals <= 0) return '1';
@@ -334,7 +346,7 @@ export default function App() {
334346
{
335347
from: sourceAccount.address,
336348
to: selectedAsset.recipientAddress,
337-
amount: normalizeInputAmount(amount),
349+
amount: normalizeInputAmount(amount, selectedAsset.decimals),
338350
chain: sourceAccount.chain,
339351
asset: selectedAsset.assetType,
340352
...(remark ? { remark } : {}),

packages/bio-sdk/src/types.ts

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,21 @@ export interface BioAccount {
1212
publicKey: string
1313
}
1414

15+
/**
16+
* Raw amount string (minimum unit, integer only)
17+
*
18+
* Example:
19+
* - decimals = 8
20+
* - human amount 10.00000000 => raw amount "1000000000"
21+
*/
22+
export type RawAmountString = string
23+
1524
/** Transfer parameters */
1625
export interface TransferParams {
1726
from: string
1827
to: string
19-
amount: string
28+
/** amount uses raw minimum-unit integer string */
29+
amount: RawAmountString
2030
chain: string
2131
asset?: string
2232
}
@@ -94,7 +104,12 @@ export interface BioMethods {
94104
/** Sign an unsigned transaction (requires user confirmation) */
95105
bio_signTransaction: (params: { from: string; chain: string; unsignedTx: BioUnsignedTransaction }) => Promise<BioSignedTransaction>
96106

97-
/** Send a transaction */
107+
/**
108+
* Send a transaction
109+
*
110+
* Note:
111+
* - params.amount must be raw integer string (minimum unit)
112+
*/
98113
bio_sendTransaction: (params: TransferParams) => Promise<{ txHash: string }>
99114

100115
/** Get current chain ID */
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
import { beforeEach, describe, expect, it, vi } from 'vitest';
2+
3+
import { BioErrorCodes } from '../types';
4+
import { handleDestroyAsset, setDestroyDialog } from '../handlers/destroy';
5+
6+
describe('handleDestroyAsset amount semantics', () => {
7+
const context = {
8+
appId: 'test-miniapp',
9+
appName: 'Test Miniapp',
10+
appIcon: 'https://miniapp.example/icon.png',
11+
origin: 'https://test.app',
12+
permissions: ['bio_destroyAsset'],
13+
};
14+
15+
beforeEach(() => {
16+
setDestroyDialog(null);
17+
});
18+
19+
it('rejects formatted amount before opening dialog', async () => {
20+
const dialog = vi.fn(async () => ({ txHash: 'tx-hash' }));
21+
setDestroyDialog(dialog);
22+
23+
await expect(
24+
handleDestroyAsset(
25+
{
26+
from: 'b_sender',
27+
amount: '10.00000000',
28+
chain: 'bfmeta',
29+
asset: 'BFM',
30+
},
31+
context,
32+
),
33+
).rejects.toMatchObject({
34+
code: BioErrorCodes.INVALID_PARAMS,
35+
message: 'Invalid amount: expected raw integer string',
36+
});
37+
38+
expect(dialog).not.toHaveBeenCalled();
39+
});
40+
41+
it('accepts raw amount and opens dialog', async () => {
42+
const dialog = vi.fn(async () => ({ txHash: 'tx-hash' }));
43+
setDestroyDialog(dialog);
44+
45+
const result = await handleDestroyAsset(
46+
{
47+
from: 'b_sender',
48+
amount: '1000000000',
49+
chain: 'bfmeta',
50+
asset: 'BFM',
51+
},
52+
context,
53+
);
54+
55+
expect(result).toEqual({ txHash: 'tx-hash' });
56+
expect(dialog).toHaveBeenCalledWith(
57+
expect.objectContaining({
58+
amount: '1000000000',
59+
}),
60+
);
61+
});
62+
});
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import { describe, expect, it } from 'vitest';
2+
3+
import { isRawAmountString, parseRawAmount } from '../raw-amount';
4+
5+
describe('raw amount helpers', () => {
6+
it('accepts integer raw amount string', () => {
7+
expect(isRawAmountString('1000000000')).toBe(true);
8+
expect(isRawAmountString(' 1000000000 ')).toBe(true);
9+
});
10+
11+
it('rejects formatted and non-decimal values', () => {
12+
expect(isRawAmountString('10.00000000')).toBe(false);
13+
expect(isRawAmountString('-100')).toBe(false);
14+
expect(isRawAmountString('1e9')).toBe(false);
15+
expect(isRawAmountString('0x10')).toBe(false);
16+
});
17+
18+
it('parses raw amount with decimals correctly', () => {
19+
const amount = parseRawAmount('1000000000', 8, 'USDT');
20+
expect(amount.toRawString()).toBe('1000000000');
21+
expect(amount.toFormatted({ trimTrailingZeros: false })).toBe('10.00000000');
22+
});
23+
24+
it('throws for non-raw amount', () => {
25+
expect(() => parseRawAmount('10.00000000', 8, 'USDT')).toThrow('Invalid raw amount');
26+
});
27+
});

0 commit comments

Comments
 (0)