diff --git a/docs/white-book/06-Service-Ref/03-Chain/05-BioForest.md b/docs/white-book/06-Service-Ref/03-Chain/05-BioForest.md index 6821f3952..bb6ee2677 100644 --- a/docs/white-book/06-Service-Ref/03-Chain/05-BioForest.md +++ b/docs/white-book/06-Service-Ref/03-Chain/05-BioForest.md @@ -32,7 +32,7 @@ chain-adapter/bioforest/ | 地址格式 | `b...` / `BFM...` | `0x...` | `bc1...` | `T...` | | HD 派生 | 无 (单密钥) | BIP44 | BIP84 | BIP44 | | 代币标准 | AST | ERC-20 | - | TRC-20 | -| 手续费 | 固定费率 | Gas | 费率 | 能量/带宽 | +| 手续费 | 动态计算 | Gas | 费率 | 能量/带宽 | --- @@ -145,19 +145,55 @@ type BioforestTransactionType = ### 手续费模型 -BioForest 使用 **固定费率** 模型: +BioForest 使用 **动态计算** 模型,手续费根据交易体自动计算: + +#### 影响手续费的因素 + +| 因素 | 说明 | +|------|------| +| **交易类型** | 转账、设置支付密码等不同类型费用不同 | +| **支付密码** | 若发送方已设置支付密码,交易需二次签名,体积增大 | +| **备注 (remark)** | 备注内容越多,交易体积越大 | +| **转账金额** | 影响交易序列化后的字节数 | + +#### SDK 提供的手续费计算函数 ```typescript -async estimateFee(params: TransferParams): Promise { - // 固定手续费 - 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)) } ``` diff --git a/src/services/bioforest-sdk/index.ts b/src/services/bioforest-sdk/index.ts index 76f351b9b..92da8cbda 100644 --- a/src/services/bioforest-sdk/index.ts +++ b/src/services/bioforest-sdk/index.ts @@ -464,6 +464,120 @@ export async function verifyTwoStepSecret( return false } +// ===== Fee Estimation APIs ===== + +export type FeeIntent = + | { type: 'transfer'; amount: string; remark?: Record } + | { 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 { + 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, +): Promise { + 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 diff --git a/src/services/chain-adapter/bioforest/chain-service.ts b/src/services/chain-adapter/bioforest/chain-service.ts index 98394da30..77d38a8f8 100644 --- a/src/services/chain-adapter/bioforest/chain-service.ts +++ b/src/services/chain-adapter/bioforest/chain-service.ts @@ -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 @@ -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, + ) } } diff --git a/src/services/chain-adapter/bioforest/transaction-service.ts b/src/services/chain-adapter/bioforest/transaction-service.ts index fe44e7ea7..c88b78107 100644 --- a/src/services/chain-adapter/bioforest/transaction-service.ts +++ b/src/services/chain-adapter/bioforest/transaction-service.ts @@ -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 @@ -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) @@ -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, + ) } }