使用Kotlin开发Android区块链钱包实战指南

488 阅读4分钟

一、钱包核心架构设计

分层实现方案:

  1. 安全层:密钥存储与加密模块
  2. 业务层:交易处理与区块链交互
  3. 网络层:节点通信与API管理
  4. UI层:用户界面与交互

二、完整代码实现

1. 安全模块:助记词与密钥管理

// MnemonicManager.kt
object MnemonicManager {
    private const val ENTROPY_BITS = 128 // 对应12个助记词

    fun generateMnemonic(): List<String> {
        val entropy = ByteArray(ENTROPY_BITS / 8).apply {
            SecureRandom().nextBytes(this)
        }
        return MnemonicCode().toMnemonic(entropy)
    }

    fun validateMnemonic(mnemonic: List<String>): Boolean {
        return try {
            MnemonicCode().check(mnemonic)
            true
        } catch (e: MnemonicException) {
            false
        }
    }
}

// KeyStorage.kt
class KeyStorage(context: Context) {
    private val androidKeyStore = AndroidKeyStore.getInstance()
    private val sharedPrefs = context.getSharedPreferences("wallet_prefs", MODE_PRIVATE)

    fun encryptAndSaveMnemonic(mnemonic: List<String>, password: String) {
        val cipher = Cipher.getInstance("AES/GCM/NoPadding")
        val key = generateKey(password)
        cipher.init(Cipher.ENCRYPT_MODE, key)
        
        val encrypted = cipher.doFinal(mnemonic.joinToString(" ").toByteArray())
        val iv = cipher.iv
        
        sharedPrefs.edit {
            putString("encrypted_mnemonic", Base64.encodeToString(encrypted, Base64.DEFAULT))
            putString("iv", Base64.encodeToString(iv, Base64.DEFAULT))
        }
    }

    private fun generateKey(password: String): SecretKey {
        val keySpec = PBEKeySpec(
            password.toCharArray(),
            "salt_value".toByteArray(),
            10000,
            256
        )
        return SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256")
            .generateSecret(keySpec)
    }
}

2. 钱包核心逻辑实现

// EthereumWallet.kt
class EthereumWallet(private val context: Context) {
    private val web3j: Web3j by lazy {
        Web3j.build(HttpService("https://mainnet.infura.io/v3/YOUR_API_KEY"))
    }

    suspend fun createWallet(password: String): WalletInfo {
        return withContext(Dispatchers.IO) {
            // 生成助记词
            val mnemonic = MnemonicManager.generateMnemonic()
            
            // 生成确定性钱包
            val seed = DeterministicSeed(mnemonic, null, "", System.currentTimeMillis())
            val masterKey = HDKeyDerivation.createMasterPrivateKey(seed.seedBytes)
            
            // 根据BIP44路径派生
            val path = "m/44'/60'/0'/0/0"
            val derivedKey = derivePath(path, masterKey)
            
            // 创建凭证
            val credentials = Credentials.create(
                Numeric.toHexStringNoPrefix(derivedKey.privKeyBytes)
            )
            
            // 加密存储
            KeyStorage(context).encryptAndSaveMnemonic(mnemonic, password)
            
            WalletInfo(
                address = credentials.address,
                publicKey = Numeric.toHexStringNoPrefix(credentials.ecKeyPair.publicKey.toByteArray()),
                mnemonic = mnemonic
            )
        }
    }

    private fun derivePath(path: String, masterKey: DeterministicKey): DeterministicKey {
        val parts = path.split("/").drop(1)
        var key = masterKey
        for (segment in parts) {
            val hardened = segment.endsWith("'")
            val index = segment.replace("'", "").toInt()
            key = HDKeyDerivation.deriveChildKey(key, 
                if (hardened) ChildNumber(index, true) 
                else ChildNumber(index, false)
            )
        }
        return key
    }

    data class WalletInfo(
        val address: String,
        val publicKey: String,
        val mnemonic: List<String>
    )
}

3. 交易处理模块

// TransactionManager.kt
class TransactionManager(
    private val web3j: Web3j,
    private val credentials: Credentials
) {
    private val gasEstimator = GasEstimator(web3j)

    suspend fun sendEther(
        toAddress: String,
        amountInEther: Double
    ): TransactionResult {
        return withContext(Dispatchers.IO) {
            // 参数验证
            if (!WalletUtils.isValidAddress(toAddress)) {
                throw IllegalArgumentException("Invalid recipient address")
            }

            // 单位转换
            val value = Convert.toWei(amountInEther.toString(), Convert.Unit.ETHER).toBigInteger()
            
            // 获取nonce
            val nonce = web3j.ethGetTransactionCount(
                credentials.address, 
                DefaultBlockParameterName.PENDING
            ).send().transactionCount
            
            // 估算Gas
            val gasParams = gasEstimator.estimateGas()
            
            // 构造原始交易
            val rawTx = RawTransaction.createEtherTransaction(
                nonce,
                gasParams.gasPrice,
                gasParams.gasLimit,
                toAddress,
                value
            )
            
            // 签名交易
            val signedMessage = TransactionEncoder.signMessage(rawTx, credentials)
            val hexValue = Numeric.toHexString(signedMessage)
            
            // 广播交易
            val txHash = web3j.ethSendRawTransaction(hexValue).send().transactionHash
            
            TransactionResult(
                txHash = txHash,
                status = TransactionStatus.PENDING
            )
        }
    }

    data class TransactionResult(
        val txHash: String,
        val status: TransactionStatus
    )

    enum class TransactionStatus { PENDING, CONFIRMED, FAILED }
}

// GasEstimator.kt
class GasEstimator(private val web3j: Web3j) {
    suspend fun estimateGas(): GasParams {
        return try {
            val gasPrice = web3j.ethGasPrice().send().gasPrice
            val gasLimit = BigInteger.valueOf(21_000) // 简单转账的基础Gas
            GasParams(gasPrice, gasLimit)
        } catch (e: Exception) {
            // 异常处理逻辑
            GasParams.default()
        }
    }

    data class GasParams(
        val gasPrice: BigInteger,
        val gasLimit: BigInteger
    ) {
        companion object {
            fun default() = GasParams(
                BigInteger.valueOf(20_000_000_000L), // 20 Gwei
                BigInteger.valueOf(21_000)
            )
        }
    }
}

三、Android UI集成示例

1. 创建钱包界面

// CreateWalletFragment.kt
class CreateWalletFragment : Fragment() {
    private val viewModel: WalletViewModel by viewModels()
    
    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        
        btnCreateWallet.setOnClickListener {
            val password = etPassword.text.toString()
            if (password.length < 8) {
                showError("密码至少需要8个字符")
                return@setOnClickListener
            }
            
            viewModel.createWallet(password).observe(viewLifecycleOwner) { state ->
                when (state) {
                    is WalletState.Loading -> showProgress()
                    is WalletState.Success -> showWalletInfo(state.walletInfo)
                    is WalletState.Error -> showError(state.message)
                }
            }
        }
    }
    
    private fun showWalletInfo(info: WalletInfo) {
        // 显示地址和助记词警告信息
    }
}

// WalletViewModel.kt
class WalletViewModel : ViewModel() {
    private val _walletState = MutableLiveData<WalletState>()
    val walletState: LiveData<WalletState> = _walletState

    fun createWallet(password: String) {
        viewModelScope.launch {
            _walletState.value = WalletState.Loading
            try {
                val wallet = EthereumWallet(context).createWallet(password)
                _walletState.value = WalletState.Success(wallet)
            } catch (e: Exception) {
                _walletState.value = WalletState.Error("创建钱包失败: ${e.message}")
            }
        }
    }

    sealed class WalletState {
        object Loading : WalletState()
        data class Success(val walletInfo: WalletInfo) : WalletState()
        data class Error(val message: String) : WalletState()
    }
}

2. 交易确认对话框

class TransactionDialog : DialogFragment() {
    fun showTransactionDetails(txData: TxData) {
        // 显示交易详细信息
        binding.tvToAddress.text = txData.toAddress
        binding.tvAmount.text = "${txData.amount} ETH"
        binding.tvFee.text = "矿工费: ${txData.fee} ETH"
        
        binding.btnConfirm.setOnClickListener {
            viewModel.sendTransaction(txData).observe(this) { result ->
                when (result) {
                    is TransactionResult.Success -> showTxHash(result.txHash)
                    is TransactionResult.Error -> showError(result.message)
                }
            }
        }
    }
}

四、安全增强实现

1. 生物特征认证集成

class BiometricAuthHelper(
    private val context: Context,
    private val cryptoObject: BiometricPrompt.CryptoObject
) {
    fun authenticate(callback: (Boolean) -> Unit) {
        val executor = ContextCompat.getMainExecutor(context)
        val promptInfo = BiometricPrompt.PromptInfo.Builder()
            .setTitle("钱包认证")
            .setSubtitle("使用生物特征访问钱包")
            .setNegativeButtonText("取消")
            .build()

        val biometricPrompt = BiometricPrompt(context as FragmentActivity, executor,
            object : BiometricPrompt.AuthenticationCallback() {
                override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) {
                    super.onAuthenticationSucceeded(result)
                    callback(true)
                }

                override fun onAuthenticationFailed() {
                    super.onAuthenticationFailed()
                    callback(false)
                }
            })

        biometricPrompt.authenticate(promptInfo, cryptoObject)
    }
}

2. 安全键盘实现

<!-- secure_keyboard.xml -->
<androidx.gridlayout.widget.GridLayout
    android:id="@+id/keyboard"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    app:columnCount="3">

    <Button
        android:id="@+id/btn1"
        android:text="1"
        style="@style/KeyboardButton"
        android:inputType="none"/>
        
    <!-- 其他数字按钮... -->
        
    <Button
        android:id="@+id/btnDelete"
        android:text="←"
        style="@style/KeyboardButton"
        android:background="?selectableItemBackgroundBorderless"/>
</androidx.gridlayout.widget.GridLayout>
class SecureKeyboard(context: Context, attrs: AttributeSet) : GridLayout(context, attrs) {
    
    fun setupWithPinEntry(pinEntry: EditText) {
        for (i in 0 until childCount) {
            val child = getChildAt(i)
            if (child is Button) {
                child.setOnClickListener {
                    when (child.id) {
                        R.id.btnDelete -> {
                            val text = pinEntry.text
                            if (text.isNotEmpty()) {
                                pinEntry.setText(text.substring(0, text.length - 1))
                            }
                        }
                        else -> {
                            pinEntry.append(child.text)
                        }
                    }
                }
            }
        }
    }
}

五、测试方案

1. 单元测试示例

@RunWith(AndroidJUnit4::class)
class WalletUnitTest {
    private val testContext = InstrumentationRegistry.getInstrumentation().targetContext

    @Test
    fun testMnemonicGeneration() {
        val mnemonic = MnemonicManager.generateMnemonic()
        assertEquals(12, mnemonic.size)
        assertTrue(MnemonicManager.validateMnemonic(mnemonic))
    }

    @Test
    fun testKeyDerivation() {
        val testMnemonic = listOf(
            "abandon", "ability", "able", "about", "above", "absent",
            "absorb", "abstract", "absurd", "abuse", "access", "accident"
        )
        
        val wallet = EthereumWallet(testContext)
        val info = runBlocking { wallet.createWallet("test1234") }
        
        assertEquals("0x9858Ef...", info.address.substring(0, 10))
    }
}

2. UI测试示例

@RunWith(AndroidJUnit4::class)
class WalletUiTest {
    @get:Rule
    val activityRule = ActivityScenarioRule(MainActivity::class.java)

    @Test
    fun testCreateWalletFlow() {
        onView(withId(R.id.etPassword)).perform(typeText("StrongPass123!"))
        onView(withId(R.id.btnCreate)).perform(click())
        
        // 验证进度条显示
        onView(withId(R.id.progressBar))
            .check(matches(isDisplayed()))
            
        // 验证结果展示
        onView(withId(R.id.tvAddress))
            .check(matches(withText(startsWith("0x"))))
    }
}

六、部署与优化

1. ProGuard规则示例

# 保留web3j相关类
-keep class org.web3j.** { *; }
-keep class org.bouncycastle.** { *; }

# 保留钱包核心类
-keep class com.example.wallet.core.** { *; }

# 保留数据模型
-keep class com.example.wallet.model.** { *; }

2. 性能优化建议

  • 使用缓存策略管理区块链数据
  • 实现交易历史的分页加载
  • 使用WorkManager处理后台同步
  • 优化Gas Price预测算法

七、完整项目结构

app/
├── src/
│   ├── main/
│   │   ├── java/com/example/wallet/
│   │   │   ├── core/      # 核心钱包逻辑
│   │   │   ├── security/  # 安全模块
│   │   │   ├── network/   # 网络通信
│   │   │   ├── ui/        # 界面相关
│   │   │   └── di/        # 依赖注入
│   │   └── res/
│   │       ├── layout/
│   │       └── values/
│   └── test/              # 单元测试
└── build.gradle

八、后续开发路线

  1. 多链支持扩展

    enum class BlockchainNetwork(
        val rpcUrl: String,
        val chainId: Int
    ) {
        ETH_MAINNET(
            "https://mainnet.infura.io/v3/",
            1
        ),
        BSC_MAINNET(
            "https://bsc-dataseed.binance.org/",
            56
        )
    }
    
  2. 智能合约交互

    suspend fun callContract(
        contractAddress: String,
        function: Function,
        value: BigInteger
    ) {
        val contract = SmartContract.load(
            contractAddress,
            web3j,
            credentials,
            Contract.GAS_PRICE,
            Contract.GAS_LIMIT
        )
        val result = contract.execute(function).sendAsync().get()
    }
    

通过以上完整实现,开发者可以构建一个具备基础功能的区块链钱包应用。在实际生产环境中,还需结合具体业务需求添加以下功能:

  1. 多语言国际化支持
  2. 交易加速/取消功能
  3. 市场行情集成
  4. 智能合约模板库
  5. 合规性检查模块 安全考虑,实际开发请勿直接使用示例密钥