@@ -14,11 +14,14 @@ import kotlinx.coroutines.launch
1414import one.mixin.android.Constants
1515import one.mixin.android.R
1616import one.mixin.android.RxBus
17+ import one.mixin.android.api.request.web3.WalletRequest
18+ import one.mixin.android.api.request.web3.Web3AddressRequest
1719import one.mixin.android.crypto.CryptoWalletHelper
1820import one.mixin.android.databinding.FragmentComposeBinding
1921import one.mixin.android.db.web3.vo.Web3Address
2022import one.mixin.android.event.WalletOperationType
2123import one.mixin.android.event.WalletRefreshedEvent
24+ import one.mixin.android.extension.decodeBase64
2225import one.mixin.android.extension.openUrl
2326import one.mixin.android.extension.toast
2427import one.mixin.android.ui.common.BaseFragment
@@ -28,7 +31,16 @@ import one.mixin.android.ui.qr.CaptureActivity
2831import one.mixin.android.ui.wallet.viewmodel.FetchWalletViewModel
2932import one.mixin.android.util.viewBinding
3033import one.mixin.android.vo.WalletCategory
34+ import one.mixin.android.repository.Web3Repository
35+ import one.mixin.android.session.Session
36+ import one.mixin.android.tip.bip44.Bip44Path
37+ import org.bitcoinj.base.ScriptType
38+ import org.bitcoinj.crypto.ECKey
39+ import org.web3j.utils.Numeric
3140import timber.log.Timber
41+ import java.math.BigInteger
42+ import java.time.Instant
43+ import javax.inject.Inject
3244
3345@AndroidEntryPoint
3446class ReImportMnemonicFragment : BaseFragment (R .layout.fragment_compose) {
@@ -42,6 +54,10 @@ class ReImportMnemonicFragment : BaseFragment(R.layout.fragment_compose) {
4254
4355 private var evmAddressInfo: Web3Address ? = null
4456 private var solAddressInfo: Web3Address ? = null
57+ private var btcAddressInfo: Web3Address ? = null
58+
59+ @Inject
60+ lateinit var web3Repository: Web3Repository
4561
4662 override fun onAttach (context : Context ) {
4763 super .onAttach(context)
@@ -66,6 +82,7 @@ class ReImportMnemonicFragment : BaseFragment(R.layout.fragment_compose) {
6682 evmAddressInfo = viewModel.getAddressesByChainId(it, Constants .ChainId .ETHEREUM_CHAIN_ID )
6783 // Solana addresses are derived differently (Ed25519 / Base58), so we validate against a dedicated Solana address entry.
6884 solAddressInfo = viewModel.getAddressesByChainId(it, Constants .ChainId .SOLANA_CHAIN_ID )
85+ btcAddressInfo = viewModel.getAddressesByChainId(it, Constants .ChainId .BITCOIN_CHAIN_ID )
6986 }
7087 }
7188
@@ -83,6 +100,7 @@ class ReImportMnemonicFragment : BaseFragment(R.layout.fragment_compose) {
83100 onComplete = { words ->
84101 lifecycleScope.launch {
85102 viewModel.saveWeb3PrivateKey(requireContext(), viewModel.getSpendKey()!! , walletId!! , words)
103+ ensureBtcAddress(walletId, words)
86104 validateMnemonicForOtherWallets(words)
87105 toast(R .string.Success )
88106 RxBus .publish(WalletRefreshedEvent (walletId, WalletOperationType .CREATE ))
@@ -97,24 +115,76 @@ class ReImportMnemonicFragment : BaseFragment(R.layout.fragment_compose) {
97115
98116 private fun validateMnemonic (mnemonic : List <String >): String? {
99117 val mnemonicPhrase = mnemonic.joinToString(" " )
118+ val index: Int? = resolveDerivationIndex()
100119 evmAddressInfo?.let {
101- val index = CryptoWalletHelper .extractIndexFromPath(it.path!! )
102- val derivedAddress = CryptoWalletHelper .mnemonicToAddress(mnemonicPhrase, Constants .ChainId .ETHEREUM_CHAIN_ID , " " , index!! )
120+ val derivedAddress = CryptoWalletHelper .mnemonicToAddress(mnemonicPhrase, Constants .ChainId .ETHEREUM_CHAIN_ID , " " , requireNotNull(index))
103121 if (! derivedAddress.equals(it.destination, ignoreCase = true )) {
104122 return getString(R .string.reimport_mnemonic_phrase_error)
105123 }
106124 }
107125
108126 solAddressInfo?.let {
109- val index = CryptoWalletHelper .extractIndexFromPath(it.path!! )
110- val derivedAddress = CryptoWalletHelper .mnemonicToAddress(mnemonicPhrase, Constants .ChainId .SOLANA_CHAIN_ID , " " , index!! )
127+ val derivedAddress = CryptoWalletHelper .mnemonicToAddress(mnemonicPhrase, Constants .ChainId .SOLANA_CHAIN_ID , " " , requireNotNull(index))
128+ if (! derivedAddress.equals(it.destination, ignoreCase = true )) {
129+ return getString(R .string.reimport_mnemonic_phrase_error)
130+ }
131+ }
132+
133+ btcAddressInfo?.let {
134+ val derivedAddress = CryptoWalletHelper .mnemonicToAddress(mnemonicPhrase, Constants .ChainId .BITCOIN_CHAIN_ID , " " , requireNotNull(index))
111135 if (! derivedAddress.equals(it.destination, ignoreCase = true )) {
112136 return getString(R .string.reimport_mnemonic_phrase_error)
113137 }
114138 }
115139 return null
116140 }
117141
142+ private fun resolveDerivationIndex (): Int? {
143+ val candidatePath: String? = evmAddressInfo?.path ? : solAddressInfo?.path ? : btcAddressInfo?.path
144+ if (candidatePath.isNullOrBlank()) {
145+ return null
146+ }
147+ return CryptoWalletHelper .extractIndexFromPath(candidatePath)
148+ }
149+
150+ private suspend fun ensureBtcAddress (walletId : String , mnemonic : List <String >) {
151+ val hasBtcAddress: Boolean = viewModel.getAddressesByChainId(walletId, Constants .ChainId .BITCOIN_CHAIN_ID ) != null
152+ if (hasBtcAddress) {
153+ return
154+ }
155+ val evmAddress: Web3Address ? = viewModel.getAddressesByChainId(walletId, Constants .ChainId .ETHEREUM_CHAIN_ID )
156+ val solAddress: Web3Address ? = viewModel.getAddressesByChainId(walletId, Constants .ChainId .SOLANA_CHAIN_ID )
157+ val btcAddress: Web3Address ? = viewModel.getAddressesByChainId(walletId, Constants .ChainId .BITCOIN_CHAIN_ID )
158+ val candidatePath: String? = evmAddress?.path ? : solAddress?.path ? : btcAddress?.path
159+ val derivationIndex: Int = requireNotNull(candidatePath?.let { CryptoWalletHelper .extractIndexFromPath(it) })
160+ val mnemonicPhrase: String = mnemonic.joinToString(" " )
161+ val derivedWallet = CryptoWalletHelper .mnemonicToBitcoinSegwitWallet(mnemonicPhrase, index = derivationIndex)
162+ val destination: String = derivedWallet.address
163+ val privateKey: ByteArray = Numeric .hexStringToByteArray(derivedWallet.privateKey)
164+ val now: Instant = Instant .now()
165+ val userId: String = requireNotNull(Session .getAccountId())
166+ val message = " $destination \n $userId \n ${now.epochSecond} "
167+ val ecKey: ECKey = ECKey .fromPrivate(BigInteger (1 , privateKey), true )
168+ val signature: String = Numeric .toHexString(ecKey.signMessage(message, ScriptType .P2WPKH ).decodeBase64())
169+ val updateRequest = WalletRequest (
170+ name = null ,
171+ category = null ,
172+ addresses = listOf (
173+ Web3AddressRequest (
174+ destination = destination,
175+ chainId = Constants .ChainId .BITCOIN_CHAIN_ID ,
176+ path = Bip44Path .bitcoinSegwitPathString(derivationIndex),
177+ signature = signature,
178+ timestamp = now.toString(),
179+ ),
180+ ),
181+ )
182+ val response = web3Repository.updateWallet(walletId, updateRequest)
183+ if (! response.isSuccess) {
184+ Timber .e(" Failed to update BTC address walletId=$walletId errorCode=${response.errorCode} errorDescription=${response.errorDescription} " )
185+ }
186+ }
187+
118188 private suspend fun validateMnemonicForOtherWallets (mnemonic : List <String >) {
119189 val mnemonicPhrase: String = mnemonic.joinToString(" " )
120190 val currentSpendKey: ByteArray? = viewModel.getSpendKey()
@@ -146,6 +216,7 @@ class ReImportMnemonicFragment : BaseFragment(R.layout.fragment_compose) {
146216 if (! isSaved) {
147217 return @forEach
148218 }
219+ runCatching { ensureBtcAddress(wallet.id, mnemonic) }
149220 RxBus .publish(WalletRefreshedEvent (wallet.id, WalletOperationType .CREATE ))
150221 Timber .e(" Save wallet key: ${wallet.id} " )
151222 return @forEach
0 commit comments