fix(transfer): MAX button now deducts gas fee from balance
Both Electron and Android apps now calculate the maximum transferable amount by subtracting estimated gas fees from the balance: Electron (Home.tsx): - Added calculateMaxAmount() async function that fetches gas price - Uses 21000 gas limit for simple transfers - Shows loading state while calculating Android (TransferScreen.kt): - Added calculateMaxTransferAmount() in TransactionUtils - Uses coroutine to fetch gas price asynchronously - Shows "..." while calculating, falls back to balance on error Both implementations: - Add 10% buffer to gas price for safety - Round down to 6 decimal places - Show error if balance insufficient for gas 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
7f66ed0ebe
commit
fd5f4d10ed
|
|
@ -195,6 +195,7 @@ fun TssPartyApp(
|
|||
isLoading = uiState.isLoading,
|
||||
error = uiState.error,
|
||||
networkType = settings.networkType,
|
||||
rpcUrl = settings.kavaRpcUrl,
|
||||
onPrepareTransaction = { toAddress, amount ->
|
||||
viewModel.prepareTransfer(shareId, toAddress, amount)
|
||||
},
|
||||
|
|
|
|||
|
|
@ -26,6 +26,7 @@ import com.durian.tssparty.domain.model.NetworkType
|
|||
import com.durian.tssparty.domain.model.SessionStatus
|
||||
import com.durian.tssparty.domain.model.ShareRecord
|
||||
import com.durian.tssparty.util.TransactionUtils
|
||||
import kotlinx.coroutines.launch
|
||||
import java.math.BigInteger
|
||||
|
||||
/**
|
||||
|
|
@ -57,6 +58,7 @@ fun TransferScreen(
|
|||
isLoading: Boolean = false,
|
||||
error: String? = null,
|
||||
networkType: NetworkType = NetworkType.MAINNET,
|
||||
rpcUrl: String = "https://evm.kava.io",
|
||||
onPrepareTransaction: (toAddress: String, amount: String) -> Unit,
|
||||
onConfirmTransaction: (password: String) -> Unit,
|
||||
onCopyInviteCode: () -> Unit,
|
||||
|
|
@ -115,6 +117,7 @@ fun TransferScreen(
|
|||
amount = amount,
|
||||
onAmountChange = { amount = it },
|
||||
error = validationError ?: error,
|
||||
rpcUrl = rpcUrl,
|
||||
onSubmit = {
|
||||
when {
|
||||
toAddress.isBlank() -> validationError = "请输入收款地址"
|
||||
|
|
@ -196,9 +199,12 @@ private fun TransferInputScreen(
|
|||
amount: String,
|
||||
onAmountChange: (String) -> Unit,
|
||||
error: String?,
|
||||
rpcUrl: String,
|
||||
onSubmit: () -> Unit,
|
||||
onCancel: () -> Unit
|
||||
) {
|
||||
val scope = rememberCoroutineScope()
|
||||
var isCalculatingMax by remember { mutableStateOf(false) }
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
|
|
@ -275,9 +281,25 @@ private fun TransferInputScreen(
|
|||
trailingIcon = {
|
||||
if (balance != null) {
|
||||
TextButton(
|
||||
onClick = { onAmountChange(balance) }
|
||||
onClick = {
|
||||
scope.launch {
|
||||
isCalculatingMax = true
|
||||
val result = TransactionUtils.calculateMaxTransferAmount(balance, rpcUrl)
|
||||
result.onSuccess { maxAmount ->
|
||||
onAmountChange(maxAmount)
|
||||
}.onFailure {
|
||||
// Fallback to balance if calculation fails
|
||||
onAmountChange(balance)
|
||||
}
|
||||
isCalculatingMax = false
|
||||
}
|
||||
},
|
||||
enabled = !isCalculatingMax
|
||||
) {
|
||||
Text("全部", style = MaterialTheme.typography.labelSmall)
|
||||
Text(
|
||||
if (isCalculatingMax) "..." else "全部",
|
||||
style = MaterialTheme.typography.labelSmall
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -356,6 +356,64 @@ object TransactionUtils {
|
|||
return gweiDecimal.toPlainString()
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate maximum transferable amount after deducting gas fee
|
||||
* @param balance Current balance in KAVA
|
||||
* @param rpcUrl RPC endpoint URL
|
||||
* @return Maximum amount in KAVA string, or "0" if insufficient balance
|
||||
*/
|
||||
suspend fun calculateMaxTransferAmount(balance: String, rpcUrl: String): Result<String> = withContext(Dispatchers.IO) {
|
||||
try {
|
||||
val balanceKava = BigDecimal(balance)
|
||||
if (balanceKava <= BigDecimal.ZERO) {
|
||||
return@withContext Result.success("0")
|
||||
}
|
||||
|
||||
// Get current gas price
|
||||
val gasPriceResult = getGasPrice(rpcUrl)
|
||||
val gasPrice = gasPriceResult.getOrElse {
|
||||
// Default to 1 gwei if failed
|
||||
BigInteger.valueOf(1000000000)
|
||||
}
|
||||
|
||||
// Add 10% buffer to gas price
|
||||
val gasPriceWithBuffer = gasPrice.multiply(BigInteger.valueOf(110)).divide(BigInteger.valueOf(100))
|
||||
|
||||
// Simple transfer gas limit is 21000
|
||||
val gasLimit = BigInteger.valueOf(21000)
|
||||
val gasFee = gasPriceWithBuffer.multiply(gasLimit)
|
||||
|
||||
// Convert gas fee to KAVA
|
||||
val gasFeeKava = BigDecimal(gasFee).divide(BigDecimal("1000000000000000000"), 8, java.math.RoundingMode.UP)
|
||||
|
||||
// Calculate max amount = balance - gas fee
|
||||
val maxAmount = balanceKava.subtract(gasFeeKava)
|
||||
|
||||
if (maxAmount <= BigDecimal.ZERO) {
|
||||
return@withContext Result.success("0")
|
||||
}
|
||||
|
||||
// Round down to 6 decimal places
|
||||
val formattedMax = maxAmount.setScale(6, java.math.RoundingMode.DOWN).stripTrailingZeros().toPlainString()
|
||||
Result.success(formattedMax)
|
||||
} catch (e: Exception) {
|
||||
// Fallback: use default gas estimate (21000 * 1 gwei = 0.000021 KAVA)
|
||||
try {
|
||||
val balanceKava = BigDecimal(balance)
|
||||
val defaultGasFee = BigDecimal("0.000021")
|
||||
val maxAmount = balanceKava.subtract(defaultGasFee)
|
||||
if (maxAmount <= BigDecimal.ZERO) {
|
||||
Result.success("0")
|
||||
} else {
|
||||
val formattedMax = maxAmount.setScale(6, java.math.RoundingMode.DOWN).stripTrailingZeros().toPlainString()
|
||||
Result.success(formattedMax)
|
||||
}
|
||||
} catch (e2: Exception) {
|
||||
Result.failure(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun keccak256(data: ByteArray): ByteArray {
|
||||
val keccak = Keccak.Digest256()
|
||||
return keccak.digest(data)
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import {
|
|||
isValidAmount,
|
||||
getCurrentNetwork,
|
||||
getCurrentRpcUrl,
|
||||
getGasPrice,
|
||||
type PreparedTransaction,
|
||||
} from '../utils/transaction';
|
||||
|
||||
|
|
@ -80,6 +81,52 @@ export default function Home() {
|
|||
const [transferStep, setTransferStep] = useState<'input' | 'confirm' | 'preparing' | 'error'>('input');
|
||||
const [transferError, setTransferError] = useState<string | null>(null);
|
||||
const [preparedTx, setPreparedTx] = useState<PreparedTransaction | null>(null);
|
||||
const [isCalculatingMax, setIsCalculatingMax] = useState(false);
|
||||
|
||||
// 计算扣除 Gas 费后的最大可转账金额
|
||||
const calculateMaxAmount = async () => {
|
||||
if (!transferShare?.kavaBalance || !transferShare?.evmAddress) return;
|
||||
|
||||
setIsCalculatingMax(true);
|
||||
try {
|
||||
const balance = parseFloat(transferShare.kavaBalance);
|
||||
if (balance <= 0) {
|
||||
setTransferAmount('0');
|
||||
return;
|
||||
}
|
||||
|
||||
// 获取当前 gas 价格
|
||||
const { maxFeePerGas } = await getGasPrice();
|
||||
// 简单转账的 gas 限制是 21000
|
||||
const gasLimit = BigInt(21000);
|
||||
const gasFee = maxFeePerGas * gasLimit;
|
||||
// 转换为 KAVA (18 位小数)
|
||||
const gasFeeKava = Number(gasFee) / 1e18;
|
||||
|
||||
// 计算最大可转账金额 = 余额 - Gas费
|
||||
const maxAmount = balance - gasFeeKava;
|
||||
|
||||
if (maxAmount <= 0) {
|
||||
setTransferError('余额不足以支付 Gas 费');
|
||||
setTransferAmount('0');
|
||||
} else {
|
||||
// 保留 6 位小数,向下取整避免精度问题
|
||||
const formattedMax = Math.floor(maxAmount * 1000000) / 1000000;
|
||||
setTransferAmount(formattedMax.toString());
|
||||
setTransferError(null);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to calculate max amount:', error);
|
||||
// 如果获取 Gas 失败,使用默认估算 (1 gwei * 21000)
|
||||
const defaultGasFee = 0.000021; // ~21000 * 1 gwei
|
||||
const balance = parseFloat(transferShare.kavaBalance);
|
||||
const maxAmount = Math.max(0, balance - defaultGasFee);
|
||||
const formattedMax = Math.floor(maxAmount * 1000000) / 1000000;
|
||||
setTransferAmount(formattedMax.toString());
|
||||
} finally {
|
||||
setIsCalculatingMax(false);
|
||||
}
|
||||
};
|
||||
|
||||
const deriveAddresses = useCallback(async (shareList: ShareItem[]): Promise<ShareWithAddress[]> => {
|
||||
const sharesWithAddresses: ShareWithAddress[] = [];
|
||||
|
|
@ -519,9 +566,10 @@ export default function Home() {
|
|||
/>
|
||||
<button
|
||||
className={styles.maxButton}
|
||||
onClick={() => setTransferAmount(transferShare.kavaBalance || '0')}
|
||||
onClick={calculateMaxAmount}
|
||||
disabled={isCalculatingMax}
|
||||
>
|
||||
MAX
|
||||
{isCalculatingMax ? '...' : 'MAX'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
Loading…
Reference in New Issue