web3钱包Demo

为了加深自己理解特写一篇基于个人现有理解编写的3.0钱包小Demo,并且每一行关键代码都加注释,记录一下。 giuhub地址 github.com/jingkais/we...

项目目录

scss 复制代码
com.learn.web3walletdemo/
├── fragment/
│   ├── BackupPrivateKeyFragment          (备份私钥)
│   ├── CreateWalletFragment              (创建钱包页面)
│   ├── DAppFragment                      (Dapp浏览器界面) 待开发
│   ├── ImportMnemonicFragment            (导入助记词)
│   ├── ImportPrivateKeyFragment          (导入私钥)
│   ├── SettingsFragment                 (设置)待开发
│   ├── TransactionFragment              (交易界面) 待开发
│   └── WalletFragment                   (钱包主界面)
├── model/
│   ├── GasInfo
│   ├── NetworkInfo
│   ├── Transaction.kt
│   └── Wallet
├── utils/
│   ├── AndroidCryptoHelper   (Android加密辅助工具 - 提供在不同Android版本上安全生成密钥对的方法)
│   ├── SecurityManager  (安全管理器 - 处理敏感数据的加密存储和检索、使用Android Keystore系统保护加密密钥)
│   └── Web3Service.kt
└── MainActivity 主界面   (创建钱包)

总体介绍

以下是该Demo目前实现的功能:

  • 钱包主界面
  • 导入私钥界面
  • 导入助记词界面
  • 创建钱包界面
  • 备份私钥界面

待开发功能:

  • 交易界面
  • 设置
  • DApp浏览器界面
  • ...
kotlin 复制代码
class MainActivity : AppCompatActivity() {

    private lateinit var binding: ActivityMainBinding
    private lateinit var securityManager: SecurityManager

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        binding = ActivityMainBinding.inflate(layoutInflater)
        setContentView(binding.root)

        securityManager = SecurityManager(this)

        setupBottomNavigation()
        checkWalletExists()
    }

    private fun setupBottomNavigation() {
        binding.bottomNavigation.setOnNavigationItemSelectedListener { item ->
            when (item.itemId) {
                R.id.nav_wallet -> {
                    replaceFragment(WalletFragment())
                    true
                }
                R.id.nav_transaction -> {
                    replaceFragment(TransactionFragment())
                    true
                }
                R.id.nav_dapp -> {
                    replaceFragment(DAppFragment())
                    true
                }
                R.id.nav_settings -> {
                    replaceFragment(SettingsFragment())
                    true
                }
                else -> false
            }
        }

        if (securityManager.hasWallets()) {
            replaceFragment(WalletFragment())
        }
    }

    private fun checkWalletExists() {
        if (!securityManager.hasWallets()) {
            replaceFragment(CreateWalletFragment())
            binding.bottomNavigation.visibility = View.GONE
        } else {
            binding.bottomNavigation.visibility = View.VISIBLE
        }
    }

    private fun replaceFragment(fragment: androidx.fragment.app.Fragment) {
        supportFragmentManager.beginTransaction()
            .replace(R.id.fragment_container, fragment)
            .commit()
    }

    fun showBottomNavigation() {
        binding.bottomNavigation.visibility = View.VISIBLE
    }

    fun hideBottomNavigation() {
        binding.bottomNavigation.visibility = View.GONE
    }
}
js 复制代码
class Web3Service {

    companion object {
        // 多个可靠的公共 RPC 节点(自动故障转移)
        private val SEPOLIA_NODES = listOf(
            "https://ethereum-sepolia-rpc.publicnode.com",  // 公共节点,无需API密钥
            "https://rpc.sepolia.org",                      // Sepolia官方RPC
            "https://sepolia.drpc.org",                     // 备用RPC节点
            "https://rpc2.sepolia.org"                      // 另一个备用节点
        )

        private val MAINNET_NODES = listOf(
            "https://eth.llamarpc.com",                     // 主网公共节点
            "https://rpc.ankr.com/eth",                     // Ankr提供的节点
            "https://ethereum.publicnode.com"               // 以太坊公共节点
        )

        // 区块链网络ID定义
        const val SEPOLIA_CHAIN_ID = 11155111L              // Sepolia测试网链ID
        const val MAINNET_CHAIN_ID = 1L                     // 以太坊主网链ID
    }

    // 当前使用的RPC节点URL
    private var currentRpcUrl = SEPOLIA_NODES[0]
    // Web3j实例,用于与以太坊节点通信
    private var web3j: Web3j = Web3j.build(HttpService(currentRpcUrl))
    // 当前连接的区块链网络ID
    private var currentChainId: Long = SEPOLIA_CHAIN_ID
    // 标识当前是否连接主网
    private var isMainnet: Boolean = false

    /**
     * 创建新钱包 - 生成新的以太坊钱包地址和私钥
     * @return WalletInfo 包含地址、私钥等钱包信息
     */
    fun createNewWallet(): WalletInfo {
        return try {
            // 使用安全随机数生成器生成私钥
            val secureRandom = SecureRandom()
            val privateKeyBytes = ByteArray(32) // 256位私钥
            secureRandom.nextBytes(privateKeyBytes)

            // 将字节数组转换为十六进制字符串(去掉0x前缀)
            val privateKey = Numeric.toHexStringNoPrefix(privateKeyBytes)
            // 从私钥创建凭证对象,可以获取地址等信息
            val credentials = Credentials.create(privateKey)

            // 返回钱包信息对象
            WalletInfo(
                address = credentials.address,      // 以太坊地址
                mnemonic = null,                    // 新创建的钱包没有助记词
                privateKey = privateKey,            // 原始私钥
                walletFile = null                   // 不生成钱包文件
            )
        } catch (e: Exception) {
            // 如果标准方法失败,使用备用方法生成私钥
            val fallbackPrivateKey = generateFallbackPrivateKey()
            val credentials = Credentials.create(fallbackPrivateKey)

            WalletInfo(
                address = credentials.address,
                mnemonic = null,
                privateKey = fallbackPrivateKey,
                walletFile = null
            )
        }
    }

    /**
     * 通过助记词创建/导入钱包
     * @param mnemonic 助记词字符串
     * @param password 用于加密的钱包密码
     * @return WalletInfo 钱包信息
     */
    fun createWalletFromMnemonic(mnemonic: String, password: String): WalletInfo {
        return try {
            // 验证助记词格式
            val cleanedMnemonic = mnemonic.trim()
            if (cleanedMnemonic.isEmpty()) {
                throw Web3Exception("助记词不能为空")
            }

            // 分割助记词并验证长度
            val words = cleanedMnemonic.split("\s+".toRegex())
            if (words.size !in listOf(12, 15, 18, 21, 24)) {
                throw Web3Exception("助记词应为12、15、18、21或24个单词")
            }

            // 使用 Web3j 从助记词生成种子(基于BIP39标准)
            val seed = MnemonicUtils.generateSeed(cleanedMnemonic, password)

            // 从种子生成确定性钱包(简化版本)
            // 注意:这是一个简化实现,实际应用中应该使用完整的 BIP44 路径
            val privateKey = generatePrivateKeyFromSeed(seed)
            val credentials = Credentials.create(privateKey)

            WalletInfo(
                address = credentials.address,
                mnemonic = cleanedMnemonic,     // 保存原始助记词
                privateKey = privateKey,        // 派生出的私钥
                walletFile = null
            )
        } catch (e: Exception) {
            throw Web3Exception("通过助记词创建钱包失败: ${e.message ?: "未知错误"}")
        }
    }

    /**
     * 从种子生成私钥(简化实现)
     * @param seed BIP39种子字节数组
     * @return 十六进制私钥字符串
     */
    private fun generatePrivateKeyFromSeed(seed: ByteArray): String {
        // 这是一个简化实现
        // 实际应用中应该使用完整的 HD 钱包派生路径(BIP44)
        val secureRandom = SecureRandom(seed)
        val privateKeyBytes = ByteArray(32)
        secureRandom.nextBytes(privateKeyBytes)
        return Numeric.toHexStringNoPrefix(privateKeyBytes)
    }

    /**
     * 导入私钥钱包
     * @param privateKey 原始私钥字符串
     * @return WalletInfo 钱包信息
     */
    fun importWalletFromPrivateKey(privateKey: String): WalletInfo {
        return try {
            var cleanKey = privateKey.trim()

            // 移除可能的0x前缀
            if (cleanKey.startsWith("0x")) {
                cleanKey = cleanKey.substring(2)
            }

            // 验证私钥长度(64个十六进制字符 = 32字节)
            if (cleanKey.length != 64) {
                throw Web3Exception("私钥必须是64个十六进制字符")
            }

            // 验证私钥只包含有效的十六进制字符
            if (!cleanKey.matches(Regex("[0-9a-fA-F]+"))) {
                throw Web3Exception("私钥包含无效字符")
            }

            // 从私钥创建凭证对象
            val credentials = Credentials.create(cleanKey)

            WalletInfo(
                address = credentials.address,
                mnemonic = null,
                privateKey = cleanKey,      // 使用清理后的私钥
                walletFile = null
            )
        } catch (e: Exception) {
            throw Web3Exception("导入钱包失败: ${e.message ?: "未知错误"}")
        }
    }

    /**
     * 备用私钥生成方法 - 当标准方法失败时使用
     * @return 随机生成的私钥字符串
     */
    private fun generateFallbackPrivateKey(): String {
        val random = java.util.Random()
        val sb = StringBuilder()
        val hexChars = "0123456789abcdef"
        // 生成64个十六进制字符(32字节)
        repeat(64) {
            sb.append(hexChars[random.nextInt(hexChars.length)])
        }
        return sb.toString()
    }

    /**
     * 查询余额 - 获取指定地址的ETH余额(单位:Wei)
     * @param address 要查询的以太坊地址
     * @return BigInteger 余额(Wei单位)
     */
    suspend fun getBalance(address: String): BigInteger {
        // 首先验证地址格式
        if (!isValidEthereumAddress(address)) {
            return BigInteger.ZERO
        }

        // 根据当前网络选择节点列表
        val nodes = if (isMainnet) MAINNET_NODES else SEPOLIA_NODES
        var lastException: Exception? = null

        // 尝试所有可用节点,实现故障转移
        for (node in nodes) {
            try {
                // 为每个节点创建新的Web3j实例
                val tempWeb3j = Web3j.build(HttpService(node))
                // 查询最新区块的余额
                val ethGetBalance: EthGetBalance = tempWeb3j.ethGetBalance(address, DefaultBlockParameterName.LATEST).send()

                // 如果成功,更新当前使用的节点
                currentRpcUrl = node
                web3j = tempWeb3j

                // 返回余额(单位:Wei)
                return ethGetBalance.balance
            } catch (e: Exception) {
                lastException = e
                // 继续尝试下一个节点
            }
        }

        // 所有节点都失败,记录错误并返回0
        println("所有RPC节点都失败,最后错误: ${lastException?.message}")
        return BigInteger.ZERO
    }

    /**
     * 获取格式化的余额 - 将Wei转换为ETH并格式化显示
     * @param address 以太坊地址
     * @return String 格式化后的余额字符串(如 "1.500000 ETH")
     */
    suspend fun getFormattedBalance(address: String): String {
        return try {
            val balanceWei = getBalance(address)
            if (balanceWei == BigInteger.ZERO) {
                "0 ETH"
            } else {
                // 将Wei转换为ETH
                val balanceEth = Convert.fromWei(BigDecimal(balanceWei), Convert.Unit.ETHER)
                // 保留6位小数并格式化显示
                "${balanceEth.setScale(6, BigDecimal.ROUND_HALF_UP)} ETH"
            }
        } catch (e: Exception) {
            "0 ETH" // 发生错误时返回0
        }
    }

    /**
     * 获取美元估值 - 计算余额的美元价值(使用固定汇率)
     * @param address 以太坊地址
     * @return String 美元价值字符串(如 "$2700.00")
     */
    suspend fun getBalanceInUSD(address: String): String {
        return try {
            val balanceWei = getBalance(address)
            if (balanceWei == BigInteger.ZERO) {
                "$0.00"
            } else {
                // 将Wei转换为ETH
                val balanceEth = Convert.fromWei(BigDecimal(balanceWei), Convert.Unit.ETHER)
                // 使用固定汇率计算美元价值(实际应用中应该使用实时汇率)
                val usdValue = balanceEth.toDouble() * 1800.0
                // 格式化美元金额
                "$${String.format("%.2f", usdValue)}"
            }
        } catch (e: Exception) {
            "$0.00" // 发生错误时返回$0.00
        }
    }

    /**
     * 验证以太坊地址格式
     * @param address 要验证的地址字符串
     * @return Boolean 是否为有效的以太坊地址格式
     */
    fun isValidEthereumAddress(address: String): Boolean {
        // 正则表达式验证:以0x开头,后跟40个十六进制字符
        return address.matches(Regex("^0x[a-fA-F0-9]{40}$"))
    }

    /**
     * 切换网络 - 在主网和测试网之间切换
     * @param useMainnet 是否切换到主网
     */
    fun switchNetwork(useMainnet: Boolean) {
        isMainnet = useMainnet
        currentChainId = if (useMainnet) MAINNET_CHAIN_ID else SEPOLIA_CHAIN_ID
        currentRpcUrl = if (useMainnet) MAINNET_NODES[0] else SEPOLIA_NODES[0]
        // 重新创建Web3j实例连接到新网络
        web3j = Web3j.build(HttpService(currentRpcUrl))
    }

    /**
     * 获取当前网络信息
     * @return NetworkInfo 包含网络名称、RPC URL、链ID等信息
     */
    fun getCurrentNetworkInfo(): NetworkInfo {
        return if (isMainnet) {
            NetworkInfo(
                name = "Ethereum Mainnet",          // 主网名称
                rpcUrl = currentRpcUrl,             // 当前RPC URL
                chainId = MAINNET_CHAIN_ID,         // 主网链ID
                symbol = "ETH",                     // 代币符号
                explorerUrl = "https://etherscan.io" // 区块浏览器URL
            )
        } else {
            NetworkInfo(
                name = "Sepolia Testnet",           // 测试网名称
                rpcUrl = currentRpcUrl,
                chainId = SEPOLIA_CHAIN_ID,         // 测试网链ID
                symbol = "ETH",
                explorerUrl = "https://sepolia.etherscan.io" // 测试网区块浏览器
            )
        }
    }

    /**
     * 测试网络连接 - 检查当前节点是否可用
     * @return Boolean 连接是否成功
     */
    suspend fun testNetworkConnection(): Boolean {
        return try {
            // 使用零地址进行简单的余额查询来测试连接
            val testAddress = "0x0000000000000000000000000000000000000000"
            web3j.ethGetBalance(testAddress, DefaultBlockParameterName.LATEST).send()
            true // 如果没有异常,说明连接成功
        } catch (e: Exception) {
            false // 发生异常,连接失败
        }
    }
}
js 复制代码
/**
 * 安全管理器 - 处理敏感数据的加密存储和检索
 * 使用Android Keystore系统保护加密密钥
 */
class SecurityManager(private val context: Context) {

    companion object {
        private const val ANDROID_KEYSTORE = "AndroidKeyStore"  // Android密钥库提供者
        private const val KEY_ALIAS = "Web3Wallet_Key"          // 密钥别名
        private const val SHARED_PREFS_NAME = "encrypted_web3_prefs" // 加密共享偏好名称
        private const val TRANSFORMATION = "AES/GCM/NoPadding"  // 加密算法和模式
        private const val IV_LENGTH = 12                        // 初始化向量长度(字节)
    }

    // Android密钥库实例
    private val keyStore: KeyStore = KeyStore.getInstance(ANDROID_KEYSTORE).apply {
        load(null) // 加载密钥库
    }

    // 主密钥,用于加密共享偏好设置
    private val masterKey: MasterKey by lazy {
        MasterKey.Builder(context)
            .setKeyScheme(MasterKey.KeyScheme.AES256_GCM) // 使用AES256-GCM加密方案
            .build()
    }

    // 加密的共享偏好设置实例
    private val encryptedPrefs by lazy {
        EncryptedSharedPreferences.create(
            context,
            SHARED_PREFS_NAME,
            masterKey,
            EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,    // 键加密方案
            EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM   // 值加密方案
        )
    }

    /**
     * 获取或创建加密密钥 - 使用Android Keystore保护密钥
     * @return SecretKey 加密密钥
     */
    @RequiresApi(Build.VERSION_CODES.M)
    private fun getOrCreateSecretKey(): SecretKey {
        // 检查密钥是否已存在
        if (!keyStore.containsAlias(KEY_ALIAS)) {
            // 创建新的密钥生成器
            val keyGenerator = KeyGenerator.getInstance(
                KeyProperties.KEY_ALGORITHM_AES,
                ANDROID_KEYSTORE
            )

            // 配置密钥生成参数
            val keyGenParameterSpec = KeyGenParameterSpec.Builder(
                KEY_ALIAS,
                KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT // 密钥用途
            )
                .setBlockModes(KeyProperties.BLOCK_MODE_GCM)       // 使用GCM分组模式
                .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE) // 无填充
                .setKeySize(256)                                   // 256位密钥
                .build()

            keyGenerator.init(keyGenParameterSpec)
            keyGenerator.generateKey() // 生成密钥
        }

        // 返回已存在的密钥
        return keyStore.getKey(KEY_ALIAS, null) as SecretKey
    }

    /**
     * 加密数据 - 使用AES-GCM加密算法
     * @param plaintext 要加密的明文
     * @return String Base64编码的加密数据
     */
    @RequiresApi(Build.VERSION_CODES.M)
    fun encryptData(plaintext: String): String {
        // 获取密码器实例
        val cipher = Cipher.getInstance(TRANSFORMATION)
        // 初始化为加密模式
        cipher.init(Cipher.ENCRYPT_MODE, getOrCreateSecretKey())

        val iv = cipher.iv // 获取初始化向量
        val encrypted = cipher.doFinal(plaintext.toByteArray(Charsets.UTF_8)) // 执行加密

        // 组合IV和加密数据
        val combined = ByteArray(iv.size + encrypted.size)
        System.arraycopy(iv, 0, combined, 0, iv.size)              // 复制IV到前面
        System.arraycopy(encrypted, 0, combined, iv.size, encrypted.size) // 复制加密数据到后面

        // 返回Base64编码的字符串
        return Base64.encodeToString(combined, Base64.DEFAULT)
    }

    /**
     * 解密数据 - 使用AES-GCM解密算法
     * @param encryptedData Base64编码的加密数据
     * @return String 解密后的明文
     */
    @RequiresApi(Build.VERSION_CODES.M)
    fun decryptData(encryptedData: String): String {
        // 解码Base64字符串
        val combined = Base64.decode(encryptedData, Base64.DEFAULT)

        // 分离IV和加密数据
        val iv = ByteArray(IV_LENGTH)
        val encrypted = ByteArray(combined.size - IV_LENGTH)

        System.arraycopy(combined, 0, iv, 0, IV_LENGTH)            // 提取IV
        System.arraycopy(combined, IV_LENGTH, encrypted, 0, encrypted.size) // 提取加密数据

        // 获取密码器实例
        val cipher = Cipher.getInstance(TRANSFORMATION)
        // 配置GCM参数(128位认证标签)
        val spec = GCMParameterSpec(128, iv)
        // 初始化为解密模式
        cipher.init(Cipher.DECRYPT_MODE, getOrCreateSecretKey(), spec)

        // 执行解密
        val decrypted = cipher.doFinal(encrypted)
        return String(decrypted, Charsets.UTF_8)
    }

    /**
     * 安全存储字符串 - 使用加密的共享偏好设置
     * @param key 键
     * @param value 值
     */
    fun securePutString(key: String, value: String) {
        encryptedPrefs.edit().putString(key, value).apply()
    }

    /**
     * 安全获取字符串 - 从加密的共享偏好设置中读取
     * @param key 键
     * @param defaultValue 默认值
     * @return String 获取的值或默认值
     */
    fun secureGetString(key: String, defaultValue: String = ""): String {
        return encryptedPrefs.getString(key, defaultValue) ?: defaultValue
    }

    /**
     * 保存钱包数据 - 存储加密的钱包信息
     * @param walletAddress 钱包地址
     * @param encryptedPrivateKey 加密的私钥
     * @param encryptedMnemonic 加密的助记词(可为空)
     */
    fun saveWalletData(walletAddress: String, encryptedPrivateKey: String, encryptedMnemonic: String?) {
        // 使用地址作为键的一部分存储私钥
        securePutString("wallet_${walletAddress}_private", encryptedPrivateKey)
        // 如果存在助记词,也存储
        encryptedMnemonic?.let {
            securePutString("wallet_${walletAddress}_mnemonic", it)
        }
        // 设置当前钱包
        securePutString("current_wallet", walletAddress)

        // 更新钱包列表
        val wallets = getWalletList().toMutableSet()
        wallets.add(walletAddress)
        securePutString("wallet_list", wallets.joinToString(","))
    }

    /**
     * 获取钱包私钥 - 解密并返回私钥
     * @param walletAddress 钱包地址
     * @return String? 解密后的私钥,如果不存在则返回null
     */
    @RequiresApi(Build.VERSION_CODES.M)
    fun getWalletPrivateKey(walletAddress: String): String? {
        val encrypted = secureGetString("wallet_${walletAddress}_private")
        return if (encrypted.isNotEmpty()) decryptData(encrypted) else null
    }

    /**
     * 获取钱包列表 - 返回所有已保存的钱包地址
     * @return Set<String> 钱包地址集合
     */
    fun getWalletList(): Set<String> {
        val listStr = secureGetString("wallet_list")
        return if (listStr.isEmpty()) emptySet() else listStr.split(",").toSet()
    }

    /**
     * 获取当前钱包地址
     * @return String? 当前钱包地址,如果不存在则返回null
     */
    fun getCurrentWallet(): String? {
        val address = secureGetString("current_wallet")
        return if (address.isNotEmpty()) address else null
    }

    /**
     * 检查是否存在钱包
     * @return Boolean 是否存在至少一个钱包
     */
    fun hasWallets(): Boolean {
        return getWalletList().isNotEmpty()
    }

    /**
     * 清除所有数据 - 删除所有存储的钱包信息
     */
    fun clearAllData() {
        encryptedPrefs.edit().clear().apply()
    }
}
kotlin 复制代码
/**
 * Android加密辅助工具 - 提供在不同Android版本上安全生成密钥对的方法
 */
object AndroidCryptoHelper {

    /**
     * 在 Android 上安全生成 EC 密钥对 - 支持多种备用方法
     * @return ECKeyPair 椭圆曲线密钥对
     */
    fun generateEcKeyPair(): ECKeyPair {
        return try {
            // 方法1: 尝试使用 Web3j 的标准方法(最高效)
            Keys.createEcKeyPair()
        } catch (e: Exception) {
            // 方法2: 如果标准方法失败,使用 Android 兼容的API
            try {
                generateEcKeyPairWithAndroidAPI()
            } catch (e2: Exception) {
                // 方法3: 最终回退方法 - 使用安全的随机数生成
                generateEcKeyPairFallback()
            }
        }
    }

    /**
     * 使用 Android API 生成 EC 密钥对 - 兼容性更好的方法
     * @return ECKeyPair 椭圆曲线密钥对
     */
    private fun generateEcKeyPairWithAndroidAPI(): ECKeyPair {
        // 获取EC密钥对生成器实例
        val keyPairGenerator = KeyPairGenerator.getInstance("EC")
        // 指定使用secp256k1曲线(比特币和以太坊使用的曲线)
        val ecGenParameterSpec = ECGenParameterSpec("secp256k1")
        // 使用安全随机数初始化生成器
        keyPairGenerator.initialize(ecGenParameterSpec, SecureRandom())
        // 生成密钥对
        val keyPair = keyPairGenerator.generateKeyPair()

        // 提取私钥和公钥
        val privateKey = keyPair.private
        val publicKey = keyPair.public

        // 转换为 Web3j 的 ECKeyPair 格式
        // 注意:这是一个简化实现,实际应用中需要更复杂的转换
        val privateKeyBytes = privateKey.encoded
        val publicKeyBytes = publicKey.encoded

        // 使用 Web3j 的工具从字节创建密钥对
        return ECKeyPair.create(privateKeyBytes)
    }

    /**
     * 回退方法 - 使用安全的随机数生成私钥(最基础但可靠的方法)
     * @return ECKeyPair 椭圆曲线密钥对
     */
    private fun generateEcKeyPairFallback(): ECKeyPair {
        // 使用安全随机数生成器
        val secureRandom = SecureRandom()
        val privateKeyBytes = ByteArray(32) // 32字节 = 256位
        secureRandom.nextBytes(privateKeyBytes) // 填充随机字节

        // 使用 Web3j 从字节创建密钥对
        return ECKeyPair.create(privateKeyBytes)
    }

    /**
     * 验证私钥格式 - 检查私钥是否符合以太坊标准
     * @param privateKey 要验证的私钥字符串
     * @return Boolean 是否为有效的私钥格式
     */
    fun isValidPrivateKey(privateKey: String): Boolean {
        return try {
            // 移除可能的 0x 前缀
            val cleanKey = if (privateKey.startsWith("0x")) {
                privateKey.substring(2)
            } else {
                privateKey
            }

            // 检查长度 (64个十六进制字符 = 32字节) 和字符有效性
            cleanKey.length == 64 && cleanKey.matches(Regex("[0-9a-fA-F]+"))
        } catch (e: Exception) {
            false // 任何异常都视为无效格式
        }
    }

    /**
     * 标准化私钥格式 - 统一私钥的表示格式
     * @param privateKey 原始私钥字符串
     * @return String 标准化后的私钥(小写,无0x前缀)
     */
    fun normalizePrivateKey(privateKey: String): String {
        var cleanKey = privateKey.trim()
        // 移除0x前缀
        if (cleanKey.startsWith("0x")) {
            cleanKey = cleanKey.substring(2)
        }
        // 转换为小写确保一致性
        return cleanKey.toLowerCase()
    }
}
kotlin 复制代码
/**
 * 钱包主界面
 */
class WalletFragment : Fragment() {

    private var _binding: FragmentWalletBinding? = null
    private val binding get() = _binding!!

    private lateinit var securityManager: SecurityManager
    private lateinit var web3Service: Web3Service
    private var currentAddress: String? = null

    override fun onCreateView(
        inflater: LayoutInflater, container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View {
        _binding = FragmentWalletBinding.inflate(inflater, container, false)
        return binding.root
    }

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)

        securityManager = SecurityManager(requireContext())
        web3Service = Web3Service()

        setupUI()
        loadWalletData()
        setupClickListeners()
    }

    private fun setupUI() {
        currentAddress = securityManager.getCurrentWallet()
        currentAddress?.let { address ->
            binding.tvAddress.text = formatAddress(address)
        }
    }

    private fun loadWalletData() {
        currentAddress?.let { address ->
            lifecycleScope.launch {
                try {
                    val balance = web3Service.getBalance(address)
                    val etherBalance = Convert.fromWei(BigDecimal(balance), Convert.Unit.ETHER)

                    binding.tvBalance.text = "${etherBalance.setScale(4, BigDecimal.ROUND_HALF_UP)} ETH"
                    binding.tvBalanceFiat.text = "≈ $${(etherBalance.toDouble() * 1800).format(2)}"

                } catch (e: Exception) {
                    showError("获取余额失败: ${e.message}")
                }
            }
        }
    }

    private fun setupClickListeners() {
        binding.btnCopyAddress.setOnClickListener {
            currentAddress?.let { address ->
                copyToClipboard(address)
                showMessage("地址已复制到剪贴板")
            }
        }

        binding.btnSend.setOnClickListener {
            showMessage("发送功能开发中...")
        }

        binding.btnReceive.setOnClickListener {
            showMessage("接收功能开发中...")
        }
    }

    private fun formatAddress(address: String): String {
        return if (address.length > 12) {
            "${address.substring(0, 6)}...${address.substring(address.length - 6)}"
        } else {
            address
        }
    }

    private fun copyToClipboard(text: String) {
        val clipboard = requireContext().getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
        val clip = ClipData.newPlainText("钱包地址", text)
        clipboard.setPrimaryClip(clip)
    }

    private fun showMessage(message: String) {
        android.widget.Toast.makeText(requireContext(), message, android.widget.Toast.LENGTH_SHORT).show()
    }

    private fun showError(message: String) {
        android.widget.Toast.makeText(requireContext(), message, android.widget.Toast.LENGTH_LONG).show()
    }

    private fun Double.format(digits: Int) = "%.${digits}f".format(this)

    override fun onDestroyView() {
        super.onDestroyView()
        _binding = null
    }
}
kotlin 复制代码
/**
 * 设置
 */
class SettingsFragment : Fragment() {

    private var _binding: FragmentSettingsBinding? = null
    private val binding get() = _binding!!
    private lateinit var securityManager: SecurityManager

    override fun onCreateView(
        inflater: LayoutInflater, container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View {
        _binding = FragmentSettingsBinding.inflate(inflater, container, false)
        return binding.root
    }

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)

        securityManager = SecurityManager(requireContext())

        binding.btnClearData.setOnClickListener {
            securityManager.clearAllData()
            showMessage("数据已清除")

            requireActivity().supportFragmentManager.beginTransaction()
                .replace(R.id.fragment_container, CreateWalletFragment())
                .commit()

            (requireActivity() as MainActivity).hideBottomNavigation()
        }
    }

    private fun showMessage(message: String) {
        android.widget.Toast.makeText(requireContext(), message, android.widget.Toast.LENGTH_SHORT)
            .show()
    }

    override fun onDestroyView() {
        super.onDestroyView()
        _binding = null
    }
}
kotlin 复制代码
/**
 * 导入私钥页面
 */
class ImportPrivateKeyFragment : Fragment() {

    private var _binding: FragmentImportPrivateKeyBinding? = null
    private val binding get() = _binding!!

    private lateinit var securityManager: SecurityManager
    private lateinit var web3Service: Web3Service

    override fun onCreateView(
        inflater: LayoutInflater, container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View {
        _binding = FragmentImportPrivateKeyBinding.inflate(inflater, container, false)
        return binding.root
    }

    @RequiresApi(Build.VERSION_CODES.M)
    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)

        securityManager = SecurityManager(requireContext())
        web3Service = Web3Service()

        binding.btnImport.setOnClickListener {
            importWalletFromPrivateKey()
        }
    }

    @RequiresApi(Build.VERSION_CODES.M)
    private fun importWalletFromPrivateKey() {
        val privateKey = binding.etPrivateKey.text.toString().trim()

        if (privateKey.isEmpty()) {
            showError("请输入私钥")
            return
        }

        try {
            val credentials = web3Service.importWalletFromPrivateKey(privateKey)

            val encryptedPrivateKey = securityManager.encryptData(privateKey)

            securityManager.saveWalletData(
                credentials.address,
                encryptedPrivateKey,
                null
            )

            (requireActivity() as MainActivity).showBottomNavigation()
            requireActivity().supportFragmentManager.beginTransaction()
                .replace(R.id.fragment_container, com.learn.web3walletdemo.ui.wallet.WalletFragment())
                .commit()

            showMessage("钱包导入成功")

        } catch (e: Exception) {
            showError("导入钱包失败: ${e.message}")
        }
    }

    private fun showError(message: String) {
        android.widget.Toast.makeText(requireContext(), message, android.widget.Toast.LENGTH_LONG).show()
    }

    private fun showMessage(message: String) {
        android.widget.Toast.makeText(requireContext(), message, android.widget.Toast.LENGTH_SHORT).show()
    }

    override fun onDestroyView() {
        super.onDestroyView()
        _binding = null
    }
}
kotlin 复制代码
/**
 * 导入助记词页面
 */
class ImportMnemonicFragment : Fragment() {

    private var _binding: FragmentImportMnemonicBinding? = null
    private val binding get() = _binding!!

    private lateinit var securityManager: SecurityManager
    private lateinit var web3Service: Web3Service

    override fun onCreateView(
        inflater: LayoutInflater, container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View {
        _binding = FragmentImportMnemonicBinding.inflate(inflater, container, false)
        return binding.root
    }

    @RequiresApi(Build.VERSION_CODES.M)
    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)

        securityManager = SecurityManager(requireContext())
        web3Service = Web3Service()

        binding.btnImport.setOnClickListener {
            importWalletFromMnemonic()
        }
    }

    @RequiresApi(Build.VERSION_CODES.M)
    private fun importWalletFromMnemonic() {
        val mnemonic = binding.etMnemonic.text.toString().trim()

        if (mnemonic.isEmpty()) {
            showError("请输入助记词")
            return
        }

        try {
            val password = "default_password"
            val walletInfo = web3Service.createWalletFromMnemonic(mnemonic, password)

            val encryptedPrivateKey = securityManager.encryptData(walletInfo.privateKey)
            val encryptedMnemonic = securityManager.encryptData(mnemonic)

            securityManager.saveWalletData(
                walletInfo.address,
                encryptedPrivateKey,
                encryptedMnemonic
            )

            (requireActivity() as MainActivity).showBottomNavigation()
            requireActivity().supportFragmentManager.beginTransaction()
                .replace(R.id.fragment_container, WalletFragment())
                .commit()

            showMessage("钱包导入成功")

        } catch (e: Exception) {
            showError("导入钱包失败: ${e.message}")
        }
    }

    private fun showError(message: String) {
        android.widget.Toast.makeText(requireContext(), message, android.widget.Toast.LENGTH_LONG).show()
    }

    private fun showMessage(message: String) {
        android.widget.Toast.makeText(requireContext(), message, android.widget.Toast.LENGTH_SHORT).show()
    }

    override fun onDestroyView() {
        super.onDestroyView()
        _binding = null
    }
}
kotlin 复制代码
/**
 * 1. CreateWalletFragment (创建钱包页面)
 */
class CreateWalletFragment : Fragment() {

    private var _binding: FragmentCreateWalletBinding? = null
    private val binding get() = _binding!!

    private lateinit var securityManager: SecurityManager
    private lateinit var web3Service: Web3Service

    override fun onCreateView(
        inflater: LayoutInflater, container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View {
        _binding = FragmentCreateWalletBinding.inflate(inflater, container, false)
        return binding.root
    }

    @RequiresApi(Build.VERSION_CODES.M)
    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)

        securityManager = SecurityManager(requireContext())
        web3Service = Web3Service()

        setupClickListeners()
    }

    @RequiresApi(Build.VERSION_CODES.M)
    private fun setupClickListeners() {
        binding.btnCreateNew.setOnClickListener {
            createNewWallet()
        }

        binding.btnImportMnemonic.setOnClickListener {
            replaceFragment(ImportMnemonicFragment())
        }

        binding.btnImportPrivateKey.setOnClickListener {
            replaceFragment(ImportPrivateKeyFragment())
        }
    }

    @RequiresApi(Build.VERSION_CODES.M)
    private fun createNewWallet() {
        val password = "default_password"

        try {
            val walletInfo = web3Service.createNewWallet()

            val encryptedPrivateKey = securityManager.encryptData(walletInfo.privateKey)
            securityManager.saveWalletData(
                walletInfo.address,
                encryptedPrivateKey,
                null
            )

            val backupFragment = BackupPrivateKeyFragment().apply {
                arguments = Bundle().apply {
                    putString("privateKey", walletInfo.privateKey)
                    putString("address", walletInfo.address)
                }
            }
            replaceFragment(backupFragment)

        } catch (e: Exception) {
            Log.e("aaaa", e.message.toString())
            showError("创建钱包失败: ${e.message}")
        }
    }

    private fun replaceFragment(fragment: Fragment) {
        requireActivity().supportFragmentManager.beginTransaction()
            .replace(R.id.fragment_container, fragment)
            .addToBackStack(null)
            .commit()
    }

    private fun showError(message: String) {
        android.widget.Toast.makeText(requireContext(), message, android.widget.Toast.LENGTH_LONG)
            .show()
    }

    override fun onDestroyView() {
        super.onDestroyView()
        _binding = null
    }
}
kotlin 复制代码
/**
 * 备份私钥页面
 */
class BackupPrivateKeyFragment : Fragment() {

    private var _binding: FragmentBackupPrivateKeyBinding? = null
    private val binding get() = _binding!!

    private lateinit var privateKey: String
    private lateinit var address: String

    override fun onCreateView(
        inflater: LayoutInflater, container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View {
        _binding = FragmentBackupPrivateKeyBinding.inflate(inflater, container, false)
        return binding.root
    }

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)

        privateKey = arguments?.getString("privateKey") ?: ""
        address = arguments?.getString("address") ?: ""

        setupUI()
    }

    private fun setupUI() {
        binding.tvPrivateKey.text = privateKey
        binding.tvAddress.text = address

        binding.btnContinue.setOnClickListener {
            (requireActivity() as MainActivity).showBottomNavigation()
            requireActivity().supportFragmentManager.beginTransaction()
                .replace(R.id.fragment_container, WalletFragment())
                .commit()
        }

        binding.btnCopyPrivateKey.setOnClickListener {
            copyToClipboard(privateKey)
            showMessage("私钥已复制到剪贴板")
        }
    }

    private fun copyToClipboard(text: String) {
        val clipboard = requireContext().getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
        val clip = ClipData.newPlainText("私钥", text)
        clipboard.setPrimaryClip(clip)
    }

    private fun showMessage(message: String) {
        android.widget.Toast.makeText(requireContext(), message, android.widget.Toast.LENGTH_SHORT).show()
    }

    override fun onDestroyView() {
        super.onDestroyView()
        _binding = null
    }
}
相关推荐
野老杂谈2 小时前
【web3】通证(Token)核心解析
web3
野老杂谈3 小时前
【web3】MetaMask 测试网查看部署合约生成的通证
web3·区块链
Xiaoxiaoxiao02093 小时前
GAEA:打造情感智能 AI 与 Web3 社区的未来
人工智能·web3·区块链
TechubNews14 小时前
香港 Web3 每日必读:华夏港元数字货币基金将在港交所上市买卖
web3
0***R51514 小时前
Web3.0在去中心化应用中的事件监听
web3·去中心化·区块链
许强0xq14 小时前
Q19: fallback 和 receive 有什么区别?
面试·web3·区块链·solidity·以太坊·evm
芒果量化14 小时前
区块链 - 铸币、转账实例介绍solidity
web3·区块链·智能合约
cipher20 小时前
纯 Viem 脚手架:最干净的链上交互方式
typescript·web3
weixin79893765432...1 天前
Web3 基于区块链的下一代互联网(科普)
web3·区块链·智能合约·solidity·钱包