为了加深自己理解特写一篇基于个人现有理解编写的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
}
}