feat(tss): 添加积分股(eUSDT)和积分值(fUSDT)代币支持
新增功能:
- 添加 ENERGY_POINTS (积分股/eUSDT) 和 FUTURE_POINTS (积分值/fUSDT) 代币类型
- 实现所有 ERC-20 代币的通用余额查询功能
- 支持四种代币的转账功能 (KAVA, dUSDT, eUSDT, fUSDT)
- 更新 UI 显示所有代币余额和代币选择器
代币合约地址 (Kava EVM):
- dUSDT (绿积分): 0xA9F3A35dBa8699c8E681D8db03F0c1A8CEB9D7c3
- eUSDT (积分股): 0x7C3275D808eFbAE90C06C7E3A9AfDdcAa8563931
- fUSDT (积分值): 0x14dc4f7d3E4197438d058C3D156dd9826A161134
技术改进:
- 添加 TokenConfig 工具类统一管理代币配置
- 添加 ERC20Selectors 常量类定义合约方法选择器
- 添加 transaction_records 表用于存储转账历史 (数据库版本升级到 v4)
- 重构余额查询和转账逻辑支持多代币类型
- 所有 ERC-20 代币使用 6 位小数精度
受影响文件:
- Android: Models.kt, TssRepository.kt, TransactionUtils.kt, Database.kt,
AppModule.kt, TransferScreen.kt, WalletsScreen.kt
- Electron: transaction.ts, Home.tsx
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
d332ef99a7
commit
c5db77d23a
|
|
@ -93,15 +93,159 @@ interface AppSettingDao {
|
|||
suspend fun setValue(setting: AppSettingEntity)
|
||||
}
|
||||
|
||||
/**
|
||||
* 转账记录数据库实体
|
||||
* Entity for storing transaction history records
|
||||
*/
|
||||
@Entity(
|
||||
tableName = "transaction_records",
|
||||
foreignKeys = [
|
||||
ForeignKey(
|
||||
entity = ShareRecordEntity::class,
|
||||
parentColumns = ["id"],
|
||||
childColumns = ["share_id"],
|
||||
onDelete = ForeignKey.CASCADE // 删除钱包时自动删除关联的转账记录
|
||||
)
|
||||
],
|
||||
indices = [
|
||||
Index(value = ["share_id"]),
|
||||
Index(value = ["tx_hash"], unique = true),
|
||||
Index(value = ["from_address"]),
|
||||
Index(value = ["to_address"]),
|
||||
Index(value = ["created_at"])
|
||||
]
|
||||
)
|
||||
data class TransactionRecordEntity(
|
||||
@PrimaryKey(autoGenerate = true)
|
||||
val id: Long = 0,
|
||||
|
||||
@ColumnInfo(name = "share_id")
|
||||
val shareId: Long, // 关联的钱包ID
|
||||
|
||||
@ColumnInfo(name = "from_address")
|
||||
val fromAddress: String, // 发送方地址
|
||||
|
||||
@ColumnInfo(name = "to_address")
|
||||
val toAddress: String, // 接收方地址
|
||||
|
||||
@ColumnInfo(name = "amount")
|
||||
val amount: String, // 转账金额(人类可读格式)
|
||||
|
||||
@ColumnInfo(name = "token_type")
|
||||
val tokenType: String, // 代币类型:KAVA, GREEN_POINTS, ENERGY_POINTS, FUTURE_POINTS
|
||||
|
||||
@ColumnInfo(name = "tx_hash")
|
||||
val txHash: String, // 交易哈希
|
||||
|
||||
@ColumnInfo(name = "gas_price")
|
||||
val gasPrice: String, // Gas 价格(Wei)
|
||||
|
||||
@ColumnInfo(name = "gas_used")
|
||||
val gasUsed: String = "", // 实际消耗的 Gas
|
||||
|
||||
@ColumnInfo(name = "tx_fee")
|
||||
val txFee: String = "", // 交易手续费
|
||||
|
||||
@ColumnInfo(name = "status")
|
||||
val status: String, // 交易状态:PENDING, CONFIRMED, FAILED
|
||||
|
||||
@ColumnInfo(name = "direction")
|
||||
val direction: String, // 交易方向:SENT, RECEIVED
|
||||
|
||||
@ColumnInfo(name = "note")
|
||||
val note: String = "", // 备注
|
||||
|
||||
@ColumnInfo(name = "created_at")
|
||||
val createdAt: Long = System.currentTimeMillis(),
|
||||
|
||||
@ColumnInfo(name = "confirmed_at")
|
||||
val confirmedAt: Long? = null, // 确认时间
|
||||
|
||||
@ColumnInfo(name = "block_number")
|
||||
val blockNumber: Long? = null // 区块高度
|
||||
)
|
||||
|
||||
/**
|
||||
* 转账记录 DAO
|
||||
* Data Access Object for transaction records
|
||||
*/
|
||||
@Dao
|
||||
interface TransactionRecordDao {
|
||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||
suspend fun insertRecord(record: TransactionRecordEntity): Long
|
||||
|
||||
@Query("SELECT * FROM transaction_records WHERE id = :id")
|
||||
suspend fun getRecordById(id: Long): TransactionRecordEntity?
|
||||
|
||||
@Query("SELECT * FROM transaction_records WHERE tx_hash = :txHash")
|
||||
suspend fun getRecordByTxHash(txHash: String): TransactionRecordEntity?
|
||||
|
||||
@Query("SELECT * FROM transaction_records WHERE share_id = :shareId ORDER BY created_at DESC")
|
||||
fun getRecordsForShare(shareId: Long): Flow<List<TransactionRecordEntity>>
|
||||
|
||||
@Query("SELECT * FROM transaction_records WHERE share_id = :shareId ORDER BY created_at DESC LIMIT :limit OFFSET :offset")
|
||||
suspend fun getRecordsForSharePaged(shareId: Long, limit: Int, offset: Int): List<TransactionRecordEntity>
|
||||
|
||||
@Query("SELECT * FROM transaction_records WHERE share_id = :shareId AND token_type = :tokenType ORDER BY created_at DESC")
|
||||
fun getRecordsForShareByToken(shareId: Long, tokenType: String): Flow<List<TransactionRecordEntity>>
|
||||
|
||||
@Query("SELECT * FROM transaction_records WHERE status = 'PENDING' ORDER BY created_at ASC")
|
||||
suspend fun getPendingRecords(): List<TransactionRecordEntity>
|
||||
|
||||
@Query("UPDATE transaction_records SET status = :status, confirmed_at = :confirmedAt, block_number = :blockNumber, gas_used = :gasUsed, tx_fee = :txFee WHERE id = :id")
|
||||
suspend fun updateStatus(id: Long, status: String, confirmedAt: Long?, blockNumber: Long?, gasUsed: String, txFee: String)
|
||||
|
||||
@Query("""
|
||||
SELECT
|
||||
COUNT(*) as total_count,
|
||||
SUM(CASE WHEN direction = 'SENT' THEN 1 ELSE 0 END) as sent_count,
|
||||
SUM(CASE WHEN direction = 'RECEIVED' THEN 1 ELSE 0 END) as received_count
|
||||
FROM transaction_records
|
||||
WHERE share_id = :shareId AND token_type = :tokenType
|
||||
""")
|
||||
suspend fun getTransactionStats(shareId: Long, tokenType: String): TransactionStats
|
||||
|
||||
@Query("SELECT COALESCE(SUM(CAST(amount AS REAL)), 0) FROM transaction_records WHERE share_id = :shareId AND token_type = :tokenType AND direction = 'SENT' AND status = 'CONFIRMED'")
|
||||
suspend fun getTotalSentAmount(shareId: Long, tokenType: String): Double
|
||||
|
||||
@Query("SELECT COALESCE(SUM(CAST(amount AS REAL)), 0) FROM transaction_records WHERE share_id = :shareId AND token_type = :tokenType AND direction = 'RECEIVED' AND status = 'CONFIRMED'")
|
||||
suspend fun getTotalReceivedAmount(shareId: Long, tokenType: String): Double
|
||||
|
||||
@Query("SELECT COALESCE(SUM(CAST(tx_fee AS REAL)), 0) FROM transaction_records WHERE share_id = :shareId AND direction = 'SENT' AND status = 'CONFIRMED'")
|
||||
suspend fun getTotalTxFee(shareId: Long): Double
|
||||
|
||||
@Query("DELETE FROM transaction_records WHERE id = :id")
|
||||
suspend fun deleteRecordById(id: Long)
|
||||
|
||||
@Query("DELETE FROM transaction_records WHERE share_id = :shareId")
|
||||
suspend fun deleteRecordsForShare(shareId: Long)
|
||||
|
||||
@Query("SELECT COUNT(*) FROM transaction_records WHERE share_id = :shareId")
|
||||
suspend fun getRecordCount(shareId: Long): Int
|
||||
}
|
||||
|
||||
/**
|
||||
* 转账统计数据类
|
||||
*/
|
||||
data class TransactionStats(
|
||||
@ColumnInfo(name = "total_count")
|
||||
val totalCount: Int,
|
||||
@ColumnInfo(name = "sent_count")
|
||||
val sentCount: Int,
|
||||
@ColumnInfo(name = "received_count")
|
||||
val receivedCount: Int
|
||||
)
|
||||
|
||||
/**
|
||||
* Room database
|
||||
*/
|
||||
@Database(
|
||||
entities = [ShareRecordEntity::class, AppSettingEntity::class],
|
||||
version = 3, // Version 3: added party_id column to share_records
|
||||
entities = [ShareRecordEntity::class, AppSettingEntity::class, TransactionRecordEntity::class],
|
||||
version = 4, // Version 4: added transaction_records table for transfer history
|
||||
exportSchema = false
|
||||
)
|
||||
abstract class TssDatabase : RoomDatabase() {
|
||||
abstract fun shareRecordDao(): ShareRecordDao
|
||||
abstract fun appSettingDao(): AppSettingDao
|
||||
abstract fun transactionRecordDao(): TransactionRecordDao
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,6 +5,8 @@ import com.durian.tssparty.data.local.AppSettingDao
|
|||
import com.durian.tssparty.data.local.AppSettingEntity
|
||||
import com.durian.tssparty.data.local.ShareRecordDao
|
||||
import com.durian.tssparty.data.local.ShareRecordEntity
|
||||
import com.durian.tssparty.data.local.TransactionRecordDao
|
||||
import com.durian.tssparty.data.local.TransactionRecordEntity
|
||||
import com.durian.tssparty.data.local.TssNativeBridge
|
||||
import com.durian.tssparty.data.remote.GrpcClient
|
||||
import com.durian.tssparty.data.remote.GrpcConnectionEvent
|
||||
|
|
@ -31,7 +33,8 @@ class TssRepository @Inject constructor(
|
|||
private val grpcClient: GrpcClient,
|
||||
private val tssNativeBridge: TssNativeBridge,
|
||||
private val shareRecordDao: ShareRecordDao,
|
||||
private val appSettingDao: AppSettingDao
|
||||
private val appSettingDao: AppSettingDao,
|
||||
private val transactionRecordDao: TransactionRecordDao
|
||||
) {
|
||||
private val _currentSession = MutableStateFlow<TssSession?>(null)
|
||||
val currentSession: StateFlow<TssSession?> = _currentSession.asStateFlow()
|
||||
|
|
@ -1996,10 +1999,19 @@ class TssRepository @Inject constructor(
|
|||
}
|
||||
|
||||
/**
|
||||
* Get Green Points (绿积分/dUSDT) token balance for an address
|
||||
* Get ERC-20 token balance for an address
|
||||
* Uses eth_call to call balanceOf(address) on the ERC-20 contract
|
||||
* @param address The wallet address
|
||||
* @param rpcUrl The RPC endpoint URL
|
||||
* @param contractAddress The ERC-20 token contract address
|
||||
* @param decimals The token decimals (default 6 for USDT-like tokens)
|
||||
*/
|
||||
suspend fun getGreenPointsBalance(address: String, rpcUrl: String): Result<String> {
|
||||
suspend fun getERC20Balance(
|
||||
address: String,
|
||||
rpcUrl: String,
|
||||
contractAddress: String,
|
||||
decimals: Int = 6
|
||||
): Result<String> {
|
||||
return withContext(Dispatchers.IO) {
|
||||
try {
|
||||
val client = okhttp3.OkHttpClient()
|
||||
|
|
@ -2009,14 +2021,14 @@ class TssRepository @Inject constructor(
|
|||
// 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 callData = "${ERC20Selectors.BALANCE_OF}$paddedAddress"
|
||||
|
||||
val requestBody = """
|
||||
{
|
||||
"jsonrpc": "2.0",
|
||||
"method": "eth_call",
|
||||
"params": [{
|
||||
"to": "${GreenPointsToken.CONTRACT_ADDRESS}",
|
||||
"to": "$contractAddress",
|
||||
"data": "$callData"
|
||||
}, "latest"],
|
||||
"id": 1
|
||||
|
|
@ -2038,42 +2050,88 @@ class TssRepository @Inject constructor(
|
|||
}
|
||||
|
||||
val hexBalance = json.get("result").asString
|
||||
// Convert hex to decimal, then apply 6 decimals (dUSDT uses 6 decimals like USDT)
|
||||
// Convert hex to decimal, then apply decimals
|
||||
val rawBalance = java.math.BigInteger(hexBalance.removePrefix("0x"), 16)
|
||||
val divisor = java.math.BigDecimal.TEN.pow(decimals)
|
||||
val tokenBalance = java.math.BigDecimal(rawBalance).divide(
|
||||
java.math.BigDecimal("1000000"), // 10^6 for 6 decimals
|
||||
6,
|
||||
divisor,
|
||||
decimals,
|
||||
java.math.RoundingMode.DOWN
|
||||
)
|
||||
|
||||
Result.success(tokenBalance.toPlainString())
|
||||
} catch (e: Exception) {
|
||||
android.util.Log.e("TssRepository", "Failed to get Green Points balance: ${e.message}")
|
||||
android.util.Log.e("TssRepository", "Failed to get ERC20 balance for $contractAddress: ${e.message}")
|
||||
Result.failure(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get both KAVA and Green Points balances for an address
|
||||
* 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 getERC20Balance(
|
||||
address = address,
|
||||
rpcUrl = rpcUrl,
|
||||
contractAddress = GreenPointsToken.CONTRACT_ADDRESS,
|
||||
decimals = GreenPointsToken.DECIMALS
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get Energy Points (积分股/eUSDT) token balance for an address
|
||||
*/
|
||||
suspend fun getEnergyPointsBalance(address: String, rpcUrl: String): Result<String> {
|
||||
return getERC20Balance(
|
||||
address = address,
|
||||
rpcUrl = rpcUrl,
|
||||
contractAddress = EnergyPointsToken.CONTRACT_ADDRESS,
|
||||
decimals = EnergyPointsToken.DECIMALS
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get Future Points (积分值/fUSDT) token balance for an address
|
||||
*/
|
||||
suspend fun getFuturePointsBalance(address: String, rpcUrl: String): Result<String> {
|
||||
return getERC20Balance(
|
||||
address = address,
|
||||
rpcUrl = rpcUrl,
|
||||
contractAddress = FuturePointsToken.CONTRACT_ADDRESS,
|
||||
decimals = FuturePointsToken.DECIMALS
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all token balances for an address (KAVA + all ERC-20 tokens)
|
||||
*/
|
||||
suspend fun getWalletBalance(address: String, rpcUrl: String): Result<WalletBalance> {
|
||||
return withContext(Dispatchers.IO) {
|
||||
try {
|
||||
// Fetch both balances in parallel
|
||||
// Fetch all balances in parallel
|
||||
val kavaDeferred = async { getBalance(address, rpcUrl) }
|
||||
val greenPointsDeferred = async { getGreenPointsBalance(address, rpcUrl) }
|
||||
val energyPointsDeferred = async { getEnergyPointsBalance(address, rpcUrl) }
|
||||
val futurePointsDeferred = async { getFuturePointsBalance(address, rpcUrl) }
|
||||
|
||||
val kavaResult = kavaDeferred.await()
|
||||
val greenPointsResult = greenPointsDeferred.await()
|
||||
val energyPointsResult = energyPointsDeferred.await()
|
||||
val futurePointsResult = futurePointsDeferred.await()
|
||||
|
||||
val kavaBalance = kavaResult.getOrDefault("0")
|
||||
val greenPointsBalance = greenPointsResult.getOrDefault("0")
|
||||
val energyPointsBalance = energyPointsResult.getOrDefault("0")
|
||||
val futurePointsBalance = futurePointsResult.getOrDefault("0")
|
||||
|
||||
Result.success(WalletBalance(
|
||||
address = address,
|
||||
kavaBalance = kavaBalance,
|
||||
greenPointsBalance = greenPointsBalance
|
||||
greenPointsBalance = greenPointsBalance,
|
||||
energyPointsBalance = energyPointsBalance,
|
||||
futurePointsBalance = futurePointsBalance
|
||||
))
|
||||
} catch (e: Exception) {
|
||||
Result.failure(e)
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import androidx.room.migration.Migration
|
|||
import androidx.sqlite.db.SupportSQLiteDatabase
|
||||
import com.durian.tssparty.data.local.AppSettingDao
|
||||
import com.durian.tssparty.data.local.ShareRecordDao
|
||||
import com.durian.tssparty.data.local.TransactionRecordDao
|
||||
import com.durian.tssparty.data.local.TssDatabase
|
||||
import com.durian.tssparty.data.local.TssNativeBridge
|
||||
import com.durian.tssparty.data.remote.GrpcClient
|
||||
|
|
@ -45,6 +46,42 @@ object AppModule {
|
|||
}
|
||||
}
|
||||
|
||||
// Migration from version 3 to 4: add transaction_records table for transfer history
|
||||
// 添加转账记录表,用于存储交易历史和分类账
|
||||
private val MIGRATION_3_4 = object : Migration(3, 4) {
|
||||
override fun migrate(database: SupportSQLiteDatabase) {
|
||||
// 创建转账记录表
|
||||
database.execSQL("""
|
||||
CREATE TABLE IF NOT EXISTS `transaction_records` (
|
||||
`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
|
||||
`share_id` INTEGER NOT NULL,
|
||||
`from_address` TEXT NOT NULL,
|
||||
`to_address` TEXT NOT NULL,
|
||||
`amount` TEXT NOT NULL,
|
||||
`token_type` TEXT NOT NULL,
|
||||
`tx_hash` TEXT NOT NULL,
|
||||
`gas_price` TEXT NOT NULL,
|
||||
`gas_used` TEXT NOT NULL DEFAULT '',
|
||||
`tx_fee` TEXT NOT NULL DEFAULT '',
|
||||
`status` TEXT NOT NULL,
|
||||
`direction` TEXT NOT NULL,
|
||||
`note` TEXT NOT NULL DEFAULT '',
|
||||
`created_at` INTEGER NOT NULL,
|
||||
`confirmed_at` INTEGER,
|
||||
`block_number` INTEGER,
|
||||
FOREIGN KEY(`share_id`) REFERENCES `share_records`(`id`) ON DELETE CASCADE
|
||||
)
|
||||
""".trimIndent())
|
||||
|
||||
// 创建索引以优化查询性能
|
||||
database.execSQL("CREATE INDEX IF NOT EXISTS `index_transaction_records_share_id` ON `transaction_records` (`share_id`)")
|
||||
database.execSQL("CREATE UNIQUE INDEX IF NOT EXISTS `index_transaction_records_tx_hash` ON `transaction_records` (`tx_hash`)")
|
||||
database.execSQL("CREATE INDEX IF NOT EXISTS `index_transaction_records_from_address` ON `transaction_records` (`from_address`)")
|
||||
database.execSQL("CREATE INDEX IF NOT EXISTS `index_transaction_records_to_address` ON `transaction_records` (`to_address`)")
|
||||
database.execSQL("CREATE INDEX IF NOT EXISTS `index_transaction_records_created_at` ON `transaction_records` (`created_at`)")
|
||||
}
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideGson(): Gson {
|
||||
|
|
@ -59,7 +96,7 @@ object AppModule {
|
|||
TssDatabase::class.java,
|
||||
"tss_party.db"
|
||||
)
|
||||
.addMigrations(MIGRATION_1_2, MIGRATION_2_3)
|
||||
.addMigrations(MIGRATION_1_2, MIGRATION_2_3, MIGRATION_3_4)
|
||||
.build()
|
||||
}
|
||||
|
||||
|
|
@ -75,6 +112,12 @@ object AppModule {
|
|||
return database.appSettingDao()
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideTransactionRecordDao(database: TssDatabase): TransactionRecordDao {
|
||||
return database.transactionRecordDao()
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideGrpcClient(): GrpcClient {
|
||||
|
|
@ -93,8 +136,9 @@ object AppModule {
|
|||
grpcClient: GrpcClient,
|
||||
tssNativeBridge: TssNativeBridge,
|
||||
shareRecordDao: ShareRecordDao,
|
||||
appSettingDao: AppSettingDao
|
||||
appSettingDao: AppSettingDao,
|
||||
transactionRecordDao: TransactionRecordDao
|
||||
): TssRepository {
|
||||
return TssRepository(grpcClient, tssNativeBridge, shareRecordDao, appSettingDao)
|
||||
return TssRepository(grpcClient, tssNativeBridge, shareRecordDao, appSettingDao, transactionRecordDao)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -130,7 +130,21 @@ enum class NetworkType {
|
|||
*/
|
||||
enum class TokenType {
|
||||
KAVA, // Native KAVA token
|
||||
GREEN_POINTS // 绿积分 (dUSDT) ERC-20 token
|
||||
GREEN_POINTS, // 绿积分 (dUSDT) ERC-20 token
|
||||
ENERGY_POINTS, // 积分股 (eUSDT) ERC-20 token
|
||||
FUTURE_POINTS // 积分值 (fUSDT) ERC-20 token
|
||||
}
|
||||
|
||||
/**
|
||||
* ERC-20 通用函数签名(keccak256 哈希的前4字节)
|
||||
* Common ERC-20 function selectors
|
||||
*/
|
||||
object ERC20Selectors {
|
||||
const val BALANCE_OF = "0x70a08231" // balanceOf(address)
|
||||
const val TRANSFER = "0xa9059cbb" // transfer(address,uint256)
|
||||
const val APPROVE = "0x095ea7b3" // approve(address,uint256)
|
||||
const val ALLOWANCE = "0xdd62ed3e" // allowance(address,address)
|
||||
const val TOTAL_SUPPLY = "0x18160ddd" // totalSupply()
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -143,22 +157,122 @@ object GreenPointsToken {
|
|||
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()
|
||||
// ERC-20 function signatures (kept for backward compatibility)
|
||||
const val BALANCE_OF_SELECTOR = ERC20Selectors.BALANCE_OF
|
||||
const val TRANSFER_SELECTOR = ERC20Selectors.TRANSFER
|
||||
const val APPROVE_SELECTOR = ERC20Selectors.APPROVE
|
||||
const val ALLOWANCE_SELECTOR = ERC20Selectors.ALLOWANCE
|
||||
const val TOTAL_SUPPLY_SELECTOR = ERC20Selectors.TOTAL_SUPPLY
|
||||
}
|
||||
|
||||
/**
|
||||
* Wallet balance containing both native and token balances
|
||||
* Energy Points (积分股) Token Contract Configuration
|
||||
* eUSDT - ERC-20 token on Kava EVM
|
||||
* 总供应量:100.02亿 (10,002,000,000)
|
||||
*/
|
||||
object EnergyPointsToken {
|
||||
const val CONTRACT_ADDRESS = "0x7C3275D808eFbAE90C06C7E3A9AfDdcAa8563931"
|
||||
const val NAME = "积分股"
|
||||
const val SYMBOL = "eUSDT"
|
||||
const val DECIMALS = 6 // 与 dUSDT 相同的精度
|
||||
}
|
||||
|
||||
/**
|
||||
* Future Points (积分值) Token Contract Configuration
|
||||
* fUSDT - ERC-20 token on Kava EVM
|
||||
* 总供应量:1万亿 (1,000,000,000,000)
|
||||
*/
|
||||
object FuturePointsToken {
|
||||
const val CONTRACT_ADDRESS = "0x14dc4f7d3E4197438d058C3D156dd9826A161134"
|
||||
const val NAME = "积分值"
|
||||
const val SYMBOL = "fUSDT"
|
||||
const val DECIMALS = 6 // 与 dUSDT 相同的精度
|
||||
}
|
||||
|
||||
/**
|
||||
* 代币配置工具类
|
||||
* Token configuration utility
|
||||
*/
|
||||
object TokenConfig {
|
||||
/**
|
||||
* 获取代币合约地址
|
||||
*/
|
||||
fun getContractAddress(tokenType: TokenType): String? {
|
||||
return when (tokenType) {
|
||||
TokenType.KAVA -> null // 原生代币无合约地址
|
||||
TokenType.GREEN_POINTS -> GreenPointsToken.CONTRACT_ADDRESS
|
||||
TokenType.ENERGY_POINTS -> EnergyPointsToken.CONTRACT_ADDRESS
|
||||
TokenType.FUTURE_POINTS -> FuturePointsToken.CONTRACT_ADDRESS
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取代币精度
|
||||
*/
|
||||
fun getDecimals(tokenType: TokenType): Int {
|
||||
return when (tokenType) {
|
||||
TokenType.KAVA -> 18 // KAVA 原生代币精度
|
||||
TokenType.GREEN_POINTS -> GreenPointsToken.DECIMALS
|
||||
TokenType.ENERGY_POINTS -> EnergyPointsToken.DECIMALS
|
||||
TokenType.FUTURE_POINTS -> FuturePointsToken.DECIMALS
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取代币名称
|
||||
*/
|
||||
fun getName(tokenType: TokenType): String {
|
||||
return when (tokenType) {
|
||||
TokenType.KAVA -> "KAVA"
|
||||
TokenType.GREEN_POINTS -> GreenPointsToken.NAME
|
||||
TokenType.ENERGY_POINTS -> EnergyPointsToken.NAME
|
||||
TokenType.FUTURE_POINTS -> FuturePointsToken.NAME
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取代币符号
|
||||
*/
|
||||
fun getSymbol(tokenType: TokenType): String {
|
||||
return when (tokenType) {
|
||||
TokenType.KAVA -> "KAVA"
|
||||
TokenType.GREEN_POINTS -> GreenPointsToken.SYMBOL
|
||||
TokenType.ENERGY_POINTS -> EnergyPointsToken.SYMBOL
|
||||
TokenType.FUTURE_POINTS -> FuturePointsToken.SYMBOL
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断是否为 ERC-20 代币
|
||||
*/
|
||||
fun isERC20(tokenType: TokenType): Boolean {
|
||||
return tokenType != TokenType.KAVA
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Wallet balance containing native and all token balances
|
||||
* 钱包余额,包含原生代币和所有 ERC-20 代币余额
|
||||
*/
|
||||
data class WalletBalance(
|
||||
val address: String,
|
||||
val kavaBalance: String = "0", // Native KAVA balance
|
||||
val greenPointsBalance: String = "0" // 绿积分 (dUSDT) balance
|
||||
)
|
||||
val kavaBalance: String = "0", // Native KAVA balance
|
||||
val greenPointsBalance: String = "0", // 绿积分 (dUSDT) balance
|
||||
val energyPointsBalance: String = "0", // 积分股 (eUSDT) balance
|
||||
val futurePointsBalance: String = "0" // 积分值 (fUSDT) balance
|
||||
) {
|
||||
/**
|
||||
* 根据代币类型获取余额
|
||||
*/
|
||||
fun getBalance(tokenType: TokenType): String {
|
||||
return when (tokenType) {
|
||||
TokenType.KAVA -> kavaBalance
|
||||
TokenType.GREEN_POINTS -> greenPointsBalance
|
||||
TokenType.ENERGY_POINTS -> energyPointsBalance
|
||||
TokenType.FUTURE_POINTS -> futurePointsBalance
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Share backup data for export/import
|
||||
|
|
|
|||
|
|
@ -27,10 +27,13 @@ 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.EnergyPointsToken
|
||||
import com.durian.tssparty.domain.model.FuturePointsToken
|
||||
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.TokenConfig
|
||||
import com.durian.tssparty.domain.model.TokenType
|
||||
import com.durian.tssparty.domain.model.WalletBalance
|
||||
import com.durian.tssparty.util.TransactionUtils
|
||||
|
|
@ -156,10 +159,8 @@ fun TransferScreen(
|
|||
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
|
||||
}
|
||||
val currentBalance = walletBalance?.getBalance(selectedTokenType)
|
||||
?: if (selectedTokenType == TokenType.KAVA) balance else null
|
||||
when {
|
||||
toAddress.isBlank() -> validationError = "请输入收款地址"
|
||||
!toAddress.startsWith("0x") || toAddress.length != 42 -> validationError = "地址格式不正确"
|
||||
|
|
@ -257,14 +258,9 @@ private fun TransferInputScreen(
|
|||
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
|
||||
}
|
||||
val currentBalance = walletBalance?.getBalance(selectedTokenType)
|
||||
?: if (selectedTokenType == TokenType.KAVA) balance else null
|
||||
val tokenSymbol = TokenConfig.getName(selectedTokenType)
|
||||
|
||||
Column(
|
||||
modifier = Modifier
|
||||
|
|
@ -293,38 +289,74 @@ private fun TransferInputScreen(
|
|||
)
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
|
||||
// Show both balances
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween
|
||||
) {
|
||||
// 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
|
||||
)
|
||||
// Show all token balances in a 2x2 grid
|
||||
Column {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween
|
||||
) {
|
||||
// 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)
|
||||
)
|
||||
}
|
||||
}
|
||||
// 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(4.dp))
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween
|
||||
) {
|
||||
// Energy Points balance (积分股)
|
||||
Column {
|
||||
Text(
|
||||
text = EnergyPointsToken.NAME,
|
||||
style = MaterialTheme.typography.labelSmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
Text(
|
||||
text = walletBalance?.energyPointsBalance ?: "加载中...",
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
fontWeight = FontWeight.Medium,
|
||||
color = Color(0xFF2196F3) // Blue
|
||||
)
|
||||
}
|
||||
// Future Points balance (积分值)
|
||||
Column(horizontalAlignment = Alignment.End) {
|
||||
Text(
|
||||
text = FuturePointsToken.NAME,
|
||||
style = MaterialTheme.typography.labelSmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
Text(
|
||||
text = walletBalance?.futurePointsBalance ?: "加载中...",
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
fontWeight = FontWeight.Medium,
|
||||
color = Color(0xFF9C27B0) // Purple
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -339,6 +371,7 @@ private fun TransferInputScreen(
|
|||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
// First row: KAVA and Green Points
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
||||
|
|
@ -359,7 +392,7 @@ private fun TransferInputScreen(
|
|||
},
|
||||
modifier = Modifier.weight(1f)
|
||||
)
|
||||
// Green Points option
|
||||
// Green Points option (绿积分)
|
||||
FilterChip(
|
||||
selected = selectedTokenType == TokenType.GREEN_POINTS,
|
||||
onClick = { onTokenTypeChange(TokenType.GREEN_POINTS) },
|
||||
|
|
@ -380,6 +413,53 @@ private fun TransferInputScreen(
|
|||
modifier = Modifier.weight(1f)
|
||||
)
|
||||
}
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
// Second row: Energy Points and Future Points
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
// Energy Points option (积分股)
|
||||
FilterChip(
|
||||
selected = selectedTokenType == TokenType.ENERGY_POINTS,
|
||||
onClick = { onTokenTypeChange(TokenType.ENERGY_POINTS) },
|
||||
label = { Text(EnergyPointsToken.NAME) },
|
||||
leadingIcon = {
|
||||
if (selectedTokenType == TokenType.ENERGY_POINTS) {
|
||||
Icon(
|
||||
Icons.Default.Check,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(18.dp)
|
||||
)
|
||||
}
|
||||
},
|
||||
colors = FilterChipDefaults.filterChipColors(
|
||||
selectedContainerColor = Color(0xFF2196F3).copy(alpha = 0.2f),
|
||||
selectedLabelColor = Color(0xFF2196F3)
|
||||
),
|
||||
modifier = Modifier.weight(1f)
|
||||
)
|
||||
// Future Points option (积分值)
|
||||
FilterChip(
|
||||
selected = selectedTokenType == TokenType.FUTURE_POINTS,
|
||||
onClick = { onTokenTypeChange(TokenType.FUTURE_POINTS) },
|
||||
label = { Text(FuturePointsToken.NAME) },
|
||||
leadingIcon = {
|
||||
if (selectedTokenType == TokenType.FUTURE_POINTS) {
|
||||
Icon(
|
||||
Icons.Default.Check,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(18.dp)
|
||||
)
|
||||
}
|
||||
},
|
||||
colors = FilterChipDefaults.filterChipColors(
|
||||
selectedContainerColor = Color(0xFF9C27B0).copy(alpha = 0.2f),
|
||||
selectedLabelColor = Color(0xFF9C27B0)
|
||||
),
|
||||
modifier = Modifier.weight(1f)
|
||||
)
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
|
||||
|
|
@ -418,9 +498,14 @@ private fun TransferInputScreen(
|
|||
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Decimal),
|
||||
leadingIcon = {
|
||||
Icon(
|
||||
if (selectedTokenType == TokenType.GREEN_POINTS) Icons.Default.Stars else Icons.Default.AttachMoney,
|
||||
if (selectedTokenType == TokenType.KAVA) Icons.Default.AttachMoney else Icons.Default.Stars,
|
||||
contentDescription = null,
|
||||
tint = if (selectedTokenType == TokenType.GREEN_POINTS) Color(0xFF4CAF50) else MaterialTheme.colorScheme.onSurfaceVariant
|
||||
tint = when (selectedTokenType) {
|
||||
TokenType.KAVA -> MaterialTheme.colorScheme.onSurfaceVariant
|
||||
TokenType.GREEN_POINTS -> Color(0xFF4CAF50)
|
||||
TokenType.ENERGY_POINTS -> Color(0xFF2196F3)
|
||||
TokenType.FUTURE_POINTS -> Color(0xFF9C27B0)
|
||||
}
|
||||
)
|
||||
},
|
||||
trailingIcon = {
|
||||
|
|
@ -439,7 +524,7 @@ private fun TransferInputScreen(
|
|||
onAmountChange(currentBalance)
|
||||
}
|
||||
} else {
|
||||
// For tokens, use the full balance
|
||||
// For ERC-20 tokens (dUSDT, eUSDT, fUSDT), use the full balance
|
||||
onAmountChange(currentBalance)
|
||||
}
|
||||
isCalculatingMax = false
|
||||
|
|
|
|||
|
|
@ -35,6 +35,8 @@ 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.EnergyPointsToken
|
||||
import com.durian.tssparty.domain.model.FuturePointsToken
|
||||
import com.durian.tssparty.domain.model.GreenPointsToken
|
||||
import com.durian.tssparty.domain.model.NetworkType
|
||||
import com.durian.tssparty.domain.model.ShareRecord
|
||||
|
|
@ -281,62 +283,123 @@ private fun WalletItemCard(
|
|||
|
||||
Spacer(modifier = Modifier.height(12.dp))
|
||||
|
||||
// Balance display - now shows both KAVA and Green Points
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween
|
||||
) {
|
||||
// KAVA balance
|
||||
Column {
|
||||
Text(
|
||||
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))
|
||||
// Balance display - shows all token balances in a 2x2 grid
|
||||
Column {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween
|
||||
) {
|
||||
// KAVA balance
|
||||
Column {
|
||||
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
|
||||
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)
|
||||
)
|
||||
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
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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))
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween
|
||||
) {
|
||||
// Energy Points (积分股) balance
|
||||
Column {
|
||||
Text(
|
||||
text = walletBalance?.greenPointsBalance ?: "加载中...",
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = if (walletBalance != null)
|
||||
Color(0xFF4CAF50)
|
||||
else
|
||||
MaterialTheme.colorScheme.outline,
|
||||
fontWeight = FontWeight.Medium
|
||||
text = EnergyPointsToken.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(0xFF2196F3) // Blue
|
||||
)
|
||||
Spacer(modifier = Modifier.width(4.dp))
|
||||
Text(
|
||||
text = walletBalance?.energyPointsBalance ?: "加载中...",
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = if (walletBalance != null)
|
||||
Color(0xFF2196F3)
|
||||
else
|
||||
MaterialTheme.colorScheme.outline,
|
||||
fontWeight = FontWeight.Medium
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Future Points (积分值) balance
|
||||
Column(horizontalAlignment = Alignment.End) {
|
||||
Text(
|
||||
text = FuturePointsToken.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(0xFF9C27B0) // Purple
|
||||
)
|
||||
Spacer(modifier = Modifier.width(4.dp))
|
||||
Text(
|
||||
text = walletBalance?.futurePointsBalance ?: "加载中...",
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = if (walletBalance != null)
|
||||
Color(0xFF9C27B0)
|
||||
else
|
||||
MaterialTheme.colorScheme.outline,
|
||||
fontWeight = FontWeight.Medium
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,10 @@
|
|||
package com.durian.tssparty.util
|
||||
|
||||
import com.durian.tssparty.domain.model.ERC20Selectors
|
||||
import com.durian.tssparty.domain.model.EnergyPointsToken
|
||||
import com.durian.tssparty.domain.model.FuturePointsToken
|
||||
import com.durian.tssparty.domain.model.GreenPointsToken
|
||||
import com.durian.tssparty.domain.model.TokenConfig
|
||||
import com.durian.tssparty.domain.model.TokenType
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
|
|
@ -61,7 +65,7 @@ object TransactionUtils {
|
|||
/**
|
||||
* 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 (绿积分)
|
||||
* Supports both native KAVA transfers and ERC-20 token transfers (绿积分/积分股/积分值)
|
||||
*/
|
||||
suspend fun prepareTransaction(params: TransactionParams): Result<PreparedTransaction> = withContext(Dispatchers.IO) {
|
||||
try {
|
||||
|
|
@ -77,13 +81,16 @@ object TransactionUtils {
|
|||
// Native KAVA transfer
|
||||
Triple(params.to, kavaToWei(params.amount), ByteArray(0))
|
||||
}
|
||||
TokenType.GREEN_POINTS -> {
|
||||
// ERC-20 token transfer (绿积分)
|
||||
TokenType.GREEN_POINTS, TokenType.ENERGY_POINTS, TokenType.FUTURE_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 contractAddress = TokenConfig.getContractAddress(params.tokenType)
|
||||
?: return@withContext Result.failure(Exception("Invalid token type"))
|
||||
val decimals = TokenConfig.getDecimals(params.tokenType)
|
||||
val tokenAmount = tokenToRaw(params.amount, decimals)
|
||||
val transferData = encodeErc20Transfer(params.to, tokenAmount)
|
||||
Triple(GreenPointsToken.CONTRACT_ADDRESS, BigInteger.ZERO, transferData)
|
||||
Triple(contractAddress, BigInteger.ZERO, transferData)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -98,7 +105,7 @@ object TransactionUtils {
|
|||
// Default gas limits
|
||||
when (params.tokenType) {
|
||||
TokenType.KAVA -> BigInteger.valueOf(21000)
|
||||
TokenType.GREEN_POINTS -> BigInteger.valueOf(65000) // ERC-20 transfers need more gas
|
||||
else -> BigInteger.valueOf(65000) // ERC-20 transfers need more gas
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -139,7 +146,7 @@ object TransactionUtils {
|
|||
*/
|
||||
private fun encodeErc20Transfer(to: String, amount: BigInteger): ByteArray {
|
||||
// Function selector: transfer(address,uint256) = 0xa9059cbb
|
||||
val selector = GreenPointsToken.TRANSFER_SELECTOR.removePrefix("0x").hexToByteArray()
|
||||
val selector = ERC20Selectors.TRANSFER.removePrefix("0x").hexToByteArray()
|
||||
|
||||
// Encode recipient address (padded to 32 bytes)
|
||||
val paddedAddress = to.removePrefix("0x").lowercase().padStart(64, '0').hexToByteArray()
|
||||
|
|
@ -152,21 +159,43 @@ object TransactionUtils {
|
|||
}
|
||||
|
||||
/**
|
||||
* Convert Green Points amount to raw units (6 decimals)
|
||||
* Convert token amount to raw units based on decimals
|
||||
* @param amount Human-readable amount (e.g., "100.5")
|
||||
* @param decimals Token decimals (e.g., 6 for USDT-like tokens, 18 for native)
|
||||
*/
|
||||
fun greenPointsToRaw(amount: String): BigInteger {
|
||||
fun tokenToRaw(amount: String, decimals: Int): BigInteger {
|
||||
val decimal = BigDecimal(amount)
|
||||
val rawDecimal = decimal.multiply(BigDecimal("1000000")) // 10^6
|
||||
val multiplier = BigDecimal.TEN.pow(decimals)
|
||||
val rawDecimal = decimal.multiply(multiplier)
|
||||
return rawDecimal.toBigInteger()
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert raw units to human-readable token amount
|
||||
* @param raw Raw amount in smallest units
|
||||
* @param decimals Token decimals (e.g., 6 for USDT-like tokens, 18 for native)
|
||||
*/
|
||||
fun rawToToken(raw: BigInteger, decimals: Int): String {
|
||||
val rawDecimal = BigDecimal(raw)
|
||||
val divisor = BigDecimal.TEN.pow(decimals)
|
||||
val displayDecimal = rawDecimal.divide(divisor, decimals, java.math.RoundingMode.DOWN)
|
||||
return displayDecimal.toPlainString()
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert Green Points amount to raw units (6 decimals)
|
||||
* @deprecated Use tokenToRaw(amount, 6) instead
|
||||
*/
|
||||
fun greenPointsToRaw(amount: String): BigInteger {
|
||||
return tokenToRaw(amount, GreenPointsToken.DECIMALS)
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert raw units to Green Points display amount
|
||||
* @deprecated Use rawToToken(raw, 6) instead
|
||||
*/
|
||||
fun rawToGreenPoints(raw: BigInteger): String {
|
||||
val rawDecimal = BigDecimal(raw)
|
||||
val displayDecimal = rawDecimal.divide(BigDecimal("1000000"), 6, java.math.RoundingMode.DOWN)
|
||||
return displayDecimal.toPlainString()
|
||||
return rawToToken(raw, GreenPointsToken.DECIMALS)
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -11,7 +11,12 @@ import {
|
|||
getCurrentRpcUrl,
|
||||
getGasPrice,
|
||||
fetchGreenPointsBalance,
|
||||
fetchEnergyPointsBalance,
|
||||
fetchFuturePointsBalance,
|
||||
GREEN_POINTS_TOKEN,
|
||||
ENERGY_POINTS_TOKEN,
|
||||
FUTURE_POINTS_TOKEN,
|
||||
TOKEN_CONFIG,
|
||||
type PreparedTransaction,
|
||||
type TokenType,
|
||||
} from '../utils/transaction';
|
||||
|
|
@ -32,6 +37,8 @@ interface ShareWithAddress extends ShareItem {
|
|||
evmAddress?: string;
|
||||
kavaBalance?: string;
|
||||
greenPointsBalance?: string;
|
||||
energyPointsBalance?: string;
|
||||
futurePointsBalance?: string;
|
||||
balanceLoading?: boolean;
|
||||
}
|
||||
|
||||
|
|
@ -89,15 +96,30 @@ export default function Home() {
|
|||
const [isCalculatingMax, setIsCalculatingMax] = useState(false);
|
||||
const [copySuccess, setCopySuccess] = useState(false);
|
||||
|
||||
// 获取当前选择代币的余额
|
||||
const getTokenBalance = (share: ShareWithAddress | null, tokenType: TokenType): string => {
|
||||
if (!share) return '0';
|
||||
switch (tokenType) {
|
||||
case 'KAVA':
|
||||
return share.kavaBalance || '0';
|
||||
case 'GREEN_POINTS':
|
||||
return share.greenPointsBalance || '0';
|
||||
case 'ENERGY_POINTS':
|
||||
return share.energyPointsBalance || '0';
|
||||
case 'FUTURE_POINTS':
|
||||
return share.futurePointsBalance || '0';
|
||||
}
|
||||
};
|
||||
|
||||
// 计算扣除 Gas 费后的最大可转账金额
|
||||
const calculateMaxAmount = async () => {
|
||||
if (!transferShare?.evmAddress) return;
|
||||
|
||||
setIsCalculatingMax(true);
|
||||
try {
|
||||
if (transferTokenType === 'GREEN_POINTS') {
|
||||
// For token transfers, use the full token balance (gas is paid in KAVA)
|
||||
const balance = transferShare.greenPointsBalance || '0';
|
||||
if (TOKEN_CONFIG.isERC20(transferTokenType)) {
|
||||
// For ERC-20 token transfers, use the full token balance (gas is paid in KAVA)
|
||||
const balance = getTokenBalance(transferShare, transferTokenType);
|
||||
setTransferAmount(balance);
|
||||
setTransferError(null);
|
||||
} else {
|
||||
|
|
@ -131,8 +153,8 @@ export default function Home() {
|
|||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to calculate max amount:', error);
|
||||
if (transferTokenType === 'GREEN_POINTS') {
|
||||
setTransferAmount(transferShare.greenPointsBalance || '0');
|
||||
if (TOKEN_CONFIG.isERC20(transferTokenType)) {
|
||||
setTransferAmount(getTokenBalance(transferShare, transferTokenType));
|
||||
} else {
|
||||
// 如果获取 Gas 失败,使用默认估算 (1 gwei * 21000)
|
||||
const defaultGasFee = 0.000021; // ~21000 * 1 gwei
|
||||
|
|
@ -165,12 +187,14 @@ export default function Home() {
|
|||
const updatedShares = await Promise.all(
|
||||
sharesWithAddrs.map(async (share) => {
|
||||
if (share.evmAddress) {
|
||||
// Fetch both balances in parallel
|
||||
const [kavaBalance, greenPointsBalance] = await Promise.all([
|
||||
// Fetch all balances in parallel
|
||||
const [kavaBalance, greenPointsBalance, energyPointsBalance, futurePointsBalance] = await Promise.all([
|
||||
fetchKavaBalance(share.evmAddress),
|
||||
fetchGreenPointsBalance(share.evmAddress),
|
||||
fetchEnergyPointsBalance(share.evmAddress),
|
||||
fetchFuturePointsBalance(share.evmAddress),
|
||||
]);
|
||||
return { ...share, kavaBalance, greenPointsBalance, balanceLoading: false };
|
||||
return { ...share, kavaBalance, greenPointsBalance, energyPointsBalance, futurePointsBalance, balanceLoading: false };
|
||||
}
|
||||
return { ...share, balanceLoading: false };
|
||||
})
|
||||
|
|
@ -315,11 +339,7 @@ export default function Home() {
|
|||
return '转账金额无效';
|
||||
}
|
||||
const amount = parseFloat(transferAmount);
|
||||
const balance = parseFloat(
|
||||
transferTokenType === 'GREEN_POINTS'
|
||||
? (transferShare?.greenPointsBalance || '0')
|
||||
: (transferShare?.kavaBalance || '0')
|
||||
);
|
||||
const balance = parseFloat(getTokenBalance(transferShare, transferTokenType));
|
||||
if (amount > balance) {
|
||||
return '余额不足';
|
||||
}
|
||||
|
|
@ -486,7 +506,7 @@ export default function Home() {
|
|||
</div>
|
||||
)}
|
||||
|
||||
{/* 余额显示 - KAVA 和 绿积分 */}
|
||||
{/* 余额显示 - 所有代币 */}
|
||||
{share.evmAddress && (
|
||||
<div className={styles.balanceSection}>
|
||||
<div className={styles.balanceRow}>
|
||||
|
|
@ -509,6 +529,26 @@ export default function Home() {
|
|||
)}
|
||||
</span>
|
||||
</div>
|
||||
<div className={styles.balanceRow}>
|
||||
<span className={styles.balanceLabel} style={{ color: '#2196F3' }}>{ENERGY_POINTS_TOKEN.name}</span>
|
||||
<span className={styles.balanceValue} style={{ color: '#2196F3' }}>
|
||||
{share.balanceLoading ? (
|
||||
<span className={styles.balanceLoading}>加载中...</span>
|
||||
) : (
|
||||
<>{share.energyPointsBalance || '0'}</>
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
<div className={styles.balanceRow}>
|
||||
<span className={styles.balanceLabel} style={{ color: '#9C27B0' }}>{FUTURE_POINTS_TOKEN.name}</span>
|
||||
<span className={styles.balanceValue} style={{ color: '#9C27B0' }}>
|
||||
{share.balanceLoading ? (
|
||||
<span className={styles.balanceLoading}>加载中...</span>
|
||||
) : (
|
||||
<>{share.futurePointsBalance || '0'}</>
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
|
@ -578,7 +618,10 @@ export default function Home() {
|
|||
<div className={styles.transferWalletInfo}>
|
||||
<div className={styles.transferWalletName}>{transferShare.walletName}</div>
|
||||
<div className={styles.transferWalletBalance}>
|
||||
KAVA: {transferShare.kavaBalance || '0'} | {GREEN_POINTS_TOKEN.name}: {transferShare.greenPointsBalance || '0'}
|
||||
KAVA: {transferShare.kavaBalance || '0'} | <span style={{color: '#4CAF50'}}>{GREEN_POINTS_TOKEN.name}: {transferShare.greenPointsBalance || '0'}</span>
|
||||
</div>
|
||||
<div className={styles.transferWalletBalance}>
|
||||
<span style={{color: '#2196F3'}}>{ENERGY_POINTS_TOKEN.name}: {transferShare.energyPointsBalance || '0'}</span> | <span style={{color: '#9C27B0'}}>{FUTURE_POINTS_TOKEN.name}: {transferShare.futurePointsBalance || '0'}</span>
|
||||
</div>
|
||||
<div className={styles.transferNetwork}>
|
||||
网络: Kava {getCurrentNetwork() === 'mainnet' ? '主网' : '测试网'}
|
||||
|
|
@ -605,6 +648,22 @@ export default function Home() {
|
|||
{GREEN_POINTS_TOKEN.name}
|
||||
</button>
|
||||
</div>
|
||||
<div className={styles.tokenTypeSelector} style={{ marginTop: '8px' }}>
|
||||
<button
|
||||
className={`${styles.tokenTypeButton} ${transferTokenType === 'ENERGY_POINTS' ? styles.tokenTypeActive : ''}`}
|
||||
onClick={() => { setTransferTokenType('ENERGY_POINTS'); setTransferAmount(''); }}
|
||||
style={transferTokenType === 'ENERGY_POINTS' ? { backgroundColor: '#2196F3', borderColor: '#2196F3' } : {}}
|
||||
>
|
||||
{ENERGY_POINTS_TOKEN.name}
|
||||
</button>
|
||||
<button
|
||||
className={`${styles.tokenTypeButton} ${transferTokenType === 'FUTURE_POINTS' ? styles.tokenTypeActive : ''}`}
|
||||
onClick={() => { setTransferTokenType('FUTURE_POINTS'); setTransferAmount(''); }}
|
||||
style={transferTokenType === 'FUTURE_POINTS' ? { backgroundColor: '#9C27B0', borderColor: '#9C27B0' } : {}}
|
||||
>
|
||||
{FUTURE_POINTS_TOKEN.name}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 收款地址 */}
|
||||
|
|
@ -622,7 +681,7 @@ export default function Home() {
|
|||
{/* 转账金额 */}
|
||||
<div className={styles.transferInputGroup}>
|
||||
<label className={styles.transferLabel}>
|
||||
转账金额 ({transferTokenType === 'GREEN_POINTS' ? GREEN_POINTS_TOKEN.name : 'KAVA'})
|
||||
转账金额 ({TOKEN_CONFIG.getName(transferTokenType)})
|
||||
</label>
|
||||
<div className={styles.transferAmountWrapper}>
|
||||
<input
|
||||
|
|
@ -689,8 +748,8 @@ export default function Home() {
|
|||
<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 className={styles.confirmValue} style={TOKEN_CONFIG.isERC20(transferTokenType) ? { color: transferTokenType === 'GREEN_POINTS' ? '#4CAF50' : transferTokenType === 'ENERGY_POINTS' ? '#2196F3' : '#9C27B0' } : {}}>
|
||||
{TOKEN_CONFIG.getName(transferTokenType)}
|
||||
</span>
|
||||
</div>
|
||||
<div className={styles.confirmRow}>
|
||||
|
|
@ -699,8 +758,8 @@ export default function Home() {
|
|||
</div>
|
||||
<div className={styles.confirmRow}>
|
||||
<span className={styles.confirmLabel}>转账金额</span>
|
||||
<span className={styles.confirmValue} style={transferTokenType === 'GREEN_POINTS' ? { color: '#4CAF50' } : {}}>
|
||||
{transferAmount} {transferTokenType === 'GREEN_POINTS' ? GREEN_POINTS_TOKEN.name : 'KAVA'}
|
||||
<span className={styles.confirmValue} style={TOKEN_CONFIG.isERC20(transferTokenType) ? { color: transferTokenType === 'GREEN_POINTS' ? '#4CAF50' : transferTokenType === 'ENERGY_POINTS' ? '#2196F3' : '#9C27B0' } : {}}>
|
||||
{transferAmount} {TOKEN_CONFIG.getName(transferTokenType)}
|
||||
</span>
|
||||
</div>
|
||||
<div className={styles.confirmRow}>
|
||||
|
|
|
|||
|
|
@ -17,17 +17,97 @@ export const KAVA_RPC_URL = {
|
|||
};
|
||||
|
||||
// Token types
|
||||
export type TokenType = 'KAVA' | 'GREEN_POINTS';
|
||||
export type TokenType = 'KAVA' | 'GREEN_POINTS' | 'ENERGY_POINTS' | 'FUTURE_POINTS';
|
||||
|
||||
// Green Points (绿积分) Token Configuration
|
||||
// ERC-20 通用函数选择器
|
||||
export const ERC20_SELECTORS = {
|
||||
balanceOf: '0x70a08231', // balanceOf(address)
|
||||
transfer: '0xa9059cbb', // transfer(address,uint256)
|
||||
approve: '0x095ea7b3', // approve(address,uint256)
|
||||
allowance: '0xdd62ed3e', // allowance(address,address)
|
||||
totalSupply: '0x18160ddd', // totalSupply()
|
||||
};
|
||||
|
||||
// Green Points (绿积分) Token Configuration - dUSDT
|
||||
export const GREEN_POINTS_TOKEN = {
|
||||
contractAddress: '0xA9F3A35dBa8699c8C681D8db03F0c1A8CEB9D7c3',
|
||||
name: '绿积分',
|
||||
symbol: 'dUSDT',
|
||||
decimals: 6,
|
||||
// ERC-20 function selectors
|
||||
balanceOfSelector: '0x70a08231',
|
||||
transferSelector: '0xa9059cbb',
|
||||
// ERC-20 function selectors (kept for backward compatibility)
|
||||
balanceOfSelector: ERC20_SELECTORS.balanceOf,
|
||||
transferSelector: ERC20_SELECTORS.transfer,
|
||||
};
|
||||
|
||||
// Energy Points (积分股) Token Configuration - eUSDT
|
||||
export const ENERGY_POINTS_TOKEN = {
|
||||
contractAddress: '0x7C3275D808eFbAE90C06C7E3A9AfDdcAa8563931',
|
||||
name: '积分股',
|
||||
symbol: 'eUSDT',
|
||||
decimals: 6,
|
||||
};
|
||||
|
||||
// Future Points (积分值) Token Configuration - fUSDT
|
||||
export const FUTURE_POINTS_TOKEN = {
|
||||
contractAddress: '0x14dc4f7d3E4197438d058C3D156dd9826A161134',
|
||||
name: '积分值',
|
||||
symbol: 'fUSDT',
|
||||
decimals: 6,
|
||||
};
|
||||
|
||||
// Token configuration utility
|
||||
export const TOKEN_CONFIG = {
|
||||
getContractAddress: (tokenType: TokenType): string | null => {
|
||||
switch (tokenType) {
|
||||
case 'KAVA':
|
||||
return null; // Native token has no contract
|
||||
case 'GREEN_POINTS':
|
||||
return GREEN_POINTS_TOKEN.contractAddress;
|
||||
case 'ENERGY_POINTS':
|
||||
return ENERGY_POINTS_TOKEN.contractAddress;
|
||||
case 'FUTURE_POINTS':
|
||||
return FUTURE_POINTS_TOKEN.contractAddress;
|
||||
}
|
||||
},
|
||||
getDecimals: (tokenType: TokenType): number => {
|
||||
switch (tokenType) {
|
||||
case 'KAVA':
|
||||
return 18;
|
||||
case 'GREEN_POINTS':
|
||||
return GREEN_POINTS_TOKEN.decimals;
|
||||
case 'ENERGY_POINTS':
|
||||
return ENERGY_POINTS_TOKEN.decimals;
|
||||
case 'FUTURE_POINTS':
|
||||
return FUTURE_POINTS_TOKEN.decimals;
|
||||
}
|
||||
},
|
||||
getName: (tokenType: TokenType): string => {
|
||||
switch (tokenType) {
|
||||
case 'KAVA':
|
||||
return 'KAVA';
|
||||
case 'GREEN_POINTS':
|
||||
return GREEN_POINTS_TOKEN.name;
|
||||
case 'ENERGY_POINTS':
|
||||
return ENERGY_POINTS_TOKEN.name;
|
||||
case 'FUTURE_POINTS':
|
||||
return FUTURE_POINTS_TOKEN.name;
|
||||
}
|
||||
},
|
||||
getSymbol: (tokenType: TokenType): string => {
|
||||
switch (tokenType) {
|
||||
case 'KAVA':
|
||||
return 'KAVA';
|
||||
case 'GREEN_POINTS':
|
||||
return GREEN_POINTS_TOKEN.symbol;
|
||||
case 'ENERGY_POINTS':
|
||||
return ENERGY_POINTS_TOKEN.symbol;
|
||||
case 'FUTURE_POINTS':
|
||||
return FUTURE_POINTS_TOKEN.symbol;
|
||||
}
|
||||
},
|
||||
isERC20: (tokenType: TokenType): boolean => {
|
||||
return tokenType !== 'KAVA';
|
||||
},
|
||||
};
|
||||
|
||||
// 当前网络配置 (从 localStorage 读取或使用默认值)
|
||||
|
|
@ -327,44 +407,69 @@ export function weiToKava(wei: bigint): string {
|
|||
}
|
||||
|
||||
/**
|
||||
* 将绿积分金额转换为最小单位 (6 decimals)
|
||||
* 将代币金额转换为最小单位
|
||||
* @param amount Human-readable amount
|
||||
* @param decimals Token decimals (default 6 for USDT-like tokens)
|
||||
*/
|
||||
export function greenPointsToRaw(amount: string): bigint {
|
||||
export function tokenToRaw(amount: string, decimals: number = 6): 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);
|
||||
// 补齐或截断到指定位数
|
||||
if (fraction.length > decimals) {
|
||||
fraction = fraction.substring(0, decimals);
|
||||
} else {
|
||||
fraction = fraction.padEnd(6, '0');
|
||||
fraction = fraction.padEnd(decimals, '0');
|
||||
}
|
||||
|
||||
return whole * BigInt(10 ** 6) + BigInt(fraction);
|
||||
return whole * BigInt(10 ** decimals) + BigInt(fraction);
|
||||
}
|
||||
|
||||
/**
|
||||
* 将最小单位转换为绿积分金额
|
||||
* 将最小单位转换为代币金额
|
||||
* @param raw Raw amount in smallest units
|
||||
* @param decimals Token decimals (default 6 for USDT-like tokens)
|
||||
*/
|
||||
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+$/, '');
|
||||
export function rawToToken(raw: bigint, decimals: number = 6): string {
|
||||
const rawStr = raw.toString().padStart(decimals + 1, '0');
|
||||
const whole = rawStr.slice(0, -decimals) || '0';
|
||||
const fraction = rawStr.slice(-decimals).replace(/0+$/, '');
|
||||
return fraction ? `${whole}.${fraction}` : whole;
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询绿积分 (ERC-20) 余额
|
||||
* 将绿积分金额转换为最小单位 (6 decimals)
|
||||
* @deprecated Use tokenToRaw(amount, 6) instead
|
||||
*/
|
||||
export async function fetchGreenPointsBalance(address: string): Promise<string> {
|
||||
export function greenPointsToRaw(amount: string): bigint {
|
||||
return tokenToRaw(amount, GREEN_POINTS_TOKEN.decimals);
|
||||
}
|
||||
|
||||
/**
|
||||
* 将最小单位转换为绿积分金额
|
||||
* @deprecated Use rawToToken(raw, 6) instead
|
||||
*/
|
||||
export function rawToGreenPoints(raw: bigint): string {
|
||||
return rawToToken(raw, GREEN_POINTS_TOKEN.decimals);
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询 ERC-20 代币余额
|
||||
* @param address Wallet address
|
||||
* @param contractAddress Token contract address
|
||||
* @param decimals Token decimals
|
||||
*/
|
||||
export async function fetchERC20Balance(
|
||||
address: string,
|
||||
contractAddress: string,
|
||||
decimals: number = 6
|
||||
): 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 callData = ERC20_SELECTORS.balanceOf + paddedAddress;
|
||||
|
||||
const response = await fetch(rpcUrl, {
|
||||
method: 'POST',
|
||||
|
|
@ -374,7 +479,7 @@ export async function fetchGreenPointsBalance(address: string): Promise<string>
|
|||
method: 'eth_call',
|
||||
params: [
|
||||
{
|
||||
to: GREEN_POINTS_TOKEN.contractAddress,
|
||||
to: contractAddress,
|
||||
data: callData,
|
||||
},
|
||||
'latest',
|
||||
|
|
@ -386,21 +491,65 @@ export async function fetchGreenPointsBalance(address: string): Promise<string>
|
|||
const data = await response.json();
|
||||
if (data.result && data.result !== '0x') {
|
||||
const balanceRaw = BigInt(data.result);
|
||||
return rawToGreenPoints(balanceRaw);
|
||||
return rawToToken(balanceRaw, decimals);
|
||||
}
|
||||
return '0';
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch Green Points balance:', error);
|
||||
console.error('Failed to fetch ERC20 balance:', error);
|
||||
return '0';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询绿积分 (ERC-20) 余额
|
||||
*/
|
||||
export async function fetchGreenPointsBalance(address: string): Promise<string> {
|
||||
return fetchERC20Balance(address, GREEN_POINTS_TOKEN.contractAddress, GREEN_POINTS_TOKEN.decimals);
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询积分股 (eUSDT) 余额
|
||||
*/
|
||||
export async function fetchEnergyPointsBalance(address: string): Promise<string> {
|
||||
return fetchERC20Balance(address, ENERGY_POINTS_TOKEN.contractAddress, ENERGY_POINTS_TOKEN.decimals);
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询积分值 (fUSDT) 余额
|
||||
*/
|
||||
export async function fetchFuturePointsBalance(address: string): Promise<string> {
|
||||
return fetchERC20Balance(address, FUTURE_POINTS_TOKEN.contractAddress, FUTURE_POINTS_TOKEN.decimals);
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询所有代币余额
|
||||
*/
|
||||
export async function fetchAllTokenBalances(address: string): Promise<{
|
||||
kava: string;
|
||||
greenPoints: string;
|
||||
energyPoints: string;
|
||||
futurePoints: string;
|
||||
}> {
|
||||
const [greenPoints, energyPoints, futurePoints] = await Promise.all([
|
||||
fetchGreenPointsBalance(address),
|
||||
fetchEnergyPointsBalance(address),
|
||||
fetchFuturePointsBalance(address),
|
||||
]);
|
||||
// Note: KAVA balance is fetched separately via eth_getBalance
|
||||
return {
|
||||
kava: '0', // Caller should fetch KAVA balance separately
|
||||
greenPoints,
|
||||
energyPoints,
|
||||
futurePoints,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 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;
|
||||
const selector = ERC20_SELECTORS.transfer;
|
||||
// Encode recipient address (padded to 32 bytes)
|
||||
const paddedAddress = to.toLowerCase().replace('0x', '').padStart(64, '0');
|
||||
// Encode amount (padded to 32 bytes)
|
||||
|
|
@ -476,13 +625,15 @@ export async function estimateGas(params: { from: string; to: string; value: str
|
|||
// For token transfers, we need different params
|
||||
let txParams: { from: string; to: string; value: string; data?: string };
|
||||
|
||||
if (tokenType === 'GREEN_POINTS') {
|
||||
if (TOKEN_CONFIG.isERC20(tokenType)) {
|
||||
// ERC-20 transfer: to is contract, value is 0, data is transfer call
|
||||
const tokenAmount = greenPointsToRaw(params.value);
|
||||
const contractAddress = TOKEN_CONFIG.getContractAddress(tokenType);
|
||||
const decimals = TOKEN_CONFIG.getDecimals(tokenType);
|
||||
const tokenAmount = tokenToRaw(params.value, decimals);
|
||||
const transferData = encodeErc20Transfer(params.to, tokenAmount);
|
||||
txParams = {
|
||||
from: params.from,
|
||||
to: GREEN_POINTS_TOKEN.contractAddress,
|
||||
to: contractAddress!,
|
||||
value: '0x0',
|
||||
data: transferData,
|
||||
};
|
||||
|
|
@ -511,7 +662,7 @@ export async function estimateGas(params: { from: string; to: string; value: str
|
|||
if (data.error) {
|
||||
// 如果估算失败,使用默认值
|
||||
console.warn('Gas 估算失败,使用默认值:', data.error);
|
||||
return tokenType === 'GREEN_POINTS' ? BigInt(65000) : BigInt(21000);
|
||||
return TOKEN_CONFIG.isERC20(tokenType) ? BigInt(65000) : BigInt(21000);
|
||||
}
|
||||
return BigInt(data.result);
|
||||
}
|
||||
|
|
@ -543,12 +694,14 @@ export async function prepareTransaction(params: TransactionParams): Promise<Pre
|
|||
let value: bigint;
|
||||
let data: string;
|
||||
|
||||
if (tokenType === 'GREEN_POINTS') {
|
||||
if (TOKEN_CONFIG.isERC20(tokenType)) {
|
||||
// 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();
|
||||
const contractAddress = TOKEN_CONFIG.getContractAddress(tokenType);
|
||||
const decimals = TOKEN_CONFIG.getDecimals(tokenType);
|
||||
const tokenAmount = tokenToRaw(params.value, decimals);
|
||||
toAddress = contractAddress!.toLowerCase();
|
||||
value = BigInt(0);
|
||||
data = encodeErc20Transfer(params.to, tokenAmount);
|
||||
} else {
|
||||
|
|
|
|||
Loading…
Reference in New Issue