Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -177,10 +177,16 @@ import { bio } from '@bioforest/bio-sdk';
// 请求钱包地址
const accounts = await bio.wallet.requestAccounts();

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

// 键值存储
Expand Down Expand Up @@ -259,6 +265,12 @@ class BioSDK {
}
```

## 金额语义(重要)

- 所有交易相关接口(`bio_sendTransaction` / `bio_createTransaction` / `bio_destroyAsset`)的 `amount` 均使用 **raw 最小单位整数字符串**。
- 禁止传入格式化小数字符串(例如 `10.00000000`)。
- 详见:[`02-Amount-Semantics-Standard.md`](./02-Amount-Semantics-Standard.md)

## 安全考虑

1. **来源验证**: Host 端应验证 `event.source` 确保消息来自预期的 iframe
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
# 02. MiniApp 交易金额语义标准(Raw Amount Standard)

> Last Updated: 2026-02-12
> Status: Active(生态升级基线)

本文定义 MiniApp 与 KeyApp 交互中所有“链上金额字段”的统一语义,避免“展示金额与真实广播金额不一致”的高风险问题。

---

## 1. 统一规则(必须遵守)

### 1.1 金额字段一律使用 raw(最小单位整数)

- `amount` 必须是 **十进制整数字符串**(`^\d+$`)
- 表示链最小单位(如 USDT 8 位精度时,`1000000000` 表示 `10.00000000`)
- 禁止传入格式化小数字符串(例如 `10.00000000`)

### 1.2 展示与签名/广播职责分离

- 展示层:根据 `decimals` 把 raw 转为人类可读金额
- 交易层:签名与广播始终使用 raw
- 严禁在显示修复时改动实际广播语义

### 1.3 适用接口(第一批)

- `bio_sendTransaction.params.amount`
- `bio_createTransaction.params.amount`
- `bio_destroyAsset.params.amount`

---

## 2. 风险背景(为什么必须统一)

当调用方把 raw 当 formatted,或 Host 把 formatted 当 raw,会导致:

- 面板显示金额被放大/缩小(误导用户)
- 签名/广播金额与用户预期不一致
- 后端入库与链上数据出现语义冲突(含重复提交与对账困难)

这是高风险交易语义问题,不是纯 UI 问题。

---

## 3. 当前审计结论(KeyApp 内置应用)

### 3.1 已符合或基本符合

- `xin.dweb.rwahub`:调用 `bio_sendTransaction` 前使用整数运算构造 raw(BigInt 路径)
- `xin.dweb.biobridge`(forge/redemption 主路径):在有精度上下文时将用户输入转换为 raw 再调用

### 3.2 需升级

- `xin.dweb.teleport`:当前 `bio_createTransaction` 调用路径仍可能传格式化小数字符串(示例:补 `.0`)

### 3.3 Host 侧需对齐项

- `bio_sendTransaction` 已按 raw 语义处理(现状)
- `bio_destroyAsset` 对话框仍存在 formatted 假设(需要改为 raw 语义)
- `bio_createTransaction` 当前存在“自动识别 raw/formatted”路径(应收敛为单一 raw 语义)

---

## 4. 升级计划(执行顺序)

### Phase 0:标准冻结(立即)

- 白皮书明确 raw-only 规则(本文)
- 对外同步“禁止 formatted amount”

### Phase 1:Host 收敛(优先)

- `bio_destroyAsset` 切换为 raw 解析与展示
- `bio_createTransaction` 去除双语义自动判断,统一 raw-only
- 参数不合规时统一返回 `INVALID_PARAMS`

### Phase 2:内置应用对齐

- 升级 `xin.dweb.teleport`,确保传入 raw
- 回归验证 `biobridge`、`rwahub` 多笔交易流程

### Phase 3:生态外部应用迁移

- 发布迁移窗口和截止版本
- 要求每个应用提交“amount 字段转换点”自检清单
- 逐步开启严格校验(不再接收 formatted)

---

## 5. 测试与验收基线

### 5.1 最小验收样例

- 输入 `amount="1000000000"`, `decimals=8`:展示 `10.00000000`
- 输入 `amount="10.00000000"`:直接 `INVALID_PARAMS`(严格模式)
- 广播上链金额必须保持 `raw=1000000000`

### 5.2 回归重点

- 多笔交易队列(FIFO,不互相覆盖)
- 手势/支付密码步骤下金额不漂移
- 广播失败时状态文案与实际阶段一致

---

## 6. 迁移沟通模板(摘要)

对生态方统一口径:

1. `amount` 改为 raw 整数字符串
2. UI 层自行处理 formatted ↔ raw
3. 不再依赖 Host 自动兼容 formatted

完整可执行提示词见:`/.chat/2026-02-12-miniapp-amount-raw-upgrade-plan.md`

19 changes: 12 additions & 7 deletions docs/white-book/11-DApp-Guide/02-Connectivity/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,19 +13,24 @@ The BioBridge Protocol is the communication layer between the Miniapp (running i
```json
{
"id": "uuid-v4",
"method": "wallet_requestAccounts",
"method": "bio_requestAccounts",
"params": [],
"jsonrpc": "2.0"
}
```

### Supported Methods
### Supported Methods (Core)

- `wallet_requestAccounts`: Request user address.
- `wallet_sendTransaction`: Request transaction signing.
- `wallet_signMessage`: Request message signing.
- `kv_get`: Read from isolated storage.
- `kv_set`: Write to isolated storage.
- `bio_requestAccounts`: Request user address list.
- `bio_createTransaction`: Build unsigned transaction.
- `bio_signTransaction`: Sign unsigned transaction.
- `bio_sendTransaction`: Request transfer authorization and broadcast.
- `bio_destroyAsset`: Request destroy authorization.

### Amount Semantics Standard

- See: [`02-Amount-Semantics-Standard.md`](./02-Amount-Semantics-Standard.md)
- Key rule: all transaction `amount` fields use raw integer string (minimum unit).

## Network Access

Expand Down
1 change: 1 addition & 0 deletions docs/white-book/11-DApp-Guide/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,4 @@
- **02-Connectivity (连接性)**
- [README.md](./02-Connectivity/README.md) - 连接性概述
- [01-Bio-SDK-Communication.md](./02-Connectivity/01-Bio-SDK-Communication.md) - Bio-SDK 通讯机制
- [02-Amount-Semantics-Standard.md](./02-Connectivity/02-Amount-Semantics-Standard.md) - 交易金额语义标准(Raw Amount)
18 changes: 15 additions & 3 deletions miniapps/teleport/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -159,8 +159,20 @@ const CHAIN_COLORS: Record<string, string> = {
const normalizeInternalChainName = (value: string): InternalChainName =>
value.toUpperCase() as InternalChainName;

const normalizeInputAmount = (value: string) =>
value.includes('.') ? value : `${value}.0`;
const normalizeInputAmount = (value: string, decimals: number): string => {
const normalized = value.trim();
if (!/^\d+(\.\d+)?$/.test(normalized)) {
throw new Error('Invalid amount format');
}

const [intPart, fractionalPart = ''] = normalized.split('.');
if (fractionalPart.length > decimals) {
throw new Error('Amount precision exceeds decimals');
}

const raw = `${intPart}${fractionalPart.padEnd(decimals, '0')}`.replace(/^0+(?=\d)/, '');
return raw.length > 0 ? raw : '0';
};

const formatMinAmount = (decimals: number) => {
if (decimals <= 0) return '1';
Expand Down Expand Up @@ -334,7 +346,7 @@ export default function App() {
{
from: sourceAccount.address,
to: selectedAsset.recipientAddress,
amount: normalizeInputAmount(amount),
amount: normalizeInputAmount(amount, selectedAsset.decimals),
chain: sourceAccount.chain,
asset: selectedAsset.assetType,
...(remark ? { remark } : {}),
Expand Down
19 changes: 17 additions & 2 deletions packages/bio-sdk/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,21 @@ export interface BioAccount {
publicKey: string
}

/**
* Raw amount string (minimum unit, integer only)
*
* Example:
* - decimals = 8
* - human amount 10.00000000 => raw amount "1000000000"
*/
export type RawAmountString = string

/** Transfer parameters */
export interface TransferParams {
from: string
to: string
amount: string
/** amount uses raw minimum-unit integer string */
amount: RawAmountString
chain: string
asset?: string
}
Expand Down Expand Up @@ -94,7 +104,12 @@ export interface BioMethods {
/** Sign an unsigned transaction (requires user confirmation) */
bio_signTransaction: (params: { from: string; chain: string; unsignedTx: BioUnsignedTransaction }) => Promise<BioSignedTransaction>

/** Send a transaction */
/**
* Send a transaction
*
* Note:
* - params.amount must be raw integer string (minimum unit)
*/
bio_sendTransaction: (params: TransferParams) => Promise<{ txHash: string }>

/** Get current chain ID */
Expand Down
62 changes: 62 additions & 0 deletions src/services/ecosystem/__tests__/destroy-handler.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import { beforeEach, describe, expect, it, vi } from 'vitest';

import { BioErrorCodes } from '../types';
import { handleDestroyAsset, setDestroyDialog } from '../handlers/destroy';

describe('handleDestroyAsset amount semantics', () => {
const context = {
appId: 'test-miniapp',
appName: 'Test Miniapp',
appIcon: 'https://miniapp.example/icon.png',
origin: 'https://test.app',
permissions: ['bio_destroyAsset'],
};

beforeEach(() => {
setDestroyDialog(null);
});

it('rejects formatted amount before opening dialog', async () => {
const dialog = vi.fn(async () => ({ txHash: 'tx-hash' }));
setDestroyDialog(dialog);

await expect(
handleDestroyAsset(
{
from: 'b_sender',
amount: '10.00000000',
chain: 'bfmeta',
asset: 'BFM',
},
context,
),
).rejects.toMatchObject({
code: BioErrorCodes.INVALID_PARAMS,
message: 'Invalid amount: expected raw integer string',
});

expect(dialog).not.toHaveBeenCalled();
});

it('accepts raw amount and opens dialog', async () => {
const dialog = vi.fn(async () => ({ txHash: 'tx-hash' }));
setDestroyDialog(dialog);

const result = await handleDestroyAsset(
{
from: 'b_sender',
amount: '1000000000',
chain: 'bfmeta',
asset: 'BFM',
},
context,
);

expect(result).toEqual({ txHash: 'tx-hash' });
expect(dialog).toHaveBeenCalledWith(
expect.objectContaining({
amount: '1000000000',
}),
);
});
});
27 changes: 27 additions & 0 deletions src/services/ecosystem/__tests__/raw-amount.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { describe, expect, it } from 'vitest';

import { isRawAmountString, parseRawAmount } from '../raw-amount';

describe('raw amount helpers', () => {
it('accepts integer raw amount string', () => {
expect(isRawAmountString('1000000000')).toBe(true);
expect(isRawAmountString(' 1000000000 ')).toBe(true);
});

it('rejects formatted and non-decimal values', () => {
expect(isRawAmountString('10.00000000')).toBe(false);
expect(isRawAmountString('-100')).toBe(false);
expect(isRawAmountString('1e9')).toBe(false);
expect(isRawAmountString('0x10')).toBe(false);
});

it('parses raw amount with decimals correctly', () => {
const amount = parseRawAmount('1000000000', 8, 'USDT');
expect(amount.toRawString()).toBe('1000000000');
expect(amount.toFormatted({ trimTrailingZeros: false })).toBe('10.00000000');
});

it('throws for non-raw amount', () => {
expect(() => parseRawAmount('10.00000000', 8, 'USDT')).toThrow('Invalid raw amount');
});
});
Loading