feat(token): add Green Points (绿积分) ERC-20 token support
Add support for the dUSDT token "绿积分" (Green Points) on both Android and Electron applications: Android changes: - Add TokenType enum and GreenPointsToken config in Models.kt - Implement ERC-20 balance fetching and transfer encoding in TssRepository - Update TransactionUtils with ERC-20 transfer support - Add dual balance display (KAVA + 绿积分) in WalletsScreen - Add token type selector in TransferScreen Electron changes: - Add TokenType and GREEN_POINTS_TOKEN config in transaction.ts - Implement fetchGreenPointsBalance and ERC-20 transfer encoding - Update Home.tsx with dual balance display and token selector - Add token selector styles in Home.module.css Token contract: 0xA9F3A35dBa8699c8C681D8db03F0c1A8CEB9D7c3 (Kava mainnet) 🤖 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
b3822e48eb
commit
9b9612bd5f
|
|
@ -17,6 +17,7 @@ import androidx.navigation.compose.composable
|
|||
import androidx.navigation.compose.currentBackStackEntryAsState
|
||||
import androidx.navigation.compose.rememberNavController
|
||||
import com.durian.tssparty.domain.model.AppReadyState
|
||||
import com.durian.tssparty.domain.model.TokenType
|
||||
import com.durian.tssparty.presentation.components.BottomNavItem
|
||||
import com.durian.tssparty.presentation.components.TssBottomNavigation
|
||||
import com.durian.tssparty.presentation.screens.*
|
||||
|
|
@ -63,6 +64,7 @@ fun TssPartyApp(
|
|||
val settings by viewModel.settings.collectAsState()
|
||||
val createdInviteCode by viewModel.createdInviteCode.collectAsState()
|
||||
val balances by viewModel.balances.collectAsState()
|
||||
val walletBalances by viewModel.walletBalances.collectAsState()
|
||||
val currentSessionId by viewModel.currentSessionId.collectAsState()
|
||||
val sessionParticipants by viewModel.sessionParticipants.collectAsState()
|
||||
val currentRound by viewModel.currentRound.collectAsState()
|
||||
|
|
@ -167,6 +169,7 @@ fun TssPartyApp(
|
|||
shares = shares,
|
||||
isConnected = uiState.isConnected,
|
||||
balances = balances,
|
||||
walletBalances = walletBalances,
|
||||
networkType = settings.networkType,
|
||||
onDeleteShare = { viewModel.deleteShare(it) },
|
||||
onRefreshBalance = { address -> viewModel.fetchBalance(address) },
|
||||
|
|
@ -189,6 +192,7 @@ fun TssPartyApp(
|
|||
TransferScreen(
|
||||
wallet = wallet,
|
||||
balance = balances[wallet.address],
|
||||
walletBalance = walletBalances[wallet.address],
|
||||
sessionStatus = sessionStatus,
|
||||
participants = signParticipants,
|
||||
currentRound = signCurrentRound,
|
||||
|
|
@ -202,8 +206,8 @@ fun TssPartyApp(
|
|||
error = uiState.error,
|
||||
networkType = settings.networkType,
|
||||
rpcUrl = settings.kavaRpcUrl,
|
||||
onPrepareTransaction = { toAddress, amount ->
|
||||
viewModel.prepareTransfer(shareId, toAddress, amount)
|
||||
onPrepareTransaction = { toAddress, amount, tokenType ->
|
||||
viewModel.prepareTransfer(shareId, toAddress, amount, tokenType)
|
||||
},
|
||||
onConfirmTransaction = {
|
||||
viewModel.initiateSignSession(shareId, "")
|
||||
|
|
|
|||
|
|
@ -1902,6 +1902,92 @@ class TssRepository @Inject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get Green Points (绿积分/dUSDT) token balance for an address
|
||||
* Uses eth_call to call balanceOf(address) on the ERC-20 contract
|
||||
*/
|
||||
suspend fun getGreenPointsBalance(address: String, rpcUrl: String): Result<String> {
|
||||
return withContext(Dispatchers.IO) {
|
||||
try {
|
||||
val client = okhttp3.OkHttpClient()
|
||||
val jsonMediaType = "application/json; charset=utf-8".toMediaType()
|
||||
|
||||
// Encode balanceOf(address) call data
|
||||
// Function selector: 0x70a08231
|
||||
// Address parameter: padded to 32 bytes
|
||||
val paddedAddress = address.removePrefix("0x").lowercase().padStart(64, '0')
|
||||
val callData = "${GreenPointsToken.BALANCE_OF_SELECTOR}$paddedAddress"
|
||||
|
||||
val requestBody = """
|
||||
{
|
||||
"jsonrpc": "2.0",
|
||||
"method": "eth_call",
|
||||
"params": [{
|
||||
"to": "${GreenPointsToken.CONTRACT_ADDRESS}",
|
||||
"data": "$callData"
|
||||
}, "latest"],
|
||||
"id": 1
|
||||
}
|
||||
""".trimIndent()
|
||||
|
||||
val request = okhttp3.Request.Builder()
|
||||
.url(rpcUrl)
|
||||
.post(requestBody.toRequestBody(jsonMediaType))
|
||||
.build()
|
||||
|
||||
val response = client.newCall(request).execute()
|
||||
val responseBody = response.body?.string() ?: return@withContext Result.failure(Exception("Empty response"))
|
||||
|
||||
// Parse JSON response
|
||||
val json = com.google.gson.JsonParser.parseString(responseBody).asJsonObject
|
||||
if (json.has("error")) {
|
||||
return@withContext Result.failure(Exception(json.get("error").asJsonObject.get("message").asString))
|
||||
}
|
||||
|
||||
val hexBalance = json.get("result").asString
|
||||
// Convert hex to decimal, then apply 6 decimals (dUSDT uses 6 decimals like USDT)
|
||||
val rawBalance = java.math.BigInteger(hexBalance.removePrefix("0x"), 16)
|
||||
val tokenBalance = java.math.BigDecimal(rawBalance).divide(
|
||||
java.math.BigDecimal("1000000"), // 10^6 for 6 decimals
|
||||
6,
|
||||
java.math.RoundingMode.DOWN
|
||||
)
|
||||
|
||||
Result.success(tokenBalance.toPlainString())
|
||||
} catch (e: Exception) {
|
||||
android.util.Log.e("TssRepository", "Failed to get Green Points balance: ${e.message}")
|
||||
Result.failure(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get both KAVA and Green Points balances for an address
|
||||
*/
|
||||
suspend fun getWalletBalance(address: String, rpcUrl: String): Result<WalletBalance> {
|
||||
return withContext(Dispatchers.IO) {
|
||||
try {
|
||||
// Fetch both balances in parallel
|
||||
val kavaDeferred = async { getBalance(address, rpcUrl) }
|
||||
val greenPointsDeferred = async { getGreenPointsBalance(address, rpcUrl) }
|
||||
|
||||
val kavaResult = kavaDeferred.await()
|
||||
val greenPointsResult = greenPointsDeferred.await()
|
||||
|
||||
val kavaBalance = kavaResult.getOrDefault("0")
|
||||
val greenPointsBalance = greenPointsResult.getOrDefault("0")
|
||||
|
||||
Result.success(WalletBalance(
|
||||
address = address,
|
||||
kavaBalance = kavaBalance,
|
||||
greenPointsBalance = greenPointsBalance
|
||||
))
|
||||
} catch (e: Exception) {
|
||||
Result.failure(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ========== Transfer / Sign Session Methods ==========
|
||||
|
||||
/**
|
||||
|
|
@ -1912,7 +1998,8 @@ class TssRepository @Inject constructor(
|
|||
to: String,
|
||||
amount: String,
|
||||
rpcUrl: String,
|
||||
chainId: Int = TransactionUtils.KAVA_TESTNET_CHAIN_ID
|
||||
chainId: Int = TransactionUtils.KAVA_TESTNET_CHAIN_ID,
|
||||
tokenType: TokenType = TokenType.KAVA
|
||||
): Result<TransactionUtils.PreparedTransaction> {
|
||||
return TransactionUtils.prepareTransaction(
|
||||
TransactionUtils.TransactionParams(
|
||||
|
|
@ -1920,7 +2007,8 @@ class TssRepository @Inject constructor(
|
|||
to = to,
|
||||
amount = amount,
|
||||
rpcUrl = rpcUrl,
|
||||
chainId = chainId
|
||||
chainId = chainId,
|
||||
tokenType = tokenType
|
||||
)
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -123,3 +123,38 @@ enum class NetworkType {
|
|||
MAINNET,
|
||||
TESTNET
|
||||
}
|
||||
|
||||
/**
|
||||
* Token type for transfers
|
||||
*/
|
||||
enum class TokenType {
|
||||
KAVA, // Native KAVA token
|
||||
GREEN_POINTS // 绿积分 (dUSDT) ERC-20 token
|
||||
}
|
||||
|
||||
/**
|
||||
* Green Points (绿积分) Token Contract Configuration
|
||||
* dUSDT - Fixed supply ERC-20 token on Kava EVM
|
||||
*/
|
||||
object GreenPointsToken {
|
||||
const val CONTRACT_ADDRESS = "0xA9F3A35dBa8699c8C681D8db03F0c1A8CEB9D7c3"
|
||||
const val NAME = "绿积分"
|
||||
const val SYMBOL = "dUSDT"
|
||||
const val DECIMALS = 6
|
||||
|
||||
// ERC-20 function signatures (first 4 bytes of keccak256 hash)
|
||||
const val BALANCE_OF_SELECTOR = "0x70a08231" // balanceOf(address)
|
||||
const val TRANSFER_SELECTOR = "0xa9059cbb" // transfer(address,uint256)
|
||||
const val APPROVE_SELECTOR = "0x095ea7b3" // approve(address,uint256)
|
||||
const val ALLOWANCE_SELECTOR = "0xdd62ed3e" // allowance(address,address)
|
||||
const val TOTAL_SUPPLY_SELECTOR = "0x18160ddd" // totalSupply()
|
||||
}
|
||||
|
||||
/**
|
||||
* Wallet balance containing both native and token balances
|
||||
*/
|
||||
data class WalletBalance(
|
||||
val address: String,
|
||||
val kavaBalance: String = "0", // Native KAVA balance
|
||||
val greenPointsBalance: String = "0" // 绿积分 (dUSDT) balance
|
||||
)
|
||||
|
|
|
|||
|
|
@ -9,25 +9,34 @@ import androidx.compose.material3.*
|
|||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.durian.tssparty.domain.model.GreenPointsToken
|
||||
import com.durian.tssparty.domain.model.ShareRecord
|
||||
import com.durian.tssparty.domain.model.WalletBalance
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun HomeScreen(
|
||||
shares: List<ShareRecord>,
|
||||
walletBalances: Map<String, WalletBalance>,
|
||||
isConnected: Boolean,
|
||||
onNavigateToJoin: () -> Unit,
|
||||
onNavigateToSign: (Long) -> Unit,
|
||||
onNavigateToSettings: () -> Unit,
|
||||
onDeleteShare: (Long) -> Unit
|
||||
onDeleteShare: (Long) -> Unit,
|
||||
onRefreshBalances: () -> Unit = {}
|
||||
) {
|
||||
Scaffold(
|
||||
topBar = {
|
||||
TopAppBar(
|
||||
title = { Text("TSS Party") },
|
||||
actions = {
|
||||
// Refresh button
|
||||
IconButton(onClick = onRefreshBalances) {
|
||||
Icon(Icons.Default.Refresh, contentDescription = "Refresh")
|
||||
}
|
||||
// Connection status indicator
|
||||
Icon(
|
||||
imageVector = if (isConnected) Icons.Default.CheckCircle else Icons.Default.Warning,
|
||||
|
|
@ -94,6 +103,7 @@ fun HomeScreen(
|
|||
items(shares) { share ->
|
||||
WalletCard(
|
||||
share = share,
|
||||
walletBalance = walletBalances[share.address],
|
||||
onSign = { onNavigateToSign(share.id) },
|
||||
onDelete = { onDeleteShare(share.id) }
|
||||
)
|
||||
|
|
@ -107,6 +117,7 @@ fun HomeScreen(
|
|||
@Composable
|
||||
fun WalletCard(
|
||||
share: ShareRecord,
|
||||
walletBalance: WalletBalance?,
|
||||
onSign: () -> Unit,
|
||||
onDelete: () -> Unit
|
||||
) {
|
||||
|
|
@ -138,6 +149,42 @@ fun WalletCard(
|
|||
}
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(12.dp))
|
||||
|
||||
// Balance section
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween
|
||||
) {
|
||||
// KAVA balance
|
||||
Column {
|
||||
Text(
|
||||
text = "KAVA",
|
||||
style = MaterialTheme.typography.labelSmall,
|
||||
color = MaterialTheme.colorScheme.outline
|
||||
)
|
||||
Text(
|
||||
text = walletBalance?.kavaBalance ?: "Loading...",
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
fontWeight = FontWeight.Medium
|
||||
)
|
||||
}
|
||||
// Green Points balance
|
||||
Column(horizontalAlignment = Alignment.End) {
|
||||
Text(
|
||||
text = GreenPointsToken.NAME,
|
||||
style = MaterialTheme.typography.labelSmall,
|
||||
color = MaterialTheme.colorScheme.outline
|
||||
)
|
||||
Text(
|
||||
text = walletBalance?.greenPointsBalance ?: "Loading...",
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
fontWeight = FontWeight.Medium,
|
||||
color = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
|
||||
Row(
|
||||
|
|
|
|||
|
|
@ -27,9 +27,12 @@ import android.graphics.Bitmap
|
|||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.ui.graphics.asImageBitmap
|
||||
import com.durian.tssparty.domain.model.GreenPointsToken
|
||||
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.domain.model.TokenType
|
||||
import com.durian.tssparty.domain.model.WalletBalance
|
||||
import com.durian.tssparty.util.TransactionUtils
|
||||
import com.google.zxing.BarcodeFormat
|
||||
import com.google.zxing.qrcode.QRCodeWriter
|
||||
|
|
@ -57,6 +60,7 @@ import java.math.BigInteger
|
|||
fun TransferScreen(
|
||||
wallet: ShareRecord,
|
||||
balance: String?,
|
||||
walletBalance: WalletBalance? = null,
|
||||
sessionStatus: SessionStatus,
|
||||
participants: List<String> = emptyList(),
|
||||
currentRound: Int = 0,
|
||||
|
|
@ -70,7 +74,7 @@ fun TransferScreen(
|
|||
error: String? = null,
|
||||
networkType: NetworkType = NetworkType.MAINNET,
|
||||
rpcUrl: String = "https://evm.kava.io",
|
||||
onPrepareTransaction: (toAddress: String, amount: String) -> Unit,
|
||||
onPrepareTransaction: (toAddress: String, amount: String, tokenType: TokenType) -> Unit,
|
||||
onConfirmTransaction: () -> Unit,
|
||||
onCopyInviteCode: () -> Unit,
|
||||
onBroadcastTransaction: () -> Unit,
|
||||
|
|
@ -79,6 +83,7 @@ fun TransferScreen(
|
|||
) {
|
||||
var toAddress by remember { mutableStateOf("") }
|
||||
var amount by remember { mutableStateOf("") }
|
||||
var selectedTokenType by remember { mutableStateOf(TokenType.KAVA) }
|
||||
var validationError by remember { mutableStateOf<String?>(null) }
|
||||
|
||||
// QR Scanner launcher for recipient address
|
||||
|
|
@ -140,6 +145,9 @@ fun TransferScreen(
|
|||
"input" -> TransferInputScreen(
|
||||
wallet = wallet,
|
||||
balance = balance,
|
||||
walletBalance = walletBalance,
|
||||
selectedTokenType = selectedTokenType,
|
||||
onTokenTypeChange = { selectedTokenType = it },
|
||||
toAddress = toAddress,
|
||||
onToAddressChange = { toAddress = it },
|
||||
amount = amount,
|
||||
|
|
@ -147,15 +155,20 @@ fun TransferScreen(
|
|||
error = validationError ?: error,
|
||||
rpcUrl = rpcUrl,
|
||||
onSubmit = {
|
||||
// Get current balance for the selected token type
|
||||
val currentBalance = when (selectedTokenType) {
|
||||
TokenType.KAVA -> walletBalance?.kavaBalance ?: balance
|
||||
TokenType.GREEN_POINTS -> walletBalance?.greenPointsBalance
|
||||
}
|
||||
when {
|
||||
toAddress.isBlank() -> validationError = "请输入收款地址"
|
||||
!toAddress.startsWith("0x") || toAddress.length != 42 -> validationError = "地址格式不正确"
|
||||
amount.isBlank() -> validationError = "请输入金额"
|
||||
amount.toDoubleOrNull() == null || amount.toDouble() <= 0 -> validationError = "金额无效"
|
||||
balance != null && amount.toDouble() > balance.toDouble() -> validationError = "余额不足"
|
||||
currentBalance != null && amount.toDouble() > currentBalance.toDouble() -> validationError = "余额不足"
|
||||
else -> {
|
||||
validationError = null
|
||||
onPrepareTransaction(toAddress.trim(), amount.trim())
|
||||
onPrepareTransaction(toAddress.trim(), amount.trim(), selectedTokenType)
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
@ -226,6 +239,9 @@ fun TransferScreen(
|
|||
private fun TransferInputScreen(
|
||||
wallet: ShareRecord,
|
||||
balance: String?,
|
||||
walletBalance: WalletBalance?,
|
||||
selectedTokenType: TokenType,
|
||||
onTokenTypeChange: (TokenType) -> Unit,
|
||||
toAddress: String,
|
||||
onToAddressChange: (String) -> Unit,
|
||||
amount: String,
|
||||
|
|
@ -238,6 +254,17 @@ private fun TransferInputScreen(
|
|||
) {
|
||||
val scope = rememberCoroutineScope()
|
||||
var isCalculatingMax by remember { mutableStateOf(false) }
|
||||
|
||||
// Get current balance for the selected token type
|
||||
val currentBalance = when (selectedTokenType) {
|
||||
TokenType.KAVA -> walletBalance?.kavaBalance ?: balance
|
||||
TokenType.GREEN_POINTS -> walletBalance?.greenPointsBalance
|
||||
}
|
||||
val tokenSymbol = when (selectedTokenType) {
|
||||
TokenType.KAVA -> "KAVA"
|
||||
TokenType.GREEN_POINTS -> GreenPointsToken.NAME
|
||||
}
|
||||
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
|
|
@ -264,25 +291,96 @@ private fun TransferInputScreen(
|
|||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
|
||||
// Show both balances
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween
|
||||
) {
|
||||
Text(
|
||||
text = "余额: ",
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
Text(
|
||||
text = if (balance != null) "$balance KAVA" else "加载中...",
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
fontWeight = FontWeight.Medium,
|
||||
color = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
// KAVA balance
|
||||
Column {
|
||||
Text(
|
||||
text = "KAVA",
|
||||
style = MaterialTheme.typography.labelSmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
Text(
|
||||
text = walletBalance?.kavaBalance ?: balance ?: "加载中...",
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
fontWeight = FontWeight.Medium,
|
||||
color = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
}
|
||||
// Green Points balance
|
||||
Column(horizontalAlignment = Alignment.End) {
|
||||
Text(
|
||||
text = GreenPointsToken.NAME,
|
||||
style = MaterialTheme.typography.labelSmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
Text(
|
||||
text = walletBalance?.greenPointsBalance ?: "加载中...",
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
fontWeight = FontWeight.Medium,
|
||||
color = Color(0xFF4CAF50)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(24.dp))
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
|
||||
// Token type selector
|
||||
Text(
|
||||
text = "选择转账类型",
|
||||
style = MaterialTheme.typography.labelMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
// KAVA option
|
||||
FilterChip(
|
||||
selected = selectedTokenType == TokenType.KAVA,
|
||||
onClick = { onTokenTypeChange(TokenType.KAVA) },
|
||||
label = { Text("KAVA") },
|
||||
leadingIcon = {
|
||||
if (selectedTokenType == TokenType.KAVA) {
|
||||
Icon(
|
||||
Icons.Default.Check,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(18.dp)
|
||||
)
|
||||
}
|
||||
},
|
||||
modifier = Modifier.weight(1f)
|
||||
)
|
||||
// Green Points option
|
||||
FilterChip(
|
||||
selected = selectedTokenType == TokenType.GREEN_POINTS,
|
||||
onClick = { onTokenTypeChange(TokenType.GREEN_POINTS) },
|
||||
label = { Text(GreenPointsToken.NAME) },
|
||||
leadingIcon = {
|
||||
if (selectedTokenType == TokenType.GREEN_POINTS) {
|
||||
Icon(
|
||||
Icons.Default.Check,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(18.dp)
|
||||
)
|
||||
}
|
||||
},
|
||||
colors = FilterChipDefaults.filterChipColors(
|
||||
selectedContainerColor = Color(0xFF4CAF50).copy(alpha = 0.2f),
|
||||
selectedLabelColor = Color(0xFF4CAF50)
|
||||
),
|
||||
modifier = Modifier.weight(1f)
|
||||
)
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
|
||||
// Recipient address with QR scan button
|
||||
OutlinedTextField(
|
||||
|
|
@ -308,30 +406,40 @@ private fun TransferInputScreen(
|
|||
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
|
||||
// Amount
|
||||
// Amount with dynamic label based on token type
|
||||
OutlinedTextField(
|
||||
value = amount,
|
||||
onValueChange = onAmountChange,
|
||||
label = { Text("金额 (KAVA)") },
|
||||
label = { Text("金额 ($tokenSymbol)") },
|
||||
placeholder = { Text("0.0") },
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
singleLine = true,
|
||||
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Decimal),
|
||||
leadingIcon = {
|
||||
Icon(Icons.Default.AttachMoney, contentDescription = null)
|
||||
Icon(
|
||||
if (selectedTokenType == TokenType.GREEN_POINTS) Icons.Default.Stars else Icons.Default.AttachMoney,
|
||||
contentDescription = null,
|
||||
tint = if (selectedTokenType == TokenType.GREEN_POINTS) Color(0xFF4CAF50) else MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
},
|
||||
trailingIcon = {
|
||||
if (balance != null) {
|
||||
if (currentBalance != null) {
|
||||
TextButton(
|
||||
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)
|
||||
if (selectedTokenType == TokenType.KAVA) {
|
||||
// For KAVA, calculate max after deducting gas
|
||||
val result = TransactionUtils.calculateMaxTransferAmount(currentBalance, rpcUrl)
|
||||
result.onSuccess { maxAmount ->
|
||||
onAmountChange(maxAmount)
|
||||
}.onFailure {
|
||||
// Fallback to balance if calculation fails
|
||||
onAmountChange(currentBalance)
|
||||
}
|
||||
} else {
|
||||
// For tokens, use the full balance
|
||||
onAmountChange(currentBalance)
|
||||
}
|
||||
isCalculatingMax = false
|
||||
}
|
||||
|
|
|
|||
|
|
@ -35,8 +35,10 @@ import androidx.compose.ui.unit.sp
|
|||
import androidx.compose.ui.window.Dialog
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import com.durian.tssparty.domain.model.GreenPointsToken
|
||||
import com.durian.tssparty.domain.model.NetworkType
|
||||
import com.durian.tssparty.domain.model.ShareRecord
|
||||
import com.durian.tssparty.domain.model.WalletBalance
|
||||
import com.google.zxing.BarcodeFormat
|
||||
import com.google.zxing.qrcode.QRCodeWriter
|
||||
import kotlinx.coroutines.delay
|
||||
|
|
@ -48,6 +50,7 @@ fun WalletsScreen(
|
|||
shares: List<ShareRecord>,
|
||||
isConnected: Boolean,
|
||||
balances: Map<String, String> = emptyMap(),
|
||||
walletBalances: Map<String, WalletBalance> = emptyMap(),
|
||||
networkType: NetworkType = NetworkType.MAINNET,
|
||||
onDeleteShare: (Long) -> Unit,
|
||||
onRefreshBalance: ((String) -> Unit)? = null,
|
||||
|
|
@ -146,6 +149,7 @@ fun WalletsScreen(
|
|||
WalletItemCard(
|
||||
share = share,
|
||||
balance = balances[share.address],
|
||||
walletBalance = walletBalances[share.address],
|
||||
onViewDetails = { selectedWallet = share },
|
||||
onTransfer = {
|
||||
onTransfer?.invoke(share.id)
|
||||
|
|
@ -196,6 +200,7 @@ fun WalletsScreen(
|
|||
private fun WalletItemCard(
|
||||
share: ShareRecord,
|
||||
balance: String? = null,
|
||||
walletBalance: WalletBalance? = null,
|
||||
onViewDetails: () -> Unit,
|
||||
onTransfer: () -> Unit,
|
||||
onDelete: () -> Unit
|
||||
|
|
@ -256,31 +261,63 @@ private fun WalletItemCard(
|
|||
|
||||
Spacer(modifier = Modifier.height(12.dp))
|
||||
|
||||
// Balance display
|
||||
// Balance display - now shows both KAVA and Green Points
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween
|
||||
) {
|
||||
Icon(
|
||||
Icons.Default.AccountBalance,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(16.dp),
|
||||
tint = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
Spacer(modifier = Modifier.width(4.dp))
|
||||
if (balance != null) {
|
||||
// KAVA balance
|
||||
Column {
|
||||
Text(
|
||||
text = "$balance KAVA",
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.primary,
|
||||
fontWeight = FontWeight.Medium
|
||||
)
|
||||
} else {
|
||||
// Loading state
|
||||
Text(
|
||||
text = "加载中...",
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
text = "KAVA",
|
||||
style = MaterialTheme.typography.labelSmall,
|
||||
color = MaterialTheme.colorScheme.outline
|
||||
)
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
Icon(
|
||||
Icons.Default.AccountBalance,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(16.dp),
|
||||
tint = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
Spacer(modifier = Modifier.width(4.dp))
|
||||
Text(
|
||||
text = walletBalance?.kavaBalance ?: balance ?: "加载中...",
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = if (walletBalance != null || balance != null)
|
||||
MaterialTheme.colorScheme.primary
|
||||
else
|
||||
MaterialTheme.colorScheme.outline,
|
||||
fontWeight = FontWeight.Medium
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Green Points (绿积分) balance
|
||||
Column(horizontalAlignment = Alignment.End) {
|
||||
Text(
|
||||
text = GreenPointsToken.NAME,
|
||||
style = MaterialTheme.typography.labelSmall,
|
||||
color = MaterialTheme.colorScheme.outline
|
||||
)
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
Icon(
|
||||
Icons.Default.Stars,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(16.dp),
|
||||
tint = Color(0xFF4CAF50) // Green color for Green Points
|
||||
)
|
||||
Spacer(modifier = Modifier.width(4.dp))
|
||||
Text(
|
||||
text = walletBalance?.greenPointsBalance ?: "加载中...",
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = if (walletBalance != null)
|
||||
Color(0xFF4CAF50)
|
||||
else
|
||||
MaterialTheme.colorScheme.outline,
|
||||
fontWeight = FontWeight.Medium
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1034,22 +1034,31 @@ class MainViewModel @Inject constructor(
|
|||
_createdInviteCode.value = null
|
||||
}
|
||||
|
||||
// Wallet balances cache
|
||||
// Wallet balances cache (KAVA only - deprecated, use walletBalances instead)
|
||||
private val _balances = MutableStateFlow<Map<String, String>>(emptyMap())
|
||||
val balances: StateFlow<Map<String, String>> = _balances.asStateFlow()
|
||||
|
||||
// Combined wallet balances (KAVA + Green Points)
|
||||
private val _walletBalances = MutableStateFlow<Map<String, WalletBalance>>(emptyMap())
|
||||
val walletBalances: StateFlow<Map<String, WalletBalance>> = _walletBalances.asStateFlow()
|
||||
|
||||
/**
|
||||
* Fetch balance for a wallet using share data (handles both EVM and Cosmos address formats)
|
||||
* Now fetches both KAVA and Green Points (绿积分) balances
|
||||
*/
|
||||
fun fetchBalanceForShare(share: ShareRecord) {
|
||||
viewModelScope.launch {
|
||||
val rpcUrl = _settings.value.kavaRpcUrl
|
||||
// Ensure we use EVM address format for RPC calls
|
||||
val evmAddress = AddressUtils.getEvmAddress(share.address, share.publicKey)
|
||||
val result = repository.getBalance(evmAddress, rpcUrl)
|
||||
result.onSuccess { balance ->
|
||||
// Store balance with original address as key (for UI lookup)
|
||||
_balances.update { it + (share.address to balance) }
|
||||
|
||||
// Fetch combined wallet balance (KAVA + Green Points)
|
||||
val result = repository.getWalletBalance(evmAddress, rpcUrl)
|
||||
result.onSuccess { walletBalance ->
|
||||
// Store with original address as key (for UI lookup)
|
||||
_walletBalances.update { it + (share.address to walletBalance) }
|
||||
// Also update legacy balances map for backward compatibility
|
||||
_balances.update { it + (share.address to walletBalance.kavaBalance) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1060,9 +1069,10 @@ class MainViewModel @Inject constructor(
|
|||
fun fetchBalance(address: String) {
|
||||
viewModelScope.launch {
|
||||
val rpcUrl = _settings.value.kavaRpcUrl
|
||||
val result = repository.getBalance(address, rpcUrl)
|
||||
result.onSuccess { balance ->
|
||||
_balances.update { it + (address to balance) }
|
||||
val result = repository.getWalletBalance(address, rpcUrl)
|
||||
result.onSuccess { walletBalance ->
|
||||
_walletBalances.update { it + (address to walletBalance) }
|
||||
_balances.update { it + (address to walletBalance.kavaBalance) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1110,10 +1120,10 @@ class MainViewModel @Inject constructor(
|
|||
/**
|
||||
* Prepare a transfer transaction
|
||||
*/
|
||||
fun prepareTransfer(shareId: Long, toAddress: String, amount: String) {
|
||||
fun prepareTransfer(shareId: Long, toAddress: String, amount: String, tokenType: TokenType = TokenType.KAVA) {
|
||||
viewModelScope.launch {
|
||||
_uiState.update { it.copy(isLoading = true, error = null) }
|
||||
_transferState.update { it.copy(shareId = shareId, toAddress = toAddress, amount = amount) }
|
||||
_transferState.update { it.copy(shareId = shareId, toAddress = toAddress, amount = amount, tokenType = tokenType) }
|
||||
|
||||
val share = repository.getShareById(shareId)
|
||||
if (share == null) {
|
||||
|
|
@ -1133,7 +1143,8 @@ class MainViewModel @Inject constructor(
|
|||
to = toAddress,
|
||||
amount = amount,
|
||||
rpcUrl = rpcUrl,
|
||||
chainId = chainId
|
||||
chainId = chainId,
|
||||
tokenType = tokenType
|
||||
)
|
||||
|
||||
result.fold(
|
||||
|
|
@ -1347,7 +1358,8 @@ data class MainUiState(
|
|||
data class TransferState(
|
||||
val shareId: Long = 0,
|
||||
val toAddress: String = "",
|
||||
val amount: String = ""
|
||||
val amount: String = "",
|
||||
val tokenType: TokenType = TokenType.KAVA
|
||||
)
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -1,5 +1,7 @@
|
|||
package com.durian.tssparty.util
|
||||
|
||||
import com.durian.tssparty.domain.model.GreenPointsToken
|
||||
import com.durian.tssparty.domain.model.TokenType
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import okhttp3.MediaType.Companion.toMediaType
|
||||
|
|
@ -50,14 +52,16 @@ object TransactionUtils {
|
|||
data class TransactionParams(
|
||||
val from: String,
|
||||
val to: String,
|
||||
val amount: String, // In KAVA (not wei)
|
||||
val amount: String, // In KAVA or token units (not wei)
|
||||
val rpcUrl: String,
|
||||
val chainId: Int = KAVA_TESTNET_CHAIN_ID
|
||||
val chainId: Int = KAVA_TESTNET_CHAIN_ID,
|
||||
val tokenType: TokenType = TokenType.KAVA // Token type for transfer
|
||||
)
|
||||
|
||||
/**
|
||||
* Prepare a transaction for signing
|
||||
* Gets nonce, gas price, estimates gas, and calculates sign hash
|
||||
* Supports both native KAVA transfers and ERC-20 token transfers (绿积分)
|
||||
*/
|
||||
suspend fun prepareTransaction(params: TransactionParams): Result<PreparedTransaction> = withContext(Dispatchers.IO) {
|
||||
try {
|
||||
|
|
@ -67,16 +71,36 @@ object TransactionUtils {
|
|||
// 2. Get gas price
|
||||
val gasPrice = getGasPrice(params.rpcUrl).getOrThrow()
|
||||
|
||||
// 3. Convert amount to wei (1 KAVA = 10^18 wei)
|
||||
val valueWei = kavaToWei(params.amount)
|
||||
// 3. Prepare transaction based on token type
|
||||
val (toAddress, valueWei, txData) = when (params.tokenType) {
|
||||
TokenType.KAVA -> {
|
||||
// Native KAVA transfer
|
||||
Triple(params.to, kavaToWei(params.amount), ByteArray(0))
|
||||
}
|
||||
TokenType.GREEN_POINTS -> {
|
||||
// ERC-20 token transfer (绿积分)
|
||||
// To address is the contract, value is 0
|
||||
// Data is transfer(recipient, amount) encoded
|
||||
val tokenAmount = greenPointsToRaw(params.amount)
|
||||
val transferData = encodeErc20Transfer(params.to, tokenAmount)
|
||||
Triple(GreenPointsToken.CONTRACT_ADDRESS, BigInteger.ZERO, transferData)
|
||||
}
|
||||
}
|
||||
|
||||
// 4. Estimate gas
|
||||
val gasLimit = estimateGas(
|
||||
val gasLimit = estimateGasWithData(
|
||||
from = params.from,
|
||||
to = params.to,
|
||||
to = toAddress,
|
||||
value = valueWei,
|
||||
data = txData,
|
||||
rpcUrl = params.rpcUrl
|
||||
).getOrElse { BigInteger.valueOf(21000) } // Default for simple transfer
|
||||
).getOrElse {
|
||||
// Default gas limits
|
||||
when (params.tokenType) {
|
||||
TokenType.KAVA -> BigInteger.valueOf(21000)
|
||||
TokenType.GREEN_POINTS -> BigInteger.valueOf(65000) // ERC-20 transfers need more gas
|
||||
}
|
||||
}
|
||||
|
||||
// 5. RLP encode for signing (Legacy Type 0 format)
|
||||
// Format: [nonce, gasPrice, gasLimit, to, value, data, chainId, 0, 0]
|
||||
|
|
@ -84,9 +108,9 @@ object TransactionUtils {
|
|||
nonce = nonce,
|
||||
gasPrice = gasPrice,
|
||||
gasLimit = gasLimit,
|
||||
to = params.to,
|
||||
to = toAddress,
|
||||
value = valueWei,
|
||||
data = ByteArray(0),
|
||||
data = txData,
|
||||
chainId = params.chainId
|
||||
)
|
||||
|
||||
|
|
@ -97,9 +121,10 @@ object TransactionUtils {
|
|||
nonce = nonce,
|
||||
gasPrice = gasPrice,
|
||||
gasLimit = gasLimit,
|
||||
to = params.to,
|
||||
to = toAddress,
|
||||
from = params.from,
|
||||
value = valueWei,
|
||||
data = txData,
|
||||
chainId = params.chainId,
|
||||
signHash = "0x" + signHash.toHexString(),
|
||||
rawTxForSigning = rawTxForSigning
|
||||
|
|
@ -109,6 +134,41 @@ object TransactionUtils {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Encode ERC-20 transfer(address,uint256) function call
|
||||
*/
|
||||
private fun encodeErc20Transfer(to: String, amount: BigInteger): ByteArray {
|
||||
// Function selector: transfer(address,uint256) = 0xa9059cbb
|
||||
val selector = GreenPointsToken.TRANSFER_SELECTOR.removePrefix("0x").hexToByteArray()
|
||||
|
||||
// Encode recipient address (padded to 32 bytes)
|
||||
val paddedAddress = to.removePrefix("0x").lowercase().padStart(64, '0').hexToByteArray()
|
||||
|
||||
// Encode amount (padded to 32 bytes)
|
||||
val amountHex = amount.toString(16).padStart(64, '0')
|
||||
val paddedAmount = amountHex.hexToByteArray()
|
||||
|
||||
return selector + paddedAddress + paddedAmount
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert Green Points amount to raw units (6 decimals)
|
||||
*/
|
||||
fun greenPointsToRaw(amount: String): BigInteger {
|
||||
val decimal = BigDecimal(amount)
|
||||
val rawDecimal = decimal.multiply(BigDecimal("1000000")) // 10^6
|
||||
return rawDecimal.toBigInteger()
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert raw units to Green Points display amount
|
||||
*/
|
||||
fun rawToGreenPoints(raw: BigInteger): String {
|
||||
val rawDecimal = BigDecimal(raw)
|
||||
val displayDecimal = rawDecimal.divide(BigDecimal("1000000"), 6, java.math.RoundingMode.DOWN)
|
||||
return displayDecimal.toPlainString()
|
||||
}
|
||||
|
||||
/**
|
||||
* Finalize transaction with signature
|
||||
* Returns the signed raw transaction hex string ready for broadcast
|
||||
|
|
@ -297,9 +357,20 @@ object TransactionUtils {
|
|||
to: String,
|
||||
value: BigInteger,
|
||||
rpcUrl: String
|
||||
): Result<BigInteger> = withContext(Dispatchers.IO) {
|
||||
estimateGasWithData(from, to, value, ByteArray(0), rpcUrl)
|
||||
}
|
||||
|
||||
private suspend fun estimateGasWithData(
|
||||
from: String,
|
||||
to: String,
|
||||
value: BigInteger,
|
||||
data: ByteArray,
|
||||
rpcUrl: String
|
||||
): Result<BigInteger> = withContext(Dispatchers.IO) {
|
||||
try {
|
||||
val valueHex = "0x" + value.toString(16)
|
||||
val dataHex = if (data.isEmpty()) "" else "\"data\": \"0x${data.toHexString()}\","
|
||||
val requestBody = """
|
||||
{
|
||||
"jsonrpc": "2.0",
|
||||
|
|
@ -307,7 +378,7 @@ object TransactionUtils {
|
|||
"params": [{
|
||||
"from": "$from",
|
||||
"to": "$to",
|
||||
"value": "$valueHex"
|
||||
"value": "$valueHex"${if (dataHex.isNotEmpty()) ",\n ${dataHex.trimEnd(',')}" else ""}
|
||||
}],
|
||||
"id": 1
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,115 @@
|
|||
# Durian USDT (dUSDT) 代币合约
|
||||
|
||||
## 合约概述
|
||||
|
||||
Durian USDT 是一个部署在 Kava EVM 主网上的固定供应量 ERC-20 代币。该合约**完全禁止增发**,所有代币在部署时一次性铸造给部署者地址。
|
||||
|
||||
## 合约详情
|
||||
|
||||
| 项目 | 值 |
|
||||
|------|-----|
|
||||
| 合约地址 | `0xA9F3A35dBa8699c8C681D8db03F0c1A8CEB9D7c3` |
|
||||
| 代币名称 | Durian USDT |
|
||||
| 代币符号 | dUSDT |
|
||||
| 精度 (Decimals) | 6 |
|
||||
| 总供应量 | 1,000,000,000,000 dUSDT (1万亿) |
|
||||
| 总供应量 (最小单位) | 1,000,000,000,000,000,000 (10^18) |
|
||||
|
||||
## 网络信息
|
||||
|
||||
| 项目 | 值 |
|
||||
|------|-----|
|
||||
| 网络名称 | Kava EVM Mainnet |
|
||||
| Chain ID | 2222 |
|
||||
| RPC URL | https://evm.kava.io |
|
||||
| 区块浏览器 | https://kavascan.com |
|
||||
| 原生代币 | KAVA |
|
||||
|
||||
## 持有人/管理人信息
|
||||
|
||||
| 项目 | 值 |
|
||||
|------|-----|
|
||||
| 地址 | `0x4F7E78d6B7C5FC502Ec7039848690f08c8970F1E` |
|
||||
| 私钥 | `0x886ea4cffe76c386fecf3ff321ac9ae913737c46c17bc6ce2413752144668a2a` |
|
||||
| 初始 dUSDT 余额 | 1,000,000,000,000 dUSDT (全部) |
|
||||
|
||||
> **安全警告**: 私钥必须妥善保管,切勿泄露给他人。
|
||||
|
||||
## 部署信息
|
||||
|
||||
| 项目 | 值 |
|
||||
|------|-----|
|
||||
| 部署时间 | 2026-01-02 |
|
||||
| 部署交易哈希 | `0xa73d7dce17723bc5a9d62767c515dadf4ccccc26327ed5637958ce817edd671d` |
|
||||
| Solidity 版本 | 0.8.19 |
|
||||
| EVM 版本 | Paris (无 PUSH0 操作码) |
|
||||
| 优化 | 启用 (runs: 200) |
|
||||
|
||||
## 合约特性
|
||||
|
||||
### 固定供应量 - 无增发机制
|
||||
|
||||
该合约的核心特性是**完全禁止增发**:
|
||||
|
||||
1. **无 mint 函数**: 合约代码中不存在任何铸造新代币的函数
|
||||
2. **无 owner/admin 权限**: 合约没有特权角色,无人能修改供应量
|
||||
3. **供应量在构造函数中固定**: 所有代币在部署时一次性创建
|
||||
4. **totalSupply 是 constant**: 总供应量声明为常量,无法修改
|
||||
|
||||
### 支持的 ERC-20 标准函数
|
||||
|
||||
| 函数 | 描述 |
|
||||
|------|------|
|
||||
| `name()` | 返回代币名称 "Durian USDT" |
|
||||
| `symbol()` | 返回代币符号 "dUSDT" |
|
||||
| `decimals()` | 返回精度 6 |
|
||||
| `totalSupply()` | 返回总供应量 |
|
||||
| `balanceOf(address)` | 查询地址余额 |
|
||||
| `transfer(address, uint256)` | 转账 |
|
||||
| `approve(address, uint256)` | 授权 |
|
||||
| `allowance(address, address)` | 查询授权额度 |
|
||||
| `transferFrom(address, address, uint256)` | 授权转账 |
|
||||
|
||||
## 查看链接
|
||||
|
||||
- 合约: https://kavascan.com/address/0xA9F3A35dBa8699c8C681D8db03F0c1A8CEB9D7c3
|
||||
- 持有人: https://kavascan.com/address/0x4F7E78d6B7C5FC502Ec7039848690f08c8970F1E
|
||||
- 部署交易: https://kavascan.com/tx/0xa73d7dce17723bc5a9d62767c515dadf4ccccc26327ed5637958ce817edd671d
|
||||
|
||||
## 在钱包中添加代币
|
||||
|
||||
在 MetaMask 或其他钱包中添加自定义代币:
|
||||
|
||||
1. 网络: Kava EVM (Chain ID: 2222)
|
||||
2. 合约地址: `0xA9F3A35dBa8699c8C681D8db03F0c1A8CEB9D7c3`
|
||||
3. 代币符号: `dUSDT`
|
||||
4. 精度: `6`
|
||||
|
||||
## 文件说明
|
||||
|
||||
| 文件 | 描述 |
|
||||
|------|------|
|
||||
| `DurianUSDT.sol` | Solidity 源代码 |
|
||||
| `DurianUSDT.abi` | 合约 ABI (Application Binary Interface) |
|
||||
| `DurianUSDT.bin` | 编译后的字节码 |
|
||||
| `CONTRACT_INFO.md` | 本文档 |
|
||||
|
||||
## 代码审计要点
|
||||
|
||||
该合约经过精简设计,关键安全特性:
|
||||
|
||||
1. **无 owner 模式**: 没有特权地址可以执行管理操作
|
||||
2. **无升级机制**: 合约不可升级,代码永久固定
|
||||
3. **无暂停功能**: 转账功能无法被暂停
|
||||
4. **无黑名单功能**: 没有地址可以被限制转账
|
||||
5. **使用 unchecked 块**: 在已验证的情况下使用,节省 gas
|
||||
|
||||
## 与标准 USDT 的对比
|
||||
|
||||
| 特性 | dUSDT | 标准 USDT |
|
||||
|------|-------|----------|
|
||||
| 精度 | 6 | 6 |
|
||||
| 可增发 | 否 | 是 |
|
||||
| 可暂停 | 否 | 是 |
|
||||
| 黑名单功能 | 否 | 是 |
|
||||
| 中心化管理 | 否 | 是 |
|
||||
|
|
@ -0,0 +1,229 @@
|
|||
[
|
||||
{
|
||||
"inputs": [],
|
||||
"stateMutability": "nonpayable",
|
||||
"type": "constructor"
|
||||
},
|
||||
{
|
||||
"anonymous": false,
|
||||
"inputs": [
|
||||
{
|
||||
"indexed": true,
|
||||
"internalType": "address",
|
||||
"name": "owner",
|
||||
"type": "address"
|
||||
},
|
||||
{
|
||||
"indexed": true,
|
||||
"internalType": "address",
|
||||
"name": "spender",
|
||||
"type": "address"
|
||||
},
|
||||
{
|
||||
"indexed": false,
|
||||
"internalType": "uint256",
|
||||
"name": "value",
|
||||
"type": "uint256"
|
||||
}
|
||||
],
|
||||
"name": "Approval",
|
||||
"type": "event"
|
||||
},
|
||||
{
|
||||
"anonymous": false,
|
||||
"inputs": [
|
||||
{
|
||||
"indexed": true,
|
||||
"internalType": "address",
|
||||
"name": "from",
|
||||
"type": "address"
|
||||
},
|
||||
{
|
||||
"indexed": true,
|
||||
"internalType": "address",
|
||||
"name": "to",
|
||||
"type": "address"
|
||||
},
|
||||
{
|
||||
"indexed": false,
|
||||
"internalType": "uint256",
|
||||
"name": "value",
|
||||
"type": "uint256"
|
||||
}
|
||||
],
|
||||
"name": "Transfer",
|
||||
"type": "event"
|
||||
},
|
||||
{
|
||||
"inputs": [
|
||||
{
|
||||
"internalType": "address",
|
||||
"name": "owner",
|
||||
"type": "address"
|
||||
},
|
||||
{
|
||||
"internalType": "address",
|
||||
"name": "spender",
|
||||
"type": "address"
|
||||
}
|
||||
],
|
||||
"name": "allowance",
|
||||
"outputs": [
|
||||
{
|
||||
"internalType": "uint256",
|
||||
"name": "",
|
||||
"type": "uint256"
|
||||
}
|
||||
],
|
||||
"stateMutability": "view",
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"inputs": [
|
||||
{
|
||||
"internalType": "address",
|
||||
"name": "spender",
|
||||
"type": "address"
|
||||
},
|
||||
{
|
||||
"internalType": "uint256",
|
||||
"name": "amount",
|
||||
"type": "uint256"
|
||||
}
|
||||
],
|
||||
"name": "approve",
|
||||
"outputs": [
|
||||
{
|
||||
"internalType": "bool",
|
||||
"name": "",
|
||||
"type": "bool"
|
||||
}
|
||||
],
|
||||
"stateMutability": "nonpayable",
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"inputs": [
|
||||
{
|
||||
"internalType": "address",
|
||||
"name": "account",
|
||||
"type": "address"
|
||||
}
|
||||
],
|
||||
"name": "balanceOf",
|
||||
"outputs": [
|
||||
{
|
||||
"internalType": "uint256",
|
||||
"name": "",
|
||||
"type": "uint256"
|
||||
}
|
||||
],
|
||||
"stateMutability": "view",
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"inputs": [],
|
||||
"name": "decimals",
|
||||
"outputs": [
|
||||
{
|
||||
"internalType": "uint8",
|
||||
"name": "",
|
||||
"type": "uint8"
|
||||
}
|
||||
],
|
||||
"stateMutability": "view",
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"inputs": [],
|
||||
"name": "name",
|
||||
"outputs": [
|
||||
{
|
||||
"internalType": "string",
|
||||
"name": "",
|
||||
"type": "string"
|
||||
}
|
||||
],
|
||||
"stateMutability": "view",
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"inputs": [],
|
||||
"name": "symbol",
|
||||
"outputs": [
|
||||
{
|
||||
"internalType": "string",
|
||||
"name": "",
|
||||
"type": "string"
|
||||
}
|
||||
],
|
||||
"stateMutability": "view",
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"inputs": [],
|
||||
"name": "totalSupply",
|
||||
"outputs": [
|
||||
{
|
||||
"internalType": "uint256",
|
||||
"name": "",
|
||||
"type": "uint256"
|
||||
}
|
||||
],
|
||||
"stateMutability": "view",
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"inputs": [
|
||||
{
|
||||
"internalType": "address",
|
||||
"name": "to",
|
||||
"type": "address"
|
||||
},
|
||||
{
|
||||
"internalType": "uint256",
|
||||
"name": "amount",
|
||||
"type": "uint256"
|
||||
}
|
||||
],
|
||||
"name": "transfer",
|
||||
"outputs": [
|
||||
{
|
||||
"internalType": "bool",
|
||||
"name": "",
|
||||
"type": "bool"
|
||||
}
|
||||
],
|
||||
"stateMutability": "nonpayable",
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"inputs": [
|
||||
{
|
||||
"internalType": "address",
|
||||
"name": "from",
|
||||
"type": "address"
|
||||
},
|
||||
{
|
||||
"internalType": "address",
|
||||
"name": "to",
|
||||
"type": "address"
|
||||
},
|
||||
{
|
||||
"internalType": "uint256",
|
||||
"name": "amount",
|
||||
"type": "uint256"
|
||||
}
|
||||
],
|
||||
"name": "transferFrom",
|
||||
"outputs": [
|
||||
{
|
||||
"internalType": "bool",
|
||||
"name": "",
|
||||
"type": "bool"
|
||||
}
|
||||
],
|
||||
"stateMutability": "nonpayable",
|
||||
"type": "function"
|
||||
}
|
||||
]
|
||||
|
|
@ -0,0 +1 @@
|
|||
608060405234801561001057600080fd5b5033600081815260208181526040808320670de0b6b3a76400009081905590519081527fddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef910160405180910390a36106fb8061006d6000396000f3fe608060405234801561001057600080fd5b50600436106100935760003560e01c8063313ce56711610066578063313ce5671461012b57806370a082311461014557806395d89b411461016e578063a9059cbb14610192578063dd62ed3e146101a557600080fd5b806306fdde0314610098578063095ea7b3146100d857806318160ddd146100fb57806323b872dd14610118575b600080fd5b6100c26040518060400160405280600b81526020016a111d5c9a585b881554d11560aa1b81525081565b6040516100cf91906105a0565b60405180910390f35b6100eb6100e636600461060a565b6101de565b60405190151581526020016100cf565b61010a670de0b6b3a764000081565b6040519081526020016100cf565b6100eb610126366004610634565b6102a0565b610133600681565b60405160ff90911681526020016100cf565b61010a610153366004610670565b6001600160a01b031660009081526020819052604090205490565b6100c260405180604001604052806005815260200164191554d11560da1b81525081565b6100eb6101a036600461060a565b61049a565b61010a6101b3366004610692565b6001600160a01b03918216600090815260016020908152604080832093909416825291909152205490565b60006001600160a01b03831661023b5760405162461bcd60e51b815260206004820152601760248201527f417070726f766520746f207a65726f206164647265737300000000000000000060448201526064015b60405180910390fd5b3360008181526001602090815260408083206001600160a01b03881680855290835292819020869055518581529192917f8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b92591015b60405180910390a350600192915050565b60006001600160a01b0384166102f85760405162461bcd60e51b815260206004820152601a60248201527f5472616e736665722066726f6d207a65726f20616464726573730000000000006044820152606401610232565b6001600160a01b0383166103495760405162461bcd60e51b81526020600482015260186024820152775472616e7366657220746f207a65726f206164647265737360401b6044820152606401610232565b6001600160a01b0384166000908152602081905260409020548211156103a85760405162461bcd60e51b8152602060048201526014602482015273496e73756666696369656e742062616c616e636560601b6044820152606401610232565b6001600160a01b03841660009081526001602090815260408083203384529091529020548211156104145760405162461bcd60e51b8152602060048201526016602482015275496e73756666696369656e7420616c6c6f77616e636560501b6044820152606401610232565b6001600160a01b03848116600081815260208181526040808320805488900390559387168083528483208054880190558383526001825284832033845282529184902080548790039055925185815290927fddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef910160405180910390a35060019392505050565b60006001600160a01b0383166104ed5760405162461bcd60e51b81526020600482015260186024820152775472616e7366657220746f207a65726f206164647265737360401b6044820152606401610232565b336000908152602081905260409020548211156105435760405162461bcd60e51b8152602060048201526014602482015273496e73756666696369656e742062616c616e636560601b6044820152606401610232565b33600081815260208181526040808320805487900390556001600160a01b03871680845292819020805487019055518581529192917fddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef910161028f565b600060208083528351808285015260005b818110156105cd578581018301518582016040015282016105b1565b506000604082860101526040601f19601f8301168501019250505092915050565b80356001600160a01b038116811461060557600080fd5b919050565b6000806040838503121561061d57600080fd5b610626836105ee565b946020939093013593505050565b60008060006060848603121561064957600080fd5b610652846105ee565b9250610660602085016105ee565b9150604084013590509250925092565b60006020828403121561068257600080fd5b61068b826105ee565b9392505050565b600080604083850312156106a557600080fd5b6106ae836105ee565b91506106bc602084016105ee565b9050925092905056fea264697066735822122028c97073f6e7db0ad943d101cb6873b31c3eb19bcea3eda83148447ab676a5ee64736f6c63430008130033
|
||||
|
|
@ -0,0 +1,78 @@
|
|||
// SPDX-License-Identifier: MIT
|
||||
pragma solidity 0.8.19;
|
||||
|
||||
/**
|
||||
* @title DurianUSDT
|
||||
* @dev Fixed supply ERC-20 token - NO MINTING CAPABILITY
|
||||
* Total Supply: 1,000,000,000,000 (1 Trillion) tokens with 6 decimals (matching USDT)
|
||||
*
|
||||
* IMPORTANT: This contract has NO mint function and NO way to increase supply.
|
||||
* All tokens are minted to the deployer at construction time.
|
||||
*/
|
||||
contract DurianUSDT {
|
||||
string public constant name = "Durian USDT";
|
||||
string public constant symbol = "dUSDT";
|
||||
uint8 public constant decimals = 6;
|
||||
|
||||
// Fixed total supply: 1 trillion tokens (1,000,000,000,000 * 10^6)
|
||||
uint256 public constant totalSupply = 1_000_000_000_000 * 10**6;
|
||||
|
||||
mapping(address => uint256) private _balances;
|
||||
mapping(address => mapping(address => uint256)) private _allowances;
|
||||
|
||||
event Transfer(address indexed from, address indexed to, uint256 value);
|
||||
event Approval(address indexed owner, address indexed spender, uint256 value);
|
||||
|
||||
/**
|
||||
* @dev Constructor - mints entire fixed supply to deployer
|
||||
* No mint function exists - supply is permanently fixed
|
||||
*/
|
||||
constructor() {
|
||||
_balances[msg.sender] = totalSupply;
|
||||
emit Transfer(address(0), msg.sender, totalSupply);
|
||||
}
|
||||
|
||||
function balanceOf(address account) public view returns (uint256) {
|
||||
return _balances[account];
|
||||
}
|
||||
|
||||
function transfer(address to, uint256 amount) public returns (bool) {
|
||||
require(to != address(0), "Transfer to zero address");
|
||||
require(_balances[msg.sender] >= amount, "Insufficient balance");
|
||||
|
||||
unchecked {
|
||||
_balances[msg.sender] -= amount;
|
||||
_balances[to] += amount;
|
||||
}
|
||||
|
||||
emit Transfer(msg.sender, to, amount);
|
||||
return true;
|
||||
}
|
||||
|
||||
function allowance(address owner, address spender) public view returns (uint256) {
|
||||
return _allowances[owner][spender];
|
||||
}
|
||||
|
||||
function approve(address spender, uint256 amount) public returns (bool) {
|
||||
require(spender != address(0), "Approve to zero address");
|
||||
_allowances[msg.sender][spender] = amount;
|
||||
emit Approval(msg.sender, spender, amount);
|
||||
return true;
|
||||
}
|
||||
|
||||
function transferFrom(address from, address to, uint256 amount) public returns (bool) {
|
||||
require(from != address(0), "Transfer from zero address");
|
||||
require(to != address(0), "Transfer to zero address");
|
||||
require(_balances[from] >= amount, "Insufficient balance");
|
||||
require(_allowances[from][msg.sender] >= amount, "Insufficient allowance");
|
||||
|
||||
unchecked {
|
||||
_balances[from] -= amount;
|
||||
_balances[to] += amount;
|
||||
_allowances[from][msg.sender] -= amount;
|
||||
}
|
||||
|
||||
emit Transfer(from, to, amount);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,120 @@
|
|||
# Kava EVM 网络配置
|
||||
|
||||
## 主网配置
|
||||
|
||||
| 项目 | 值 |
|
||||
|------|-----|
|
||||
| 网络名称 | Kava EVM Mainnet |
|
||||
| Chain ID | 2222 |
|
||||
| Currency Symbol | KAVA |
|
||||
| RPC URL | https://evm.kava.io |
|
||||
| WebSocket URL | wss://wevm.kava.io |
|
||||
| 区块浏览器 | https://kavascan.com |
|
||||
|
||||
## 测试网配置
|
||||
|
||||
| 项目 | 值 |
|
||||
|------|-----|
|
||||
| 网络名称 | Kava EVM Testnet |
|
||||
| Chain ID | 2221 |
|
||||
| Currency Symbol | KAVA |
|
||||
| RPC URL | https://evm.testnet.kava.io |
|
||||
| 区块浏览器 | https://testnet.kavascan.com |
|
||||
| 水龙头 | https://faucet.kava.io |
|
||||
|
||||
## RPC 端点列表
|
||||
|
||||
### 主网 RPC
|
||||
|
||||
```
|
||||
https://evm.kava.io
|
||||
https://kava-evm.publicnode.com
|
||||
https://kava.api.onfinality.io/public
|
||||
https://evm.kava.chainstacklabs.com
|
||||
```
|
||||
|
||||
### WebSocket (主网)
|
||||
|
||||
```
|
||||
wss://wevm.kava.io
|
||||
wss://kava-evm.publicnode.com
|
||||
```
|
||||
|
||||
## Gas 配置
|
||||
|
||||
| 项目 | 值 |
|
||||
|------|-----|
|
||||
| Gas Price | ~1 Gwei (动态) |
|
||||
| 合约部署 Gas Limit | ~500,000 - 1,000,000 |
|
||||
| 代币转账 Gas Limit | ~65,000 |
|
||||
| 原生转账 Gas Limit | ~21,000 |
|
||||
|
||||
## 在 MetaMask 中添加网络
|
||||
|
||||
### 主网
|
||||
|
||||
1. 打开 MetaMask
|
||||
2. 点击网络选择器 > 添加网络
|
||||
3. 填写以下信息:
|
||||
- 网络名称: `Kava EVM`
|
||||
- RPC URL: `https://evm.kava.io`
|
||||
- Chain ID: `2222`
|
||||
- 货币符号: `KAVA`
|
||||
- 区块浏览器: `https://kavascan.com`
|
||||
|
||||
### 测试网
|
||||
|
||||
1. 打开 MetaMask
|
||||
2. 点击网络选择器 > 添加网络
|
||||
3. 填写以下信息:
|
||||
- 网络名称: `Kava EVM Testnet`
|
||||
- RPC URL: `https://evm.testnet.kava.io`
|
||||
- Chain ID: `2221`
|
||||
- 货币符号: `KAVA`
|
||||
- 区块浏览器: `https://testnet.kavascan.com`
|
||||
|
||||
## Kava 双地址系统
|
||||
|
||||
Kava 网络支持两种地址格式:
|
||||
|
||||
| 类型 | 格式 | 示例 |
|
||||
|------|------|------|
|
||||
| Cosmos 地址 | kava1... | `kava1...abc` |
|
||||
| EVM 地址 | 0x... | `0x4F7E78d6B7C5FC502Ec7039848690f08c8970F1E` |
|
||||
|
||||
同一个私钥可以派生出两种地址,它们共享相同的余额。
|
||||
|
||||
## EVM 兼容性
|
||||
|
||||
Kava EVM 兼容以太坊 EVM,支持:
|
||||
|
||||
- Solidity 智能合约
|
||||
- ERC-20/ERC-721/ERC-1155 代币标准
|
||||
- Web3.js / ethers.js
|
||||
- MetaMask 等以太坊钱包
|
||||
|
||||
### 注意事项
|
||||
|
||||
- Kava EVM **不支持 PUSH0 操作码** (Shanghai 升级的特性)
|
||||
- 编译合约时需要使用 `evmVersion: "paris"` 或更早版本
|
||||
- 推荐使用 Solidity 0.8.19 或更早版本
|
||||
|
||||
## 常用合约地址
|
||||
|
||||
### 主网
|
||||
|
||||
| 代币 | 地址 |
|
||||
|------|------|
|
||||
| WKAVA (Wrapped KAVA) | `0xc86c7C0eFbd6A49B35E8714C5f59D99De09A225b` |
|
||||
| USDT | `0x919C1c267BC06a7039e03fcc2eF738525769109c` |
|
||||
| USDC | `0xfA9343C3897324496A05fC75abeD6bAC29f8A40f` |
|
||||
| DAI | `0x765277EebeCA2e31912C9946eAe1021199B39C61` |
|
||||
| dUSDT (Durian USDT) | `0xA9F3A35dBa8699c8C681D8db03F0c1A8CEB9D7c3` |
|
||||
|
||||
## 资源链接
|
||||
|
||||
- 官网: https://www.kava.io/
|
||||
- 文档: https://docs.kava.io/
|
||||
- GitHub: https://github.com/Kava-Labs
|
||||
- 区块浏览器: https://kavascan.com/
|
||||
- Discord: https://discord.com/invite/kQzh3Uv
|
||||
|
|
@ -0,0 +1,83 @@
|
|||
# 钱包密钥信息
|
||||
|
||||
> **重要安全警告**: 本文件包含私钥,仅供内部使用。切勿将此文件提交到公开仓库或分享给他人。
|
||||
|
||||
## 管理员钱包
|
||||
|
||||
该钱包用于部署和管理 dUSDT 代币合约。
|
||||
|
||||
### 地址信息
|
||||
|
||||
| 项目 | 值 |
|
||||
|------|-----|
|
||||
| EVM 地址 | `0x4F7E78d6B7C5FC502Ec7039848690f08c8970F1E` |
|
||||
| 私钥 | `0x886ea4cffe76c386fecf3ff321ac9ae913737c46c17bc6ce2413752144668a2a` |
|
||||
|
||||
### 余额信息
|
||||
|
||||
| 代币 | 余额 | 备注 |
|
||||
|------|------|------|
|
||||
| KAVA | ~0.45 KAVA | 用于支付 Gas 费用 |
|
||||
| dUSDT | 1,000,000,000,000 | 1万亿,全部供应量 |
|
||||
|
||||
### 查看链接
|
||||
|
||||
- 地址: https://kavascan.com/address/0x4F7E78d6B7C5FC502Ec7039848690f08c8970F1E
|
||||
|
||||
## 导入钱包
|
||||
|
||||
### MetaMask
|
||||
|
||||
1. 打开 MetaMask
|
||||
2. 点击账户图标 > 导入账户
|
||||
3. 选择类型: 私钥
|
||||
4. 粘贴私钥: `886ea4cffe76c386fecf3ff321ac9ae913737c46c17bc6ce2413752144668a2a`
|
||||
5. 点击导入
|
||||
|
||||
### ethers.js
|
||||
|
||||
```javascript
|
||||
import { ethers } from 'ethers';
|
||||
|
||||
const PRIVATE_KEY = '0x886ea4cffe76c386fecf3ff321ac9ae913737c46c17bc6ce2413752144668a2a';
|
||||
const provider = new ethers.JsonRpcProvider('https://evm.kava.io');
|
||||
const wallet = new ethers.Wallet(PRIVATE_KEY, provider);
|
||||
|
||||
console.log('Address:', wallet.address);
|
||||
// Output: 0x4F7E78d6B7C5FC502Ec7039848690f08c8970F1E
|
||||
```
|
||||
|
||||
### Android/Kotlin
|
||||
|
||||
```kotlin
|
||||
// 使用 Web3j 或其他库
|
||||
val privateKey = "886ea4cffe76c386fecf3ff321ac9ae913737c46c17bc6ce2413752144668a2a"
|
||||
val credentials = Credentials.create(privateKey)
|
||||
println("Address: ${credentials.address}")
|
||||
// Output: 0x4F7E78d6B7C5FC502Ec7039848690f08c8970F1E
|
||||
```
|
||||
|
||||
## 地址派生
|
||||
|
||||
该地址是通过以下步骤从私钥派生的:
|
||||
|
||||
1. 私钥 (32 bytes): `886ea4cffe76c386fecf3ff321ac9ae913737c46c17bc6ce2413752144668a2a`
|
||||
2. 公钥 (65 bytes, uncompressed): `047e0b2f84204a2f859f51be78e09af3c504e9525f49d8ab1c537ab9c2a4deb28c3b16870449f50b9b79e959649a78144a5329958a95f6697534be0156b421588b`
|
||||
3. Keccak-256(公钥[1:65])
|
||||
4. 取后 20 bytes: `4f7e78d6b7c5fc502ec7039848690f08c8970f1e`
|
||||
5. 添加 0x 前缀: `0x4F7E78d6B7C5FC502Ec7039848690f08c8970F1E` (含校验和)
|
||||
|
||||
## 安全建议
|
||||
|
||||
1. **备份私钥**: 将私钥安全存储在离线环境中
|
||||
2. **不要分享**: 永远不要将私钥分享给任何人
|
||||
3. **不要提交**: 确保 .gitignore 包含此文件
|
||||
4. **硬件钱包**: 考虑将大额资产转移到硬件钱包
|
||||
5. **多签**: 对于生产环境,考虑使用多签钱包
|
||||
|
||||
## 相关交易
|
||||
|
||||
### 合约部署交易
|
||||
|
||||
- 交易哈希: `0xa73d7dce17723bc5a9d62767c515dadf4ccccc26327ed5637958ce817edd671d`
|
||||
- 查看: https://kavascan.com/tx/0xa73d7dce17723bc5a9d62767c515dadf4ccccc26327ed5637958ce817edd671d
|
||||
|
|
@ -377,14 +377,20 @@
|
|||
/* Balance Section */
|
||||
.balanceSection {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-sm);
|
||||
padding: var(--spacing-md);
|
||||
margin-bottom: var(--spacing-md);
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
border-radius: var(--radius-md);
|
||||
}
|
||||
|
||||
.balanceRow {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.balanceLabel {
|
||||
font-size: 12px;
|
||||
color: rgba(255, 255, 255, 0.85);
|
||||
|
|
@ -519,6 +525,42 @@
|
|||
color: white;
|
||||
}
|
||||
|
||||
/* Token Type Selector */
|
||||
.tokenTypeSelector {
|
||||
display: flex;
|
||||
gap: var(--spacing-sm);
|
||||
}
|
||||
|
||||
.tokenTypeButton {
|
||||
flex: 1;
|
||||
padding: var(--spacing-sm) var(--spacing-md);
|
||||
background-color: transparent;
|
||||
color: var(--text-secondary);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: var(--radius-md);
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.tokenTypeButton:hover {
|
||||
border-color: var(--primary-color);
|
||||
color: var(--primary-color);
|
||||
}
|
||||
|
||||
.tokenTypeActive {
|
||||
background-color: var(--primary-color);
|
||||
color: white;
|
||||
border-color: var(--primary-color);
|
||||
}
|
||||
|
||||
.tokenTypeActive:hover {
|
||||
background-color: var(--primary-light);
|
||||
border-color: var(--primary-light);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.transferError {
|
||||
background-color: rgba(220, 53, 69, 0.1);
|
||||
color: #dc3545;
|
||||
|
|
|
|||
|
|
@ -10,7 +10,10 @@ import {
|
|||
getCurrentNetwork,
|
||||
getCurrentRpcUrl,
|
||||
getGasPrice,
|
||||
fetchGreenPointsBalance,
|
||||
GREEN_POINTS_TOKEN,
|
||||
type PreparedTransaction,
|
||||
type TokenType,
|
||||
} from '../utils/transaction';
|
||||
|
||||
interface ShareItem {
|
||||
|
|
@ -28,6 +31,7 @@ interface ShareItem {
|
|||
interface ShareWithAddress extends ShareItem {
|
||||
evmAddress?: string;
|
||||
kavaBalance?: string;
|
||||
greenPointsBalance?: string;
|
||||
balanceLoading?: boolean;
|
||||
}
|
||||
|
||||
|
|
@ -78,6 +82,7 @@ export default function Home() {
|
|||
const [transferTo, setTransferTo] = useState('');
|
||||
const [transferAmount, setTransferAmount] = useState('');
|
||||
const [transferPassword, setTransferPassword] = useState('');
|
||||
const [transferTokenType, setTransferTokenType] = useState<TokenType>('KAVA');
|
||||
const [transferStep, setTransferStep] = useState<'input' | 'confirm' | 'preparing' | 'error'>('input');
|
||||
const [transferError, setTransferError] = useState<string | null>(null);
|
||||
const [preparedTx, setPreparedTx] = useState<PreparedTransaction | null>(null);
|
||||
|
|
@ -86,44 +91,56 @@ export default function Home() {
|
|||
|
||||
// 计算扣除 Gas 费后的最大可转账金额
|
||||
const calculateMaxAmount = async () => {
|
||||
if (!transferShare?.kavaBalance || !transferShare?.evmAddress) return;
|
||||
if (!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());
|
||||
if (transferTokenType === 'GREEN_POINTS') {
|
||||
// For token transfers, use the full token balance (gas is paid in KAVA)
|
||||
const balance = transferShare.greenPointsBalance || '0';
|
||||
setTransferAmount(balance);
|
||||
setTransferError(null);
|
||||
} else {
|
||||
// For KAVA transfers, deduct gas fee
|
||||
const balance = parseFloat(transferShare.kavaBalance || '0');
|
||||
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());
|
||||
if (transferTokenType === 'GREEN_POINTS') {
|
||||
setTransferAmount(transferShare.greenPointsBalance || '0');
|
||||
} else {
|
||||
// 如果获取 Gas 失败,使用默认估算 (1 gwei * 21000)
|
||||
const defaultGasFee = 0.000021; // ~21000 * 1 gwei
|
||||
const balance = parseFloat(transferShare.kavaBalance || '0');
|
||||
const maxAmount = Math.max(0, balance - defaultGasFee);
|
||||
const formattedMax = Math.floor(maxAmount * 1000000) / 1000000;
|
||||
setTransferAmount(formattedMax.toString());
|
||||
}
|
||||
} finally {
|
||||
setIsCalculatingMax(false);
|
||||
}
|
||||
|
|
@ -143,13 +160,17 @@ export default function Home() {
|
|||
return sharesWithAddresses;
|
||||
}, []);
|
||||
|
||||
// 单独获取所有钱包的余额
|
||||
// 单独获取所有钱包的余额 (KAVA 和 绿积分)
|
||||
const fetchAllBalances = useCallback(async (sharesWithAddrs: ShareWithAddress[]) => {
|
||||
const updatedShares = await Promise.all(
|
||||
sharesWithAddrs.map(async (share) => {
|
||||
if (share.evmAddress) {
|
||||
const kavaBalance = await fetchKavaBalance(share.evmAddress);
|
||||
return { ...share, kavaBalance, balanceLoading: false };
|
||||
// Fetch both balances in parallel
|
||||
const [kavaBalance, greenPointsBalance] = await Promise.all([
|
||||
fetchKavaBalance(share.evmAddress),
|
||||
fetchGreenPointsBalance(share.evmAddress),
|
||||
]);
|
||||
return { ...share, kavaBalance, greenPointsBalance, balanceLoading: false };
|
||||
}
|
||||
return { ...share, balanceLoading: false };
|
||||
})
|
||||
|
|
@ -265,6 +286,7 @@ export default function Home() {
|
|||
setTransferTo('');
|
||||
setTransferAmount('');
|
||||
setTransferPassword('');
|
||||
setTransferTokenType('KAVA');
|
||||
setTransferStep('input');
|
||||
setTransferError(null);
|
||||
setPreparedTx(null);
|
||||
|
|
@ -293,7 +315,11 @@ export default function Home() {
|
|||
return '转账金额无效';
|
||||
}
|
||||
const amount = parseFloat(transferAmount);
|
||||
const balance = parseFloat(transferShare?.kavaBalance || '0');
|
||||
const balance = parseFloat(
|
||||
transferTokenType === 'GREEN_POINTS'
|
||||
? (transferShare?.greenPointsBalance || '0')
|
||||
: (transferShare?.kavaBalance || '0')
|
||||
);
|
||||
if (amount > balance) {
|
||||
return '余额不足';
|
||||
}
|
||||
|
|
@ -316,6 +342,7 @@ export default function Home() {
|
|||
from: transferShare!.evmAddress!,
|
||||
to: transferTo.trim().toLowerCase(),
|
||||
value: transferAmount.trim(),
|
||||
tokenType: transferTokenType,
|
||||
});
|
||||
setPreparedTx(prepared);
|
||||
setTransferStep('confirm');
|
||||
|
|
@ -352,6 +379,7 @@ export default function Home() {
|
|||
amount: transferAmount,
|
||||
from: transferShare.evmAddress,
|
||||
walletName: transferShare.walletName,
|
||||
tokenType: transferTokenType,
|
||||
};
|
||||
sessionStorage.setItem(`tx_${result.sessionId}`, JSON.stringify(txToStore));
|
||||
|
||||
|
|
@ -458,17 +486,29 @@ export default function Home() {
|
|||
</div>
|
||||
)}
|
||||
|
||||
{/* KAVA 余额显示 */}
|
||||
{/* 余额显示 - KAVA 和 绿积分 */}
|
||||
{share.evmAddress && (
|
||||
<div className={styles.balanceSection}>
|
||||
<span className={styles.balanceLabel}>KAVA 余额</span>
|
||||
<span className={styles.balanceValue}>
|
||||
{share.balanceLoading ? (
|
||||
<span className={styles.balanceLoading}>加载中...</span>
|
||||
) : (
|
||||
<>{share.kavaBalance || '0'} KAVA</>
|
||||
)}
|
||||
</span>
|
||||
<div className={styles.balanceRow}>
|
||||
<span className={styles.balanceLabel}>KAVA</span>
|
||||
<span className={styles.balanceValue}>
|
||||
{share.balanceLoading ? (
|
||||
<span className={styles.balanceLoading}>加载中...</span>
|
||||
) : (
|
||||
<>{share.kavaBalance || '0'}</>
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
<div className={styles.balanceRow}>
|
||||
<span className={styles.balanceLabel} style={{ color: '#4CAF50' }}>{GREEN_POINTS_TOKEN.name}</span>
|
||||
<span className={styles.balanceValue} style={{ color: '#4CAF50' }}>
|
||||
{share.balanceLoading ? (
|
||||
<span className={styles.balanceLoading}>加载中...</span>
|
||||
) : (
|
||||
<>{share.greenPointsBalance || '0'}</>
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
|
@ -538,7 +578,7 @@ export default function Home() {
|
|||
<div className={styles.transferWalletInfo}>
|
||||
<div className={styles.transferWalletName}>{transferShare.walletName}</div>
|
||||
<div className={styles.transferWalletBalance}>
|
||||
余额: {transferShare.kavaBalance || '0'} KAVA
|
||||
KAVA: {transferShare.kavaBalance || '0'} | {GREEN_POINTS_TOKEN.name}: {transferShare.greenPointsBalance || '0'}
|
||||
</div>
|
||||
<div className={styles.transferNetwork}>
|
||||
网络: Kava {getCurrentNetwork() === 'mainnet' ? '主网' : '测试网'}
|
||||
|
|
@ -547,6 +587,26 @@ export default function Home() {
|
|||
|
||||
{transferStep === 'input' && (
|
||||
<div className={styles.transferForm}>
|
||||
{/* 代币类型选择 */}
|
||||
<div className={styles.transferInputGroup}>
|
||||
<label className={styles.transferLabel}>转账类型</label>
|
||||
<div className={styles.tokenTypeSelector}>
|
||||
<button
|
||||
className={`${styles.tokenTypeButton} ${transferTokenType === 'KAVA' ? styles.tokenTypeActive : ''}`}
|
||||
onClick={() => { setTransferTokenType('KAVA'); setTransferAmount(''); }}
|
||||
>
|
||||
KAVA
|
||||
</button>
|
||||
<button
|
||||
className={`${styles.tokenTypeButton} ${transferTokenType === 'GREEN_POINTS' ? styles.tokenTypeActive : ''}`}
|
||||
onClick={() => { setTransferTokenType('GREEN_POINTS'); setTransferAmount(''); }}
|
||||
style={transferTokenType === 'GREEN_POINTS' ? { backgroundColor: '#4CAF50', borderColor: '#4CAF50' } : {}}
|
||||
>
|
||||
{GREEN_POINTS_TOKEN.name}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 收款地址 */}
|
||||
<div className={styles.transferInputGroup}>
|
||||
<label className={styles.transferLabel}>收款地址</label>
|
||||
|
|
@ -561,7 +621,9 @@ export default function Home() {
|
|||
|
||||
{/* 转账金额 */}
|
||||
<div className={styles.transferInputGroup}>
|
||||
<label className={styles.transferLabel}>转账金额 (KAVA)</label>
|
||||
<label className={styles.transferLabel}>
|
||||
转账金额 ({transferTokenType === 'GREEN_POINTS' ? GREEN_POINTS_TOKEN.name : 'KAVA'})
|
||||
</label>
|
||||
<div className={styles.transferAmountWrapper}>
|
||||
<input
|
||||
type="text"
|
||||
|
|
@ -625,13 +687,21 @@ export default function Home() {
|
|||
<h3 className={styles.confirmTitle}>确认交易</h3>
|
||||
|
||||
<div className={styles.confirmDetails}>
|
||||
<div className={styles.confirmRow}>
|
||||
<span className={styles.confirmLabel}>转账类型</span>
|
||||
<span className={styles.confirmValue} style={transferTokenType === 'GREEN_POINTS' ? { color: '#4CAF50' } : {}}>
|
||||
{transferTokenType === 'GREEN_POINTS' ? GREEN_POINTS_TOKEN.name : 'KAVA'}
|
||||
</span>
|
||||
</div>
|
||||
<div className={styles.confirmRow}>
|
||||
<span className={styles.confirmLabel}>收款地址</span>
|
||||
<span className={styles.confirmValue}>{formatAddress(transferTo, 8, 6)}</span>
|
||||
</div>
|
||||
<div className={styles.confirmRow}>
|
||||
<span className={styles.confirmLabel}>转账金额</span>
|
||||
<span className={styles.confirmValue}>{transferAmount} KAVA</span>
|
||||
<span className={styles.confirmValue} style={transferTokenType === 'GREEN_POINTS' ? { color: '#4CAF50' } : {}}>
|
||||
{transferAmount} {transferTokenType === 'GREEN_POINTS' ? GREEN_POINTS_TOKEN.name : 'KAVA'}
|
||||
</span>
|
||||
</div>
|
||||
<div className={styles.confirmRow}>
|
||||
<span className={styles.confirmLabel}>预估 Gas 费</span>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
/**
|
||||
* Kava EVM 交易构建工具
|
||||
* 用于构建 EIP-1559 交易并计算签名哈希
|
||||
* 支持原生 KAVA 和 ERC-20 代币 (绿积分) 转账
|
||||
*/
|
||||
|
||||
// Kava EVM Chain IDs
|
||||
|
|
@ -15,6 +16,20 @@ export const KAVA_RPC_URL = {
|
|||
testnet: 'https://evm.testnet.kava.io',
|
||||
};
|
||||
|
||||
// Token types
|
||||
export type TokenType = 'KAVA' | 'GREEN_POINTS';
|
||||
|
||||
// Green Points (绿积分) Token Configuration
|
||||
export const GREEN_POINTS_TOKEN = {
|
||||
contractAddress: '0xA9F3A35dBa8699c8C681D8db03F0c1A8CEB9D7c3',
|
||||
name: '绿积分',
|
||||
symbol: 'dUSDT',
|
||||
decimals: 6,
|
||||
// ERC-20 function selectors
|
||||
balanceOfSelector: '0x70a08231',
|
||||
transferSelector: '0xa9059cbb',
|
||||
};
|
||||
|
||||
// 当前网络配置 (从 localStorage 读取或使用默认值)
|
||||
export function getCurrentNetwork(): 'mainnet' | 'testnet' {
|
||||
if (typeof window !== 'undefined' && window.localStorage) {
|
||||
|
|
@ -39,13 +54,14 @@ export function getCurrentRpcUrl(): string {
|
|||
*/
|
||||
export interface TransactionParams {
|
||||
to: string; // 收款地址
|
||||
value: string; // 转账金额 (KAVA, 字符串以支持大数)
|
||||
value: string; // 转账金额 (KAVA 或代币单位, 字符串以支持大数)
|
||||
from: string; // 发送地址
|
||||
nonce?: number; // 可选,自动获取
|
||||
gasLimit?: bigint; // 可选,默认 21000
|
||||
maxFeePerGas?: bigint; // 可选,自动获取
|
||||
maxPriorityFeePerGas?: bigint; // 可选,自动获取
|
||||
data?: string; // 可选,合约调用数据
|
||||
tokenType?: TokenType; // 可选,默认 KAVA
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -310,6 +326,88 @@ export function weiToKava(wei: bigint): string {
|
|||
return fraction ? `${whole}.${fraction}` : whole;
|
||||
}
|
||||
|
||||
/**
|
||||
* 将绿积分金额转换为最小单位 (6 decimals)
|
||||
*/
|
||||
export function greenPointsToRaw(amount: string): bigint {
|
||||
const parts = amount.split('.');
|
||||
const whole = BigInt(parts[0] || '0');
|
||||
let fraction = parts[1] || '';
|
||||
|
||||
// 补齐或截断到 6 位
|
||||
if (fraction.length > 6) {
|
||||
fraction = fraction.substring(0, 6);
|
||||
} else {
|
||||
fraction = fraction.padEnd(6, '0');
|
||||
}
|
||||
|
||||
return whole * BigInt(10 ** 6) + BigInt(fraction);
|
||||
}
|
||||
|
||||
/**
|
||||
* 将最小单位转换为绿积分金额
|
||||
*/
|
||||
export function rawToGreenPoints(raw: bigint): string {
|
||||
const rawStr = raw.toString().padStart(7, '0');
|
||||
const whole = rawStr.slice(0, -6) || '0';
|
||||
const fraction = rawStr.slice(-6).replace(/0+$/, '');
|
||||
return fraction ? `${whole}.${fraction}` : whole;
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询绿积分 (ERC-20) 余额
|
||||
*/
|
||||
export async function fetchGreenPointsBalance(address: string): Promise<string> {
|
||||
try {
|
||||
const rpcUrl = getCurrentRpcUrl();
|
||||
// Encode balanceOf(address) call data
|
||||
// Function selector: 0x70a08231
|
||||
// Address parameter: padded to 32 bytes
|
||||
const paddedAddress = address.toLowerCase().replace('0x', '').padStart(64, '0');
|
||||
const callData = GREEN_POINTS_TOKEN.balanceOfSelector + paddedAddress;
|
||||
|
||||
const response = await fetch(rpcUrl, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
jsonrpc: '2.0',
|
||||
method: 'eth_call',
|
||||
params: [
|
||||
{
|
||||
to: GREEN_POINTS_TOKEN.contractAddress,
|
||||
data: callData,
|
||||
},
|
||||
'latest',
|
||||
],
|
||||
id: 1,
|
||||
}),
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
if (data.result && data.result !== '0x') {
|
||||
const balanceRaw = BigInt(data.result);
|
||||
return rawToGreenPoints(balanceRaw);
|
||||
}
|
||||
return '0';
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch Green Points balance:', error);
|
||||
return '0';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Encode ERC-20 transfer function call
|
||||
*/
|
||||
function encodeErc20Transfer(to: string, amount: bigint): string {
|
||||
// Function selector: transfer(address,uint256) = 0xa9059cbb
|
||||
const selector = GREEN_POINTS_TOKEN.transferSelector;
|
||||
// Encode recipient address (padded to 32 bytes)
|
||||
const paddedAddress = to.toLowerCase().replace('0x', '').padStart(64, '0');
|
||||
// Encode amount (padded to 32 bytes)
|
||||
const amountHex = amount.toString(16).padStart(64, '0');
|
||||
return selector + paddedAddress + amountHex;
|
||||
}
|
||||
|
||||
/**
|
||||
* 通过 RPC 获取账户 nonce
|
||||
*/
|
||||
|
|
@ -371,29 +469,49 @@ export async function getGasPrice(): Promise<{ maxFeePerGas: bigint; maxPriority
|
|||
/**
|
||||
* 预估 gas 用量
|
||||
*/
|
||||
export async function estimateGas(params: { from: string; to: string; value: string; data?: string }): Promise<bigint> {
|
||||
export async function estimateGas(params: { from: string; to: string; value: string; data?: string; tokenType?: TokenType }): Promise<bigint> {
|
||||
const rpcUrl = getCurrentRpcUrl();
|
||||
const tokenType = params.tokenType || 'KAVA';
|
||||
|
||||
// For token transfers, we need different params
|
||||
let txParams: { from: string; to: string; value: string; data?: string };
|
||||
|
||||
if (tokenType === 'GREEN_POINTS') {
|
||||
// ERC-20 transfer: to is contract, value is 0, data is transfer call
|
||||
const tokenAmount = greenPointsToRaw(params.value);
|
||||
const transferData = encodeErc20Transfer(params.to, tokenAmount);
|
||||
txParams = {
|
||||
from: params.from,
|
||||
to: GREEN_POINTS_TOKEN.contractAddress,
|
||||
value: '0x0',
|
||||
data: transferData,
|
||||
};
|
||||
} else {
|
||||
// Native KAVA transfer
|
||||
txParams = {
|
||||
from: params.from,
|
||||
to: params.to,
|
||||
value: toHex(kavaToWei(params.value)),
|
||||
data: params.data || '0x',
|
||||
};
|
||||
}
|
||||
|
||||
const response = await fetch(rpcUrl, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
jsonrpc: '2.0',
|
||||
method: 'eth_estimateGas',
|
||||
params: [{
|
||||
from: params.from,
|
||||
to: params.to,
|
||||
value: toHex(kavaToWei(params.value)),
|
||||
data: params.data || '0x',
|
||||
}],
|
||||
params: [txParams],
|
||||
id: 1,
|
||||
}),
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
if (data.error) {
|
||||
// 如果估算失败,使用默认值 21000 (普通转账)
|
||||
// 如果估算失败,使用默认值
|
||||
console.warn('Gas 估算失败,使用默认值:', data.error);
|
||||
return BigInt(21000);
|
||||
return tokenType === 'GREEN_POINTS' ? BigInt(65000) : BigInt(21000);
|
||||
}
|
||||
return BigInt(data.result);
|
||||
}
|
||||
|
|
@ -402,9 +520,11 @@ export async function estimateGas(params: { from: string; to: string; value: str
|
|||
* 准备 Legacy 交易 (Type 0)
|
||||
* KAVA 不支持 EIP-1559,所以使用 Legacy 格式
|
||||
* 返回交易数据和待签名的哈希
|
||||
* 支持原生 KAVA 和 ERC-20 代币 (绿积分) 转账
|
||||
*/
|
||||
export async function prepareTransaction(params: TransactionParams): Promise<PreparedTransaction> {
|
||||
const chainId = getCurrentChainId();
|
||||
const tokenType = params.tokenType || 'KAVA';
|
||||
|
||||
// 获取或使用提供的参数
|
||||
const nonce = params.nonce ?? await getNonce(params.from);
|
||||
|
|
@ -415,10 +535,28 @@ export async function prepareTransaction(params: TransactionParams): Promise<Pre
|
|||
to: params.to,
|
||||
value: params.value,
|
||||
data: params.data,
|
||||
tokenType,
|
||||
});
|
||||
|
||||
const value = kavaToWei(params.value);
|
||||
const data = params.data || '0x';
|
||||
// Prepare transaction based on token type
|
||||
let toAddress: string;
|
||||
let value: bigint;
|
||||
let data: string;
|
||||
|
||||
if (tokenType === 'GREEN_POINTS') {
|
||||
// ERC-20 token transfer
|
||||
// To address is the contract, value is 0
|
||||
// Data is transfer(recipient, amount) encoded
|
||||
const tokenAmount = greenPointsToRaw(params.value);
|
||||
toAddress = GREEN_POINTS_TOKEN.contractAddress.toLowerCase();
|
||||
value = BigInt(0);
|
||||
data = encodeErc20Transfer(params.to, tokenAmount);
|
||||
} else {
|
||||
// Native KAVA transfer
|
||||
toAddress = params.to.toLowerCase();
|
||||
value = kavaToWei(params.value);
|
||||
data = params.data || '0x';
|
||||
}
|
||||
|
||||
// Legacy 交易字段顺序 (EIP-155)
|
||||
// [nonce, gasPrice, gasLimit, to, value, data, chainId, 0, 0]
|
||||
|
|
@ -427,7 +565,7 @@ export async function prepareTransaction(params: TransactionParams): Promise<Pre
|
|||
nonce,
|
||||
gasPrice,
|
||||
gasLimit,
|
||||
params.to.toLowerCase(),
|
||||
toAddress,
|
||||
value,
|
||||
data,
|
||||
chainId,
|
||||
|
|
@ -447,7 +585,7 @@ export async function prepareTransaction(params: TransactionParams): Promise<Pre
|
|||
nonce,
|
||||
gasPrice,
|
||||
gasLimit,
|
||||
to: params.to.toLowerCase(),
|
||||
to: toAddress,
|
||||
value,
|
||||
data,
|
||||
signHash,
|
||||
|
|
|
|||
Loading…
Reference in New Issue