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
60 changes: 48 additions & 12 deletions docs/white-book/06-Service-Ref/03-Chain/05-BioForest.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ chain-adapter/bioforest/
| 地址格式 | `b...` / `BFM...` | `0x...` | `bc1...` | `T...` |
| HD 派生 | 无 (单密钥) | BIP44 | BIP84 | BIP44 |
| 代币标准 | AST | ERC-20 | - | TRC-20 |
| 手续费 | 固定费率 | Gas | 费率 | 能量/带宽 |
| 手续费 | 动态计算 | Gas | 费率 | 能量/带宽 |

---

Expand Down Expand Up @@ -145,19 +145,55 @@ type BioforestTransactionType =

### 手续费模型

BioForest 使用 **固定费率** 模型:
BioForest 使用 **动态计算** 模型,手续费根据交易体自动计算:

#### 影响手续费的因素

| 因素 | 说明 |
|------|------|
| **交易类型** | 转账、设置支付密码等不同类型费用不同 |
| **支付密码** | 若发送方已设置支付密码,交易需二次签名,体积增大 |
| **备注 (remark)** | 备注内容越多,交易体积越大 |
| **转账金额** | 影响交易序列化后的字节数 |

#### SDK 提供的手续费计算函数

```typescript
async estimateFee(params: TransferParams): Promise<FeeEstimate> {
// 固定手续费
const fixedFee = await this.api.getTransactionFee();

const fee: Fee = {
amount: Amount.fromRaw(fixedFee, this.config.decimals, this.config.symbol),
estimatedTime: 3, // 约 3 秒
};

return { slow: fee, standard: fee, fast: fee };
import { getMinFee, getTransferMinFee } from '@/services/bioforest-sdk'

// 方式 1: 通用接口,根据意图计算
const fee = await getMinFee({
baseUrl,
chainId,
intent: { type: 'transfer', amount: '100000000', remark: {} },
fromAddress, // 传入发送方地址,自动检查是否有支付密码
})

// 方式 2: 设置支付密码的手续费
const fee = await getMinFee({
baseUrl,
chainId,
intent: { type: 'setPayPassword' },
})

// 方式 3: 快捷函数
const fee = await getTransferMinFee(baseUrl, chainId, fromAddress, amount, remark)
```

#### 实现原理

SDK 内部通过模拟创建交易来计算最低手续费:

```typescript
// 核心调用
const minFee = await core.transactionController.getTransferTransactionMinFee({
transaction: { applyBlockHeight, timestamp, remark },
assetInfo: { sourceChainName, sourceChainMagic, assetType, amount }
})

// 若发送方有支付密码,增加 5% 缓冲 (signSignature 额外开销)
if (hasPayPassword) {
minFee = String(BigInt(minFee) * BigInt(105) / BigInt(100))
}
```

Expand Down
114 changes: 114 additions & 0 deletions src/services/bioforest-sdk/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -464,6 +464,120 @@ export async function verifyTwoStepSecret(
return false
}

// ===== Fee Estimation APIs =====

export type FeeIntent =
| { type: 'transfer'; amount: string; remark?: Record<string, string> }
| { type: 'setPayPassword' }

export interface GetMinFeeParams {
/** Full wallet API URL */
baseUrl: string
/** Chain ID */
chainId: string
/** Fee calculation intent */
intent: FeeIntent
/** Sender address (for checking if has pay password) */
fromAddress?: string
}

/**
* Get minimum fee based on transaction intent
*
* Fee calculation considers:
* - Transaction type (transfer, setPayPassword, etc.)
* - Whether sender has pay password set (affects transaction size)
* - Remark content size
* - Transfer amount
*
* @param params - Fee calculation parameters
* @returns Minimum fee in raw string format
*/
export async function getMinFee(params: GetMinFeeParams): Promise<string> {
const { baseUrl, chainId, intent, fromAddress } = params
const core = await getBioforestCore(chainId)
const lastBlock = await getLastBlock(baseUrl)
const applyBlockHeight = lastBlock.height
const timestamp = lastBlock.timestamp

switch (intent.type) {
case 'transfer': {
// Check if sender has pay password (secondPublicKey)
// This affects transaction size because it requires signSignature
let hasPayPassword = false
if (fromAddress) {
try {
const addressInfo = await getAddressInfo(baseUrl, fromAddress)
hasPayPassword = !!addressInfo.secondPublicKey
} catch {
// Ignore errors, assume no pay password
}
}

// Use a large amount to get maximum fee estimation
// The SDK calculates fee based on transaction bytes
const estimationAmount = intent.amount || '99999999999999999'

let minFee = await core.transactionController.getTransferTransactionMinFee({
transaction: {
applyBlockHeight,
timestamp,
remark: intent.remark ?? {},
},
assetInfo: {
sourceChainName: await core.getChainName(),
sourceChainMagic: await core.getMagic(),
assetType: await core.getAssetType(),
amount: estimationAmount,
},
})

// If has pay password, the transaction will have signSignature
// which adds ~64 bytes to the transaction, increasing fee
if (hasPayPassword) {
// Add ~5% buffer for signSignature overhead
minFee = String(BigInt(minFee) * BigInt(105) / BigInt(100))
}

return minFee
}

case 'setPayPassword': {
return core.transactionController.getSignatureTransactionMinFee({
newPaySecret: `${Date.now()}getSignatureTransactionMinFee`,
applyBlockHeight,
timestamp,
})
}

default:
throw new Error(`Unknown fee intent type: ${(intent as FeeIntent).type}`)
}
}

/**
* Get minimum fee for transfer transaction
* @param baseUrl - Full wallet API URL
* @param chainId - Chain ID
* @param fromAddress - Sender address (to check pay password)
* @param amount - Transfer amount
* @param remark - Transaction remark
*/
export async function getTransferMinFee(
baseUrl: string,
chainId: string,
fromAddress?: string,
amount?: string,
remark?: Record<string, string>,
): Promise<string> {
return getMinFee({
baseUrl,
chainId,
intent: { type: 'transfer', amount: amount ?? '0', remark },
fromAddress,
})
}

/**
* Get minimum fee for setting pay password
* @param baseUrl - Full wallet API URL
Expand Down
66 changes: 23 additions & 43 deletions src/services/chain-adapter/bioforest/chain-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@ import { chainConfigService } from '@/services/chain-config'
import { Amount } from '@/types/amount'
import type { IChainService, ChainInfo, GasPrice, HealthStatus } from '../types'
import { ChainServiceError, ChainErrorCodes } from '../types'
import type { BioforestBlockInfo, BioforestFeeInfo } from './types'
import type { BioforestBlockInfo } from './types'
import { getTransferMinFee } from '@/services/bioforest-sdk'

export class BioforestChainService implements IChainService {
private readonly chainId: string
Expand Down Expand Up @@ -89,59 +90,38 @@ export class BioforestChainService implements IChainService {
const { decimals, symbol } = config

if (!this.baseUrl) {
// Return default fees - BioForest minimum is around 500 (0.000005 BFM)
const defaultFee = Amount.fromRaw('1000', decimals, symbol) // 0.00001 BFM
return {
slow: defaultFee,
standard: defaultFee.mul(2),
fast: defaultFee.mul(5),
lastUpdated: Date.now(),
}
throw new ChainServiceError(
ChainErrorCodes.NETWORK_ERROR,
'RPC URL not configured for BioForest chain',
)
}

try {
const response = await fetch(`${this.baseUrl}/blockAveFee`, {
method: 'GET',
headers: { 'Content-Type': 'application/json' },
})

if (!response.ok) {
throw new ChainServiceError(
ChainErrorCodes.NETWORK_ERROR,
`Failed to fetch fee info: ${response.status}`,
)
}
// Use SDK to calculate minimum fee dynamically
// Note: This is a base fee without considering specific sender's pay password status
// For more accurate fee, use transaction-service.estimateFee() with sender address
const minFeeRaw = await getTransferMinFee(
this.baseUrl,
config.id,
undefined, // No specific sender - base fee calculation
)

const json = (await response.json()) as { success: boolean; result: BioforestFeeInfo }
if (!json.success) {
// Return default fees on API error - BioForest minimum is around 500
const defaultFee = Amount.fromRaw('1000', decimals, symbol)
return {
slow: defaultFee,
standard: defaultFee.mul(2),
fast: defaultFee.mul(5),
lastUpdated: Date.now(),
}
}
const minFee = Amount.fromRaw(json.result.minFee, decimals, symbol)
const avgFee = Amount.fromRaw(json.result.avgFee, decimals, symbol)
const minFee = Amount.fromRaw(minFeeRaw, decimals, symbol)

return {
slow: minFee,
standard: avgFee.gt(minFee) ? avgFee : minFee,
fast: avgFee.mul(2).gt(minFee) ? avgFee.mul(2) : minFee.mul(2),
standard: minFee,
fast: minFee.mul(2),
lastUpdated: Date.now(),
}
} catch (error) {
if (error instanceof ChainServiceError) throw error
// Return default on error - BioForest minimum is around 500
const defaultFee = Amount.fromRaw('1000', decimals, symbol)
return {
slow: defaultFee,
standard: defaultFee.mul(2),
fast: defaultFee.mul(5),
lastUpdated: Date.now(),
}
throw new ChainServiceError(
ChainErrorCodes.NETWORK_ERROR,
'Failed to calculate minimum fee from SDK',
undefined,
error instanceof Error ? error : undefined,
)
}
}

Expand Down
55 changes: 25 additions & 30 deletions src/services/chain-adapter/bioforest/transaction-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ import type {
import { ChainServiceError, ChainErrorCodes } from '../types'

import { signMessage, bytesToHex } from '@/lib/crypto'
import { getBioforestCore, getLastBlock } from '@/services/bioforest-sdk'
import { getTransferMinFee } from '@/services/bioforest-sdk'

export class BioforestTransactionService implements ITransactionService {
private readonly chainId: string
Expand Down Expand Up @@ -88,28 +88,25 @@ export class BioforestTransactionService implements ITransactionService {
estimatedTime: time,
})

try {
if (!this.baseUrl) {
throw new Error('No RPC URL configured')
}
if (!this.baseUrl) {
throw new ChainServiceError(
ChainErrorCodes.NETWORK_ERROR,
'RPC URL not configured for BioForest chain',
)
}

// Use SDK to calculate minimum fee (same as mpay)
const core = await getBioforestCore(config.id)
const lastBlock = await getLastBlock(this.baseUrl)

const minFeeRaw = await core.transactionController.getTransferTransactionMinFee({
transaction: {
applyBlockHeight: lastBlock.height,
timestamp: lastBlock.timestamp,
remark: {},
},
assetInfo: {
sourceChainName: await core.getChainName(),
sourceChainMagic: await core.getMagic(),
assetType: params.amount?.symbol ?? symbol,
amount: params.amount?.toRawString() ?? '0',
},
})
try {
// Use SDK to calculate minimum fee dynamically
// Considers:
// - Whether sender has pay password (secondPublicKey) - affects transaction size
// - Transaction amount and remark
const minFeeRaw = await getTransferMinFee(
this.baseUrl,
config.id,
params.from, // Pass sender address to check pay password status
params.amount?.toRawString(),
undefined, // remark - could be added to TransferParams if needed
)

const minFee = Amount.fromRaw(minFeeRaw, decimals, symbol)

Expand All @@ -119,14 +116,12 @@ export class BioforestTransactionService implements ITransactionService {
fast: createFee(minFee.mul(2), 5),
}
} catch (error) {
console.warn('[TransactionService] Failed to get min fee from SDK, using default:', error)
// Fallback to default - real minimum is around 500 (0.000005 BFM)
const defaultFee = Amount.fromRaw('1000', decimals, symbol)
return {
slow: createFee(defaultFee, 30),
standard: createFee(defaultFee, 15),
fast: createFee(defaultFee.mul(2), 5),
}
throw new ChainServiceError(
ChainErrorCodes.NETWORK_ERROR,
'Failed to calculate minimum fee from SDK',
undefined,
error instanceof Error ? error : undefined,
)
}
}

Expand Down