Skip to content

Commit 8274537

Browse files
committed
Update upgrade wallet
1 parent ca2a936 commit 8274537

File tree

11 files changed

+160
-120
lines changed

11 files changed

+160
-120
lines changed

app/src/main/java/one/mixin/android/crypto/CryptoWalletHelper.kt

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -172,16 +172,16 @@ object CryptoWalletHelper {
172172
): String {
173173
return when (chainId) {
174174
Constants.ChainId.SOLANA_CHAIN_ID -> {
175-
val keyPair = SolanaKeyGenerator.getPrivateKeyFromMnemonic(mnemonic, passphrase, index)
176-
newKeyPairFromSeed(keyPair).publicKey.encodeToBase58String()
175+
val privateKey: ByteArray = SolanaKeyGenerator.getPrivateKeyFromMnemonic(mnemonic, passphrase, index)
176+
newKeyPairFromSeed(privateKey).publicKey.encodeToBase58String()
177177
}
178178
Constants.ChainId.BITCOIN_CHAIN_ID -> {
179-
val wallet: CryptoWallet = mnemonicToBitcoinSegwitWallet(mnemonic, passphrase, index)
180-
wallet.address
179+
BitcoinKeyGenerator.mnemonicToAddress(mnemonic, passphrase, index)
181180
}
182181
in Constants.Web3EvmChainIds -> {
183-
val privateKey = EthKeyGenerator.getPrivateKeyFromMnemonic(mnemonic, passphrase, index)
184-
?: throw IllegalArgumentException("Private key generation failed")
182+
val privateKey: ByteArray =
183+
EthKeyGenerator.getPrivateKeyFromMnemonic(mnemonic, passphrase, index)
184+
?: throw IllegalArgumentException("Private key generation failed")
185185
EthKeyGenerator.privateKeyToAddress(privateKey)
186186
}
187187
else -> {

app/src/main/java/one/mixin/android/ui/common/BottomSheetViewModel.kt

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1786,6 +1786,19 @@ class BottomSheetViewModel
17861786
return tipPrivToPrivateKey(spendKey, chainId, index)
17871787
}
17881788

1789+
suspend fun getSpendKey(
1790+
context: Context,
1791+
pin: String,
1792+
): ByteArray {
1793+
val result = tip.getOrRecoverTipPriv(context, pin)
1794+
return tip.getSpendPrivFromEncryptedSalt(
1795+
tip.getMnemonicFromEncryptedPreferences(context),
1796+
tip.getEncryptedSalt(context),
1797+
pin,
1798+
result.getOrThrow(),
1799+
)
1800+
}
1801+
17891802
fun web3TokenItems(walletId: String) = tokenRepository.web3TokenItems(walletId)
17901803

17911804
fun web3TokenItemsExcludeHidden(walletId: String, isSend: Boolean): LiveData<List<Web3TokenItem>> {

app/src/main/java/one/mixin/android/ui/home/ExploreFragment.kt

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,6 @@ import one.mixin.android.event.SessionEvent
3232
import one.mixin.android.extension.addFragment
3333
import one.mixin.android.extension.defaultSharedPreferences
3434
import one.mixin.android.extension.dp
35-
import one.mixin.android.extension.isNightMode
3635
import one.mixin.android.extension.navTo
3736
import one.mixin.android.extension.notEmptyWithElse
3837
import one.mixin.android.extension.openPermissionSetting

app/src/main/java/one/mixin/android/ui/home/MainActivity.kt

Lines changed: 19 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -68,11 +68,9 @@ import one.mixin.android.Constants.INTERVAL_7_DAYS
6868
import one.mixin.android.MixinApplication
6969
import one.mixin.android.R
7070
import one.mixin.android.RxBus
71-
import one.mixin.android.api.MixinResponse
7271
import one.mixin.android.api.request.SessionRequest
7372
import one.mixin.android.api.service.ConversationService
7473
import one.mixin.android.api.service.UserService
75-
import one.mixin.android.api.ResponseError
7674
import one.mixin.android.crypto.PrivacyPreference.getIsLoaded
7775
import one.mixin.android.crypto.PrivacyPreference.getIsSyncSession
7876
import one.mixin.android.databinding.ActivityMainBinding
@@ -117,8 +115,8 @@ import one.mixin.android.job.RefreshDappJob
117115
import one.mixin.android.job.RefreshExternalSchemeJob
118116
import one.mixin.android.job.RefreshFiatsJob
119117
import one.mixin.android.job.RefreshOneTimePreKeysJob
120-
import one.mixin.android.job.RefreshStickerAlbumJob
121118
import one.mixin.android.job.RefreshSafeAccountsJob
119+
import one.mixin.android.job.RefreshStickerAlbumJob
122120
import one.mixin.android.job.RefreshUserJob
123121
import one.mixin.android.job.RefreshWeb3Job
124122
import one.mixin.android.job.RestoreTransactionJob
@@ -173,8 +171,8 @@ import one.mixin.android.ui.wallet.TokenListBottomSheetDialogFragment.Companion.
173171
import one.mixin.android.ui.wallet.TokenListBottomSheetDialogFragment.Companion.TYPE_FROM_TRANSFER
174172
import one.mixin.android.ui.wallet.WalletActivity
175173
import one.mixin.android.ui.wallet.WalletActivity.Companion.BUY
176-
import one.mixin.android.ui.wallet.ClassicWalletMissingBtcAddressFragment
177174
import one.mixin.android.ui.wallet.WalletFragment
175+
import one.mixin.android.ui.wallet.WalletMissingBtcAddressFragment
178176
import one.mixin.android.ui.wallet.components.WalletDestination
179177
import one.mixin.android.util.BiometricUtil
180178
import one.mixin.android.util.ErrorHandler
@@ -191,6 +189,7 @@ import one.mixin.android.vo.ConversationStatus
191189
import one.mixin.android.vo.Fiats
192190
import one.mixin.android.vo.Participant
193191
import one.mixin.android.vo.ParticipantRole
192+
import one.mixin.android.vo.WalletCategory
194193
import one.mixin.android.vo.isGroupConversation
195194
import one.mixin.android.web3.js.Web3Signer
196195
import one.mixin.android.worker.SessionWorker
@@ -199,7 +198,7 @@ import java.util.concurrent.TimeUnit
199198
import javax.inject.Inject
200199

201200
@AndroidEntryPoint
202-
class MainActivity : BlazeBaseActivity(), ClassicWalletMissingBtcAddressFragment.Callback {
201+
class MainActivity : BlazeBaseActivity(), WalletMissingBtcAddressFragment.Callback {
203202
private lateinit var navigationController: NavigationController
204203

205204
@Inject
@@ -1083,9 +1082,9 @@ class MainActivity : BlazeBaseActivity(), ClassicWalletMissingBtcAddressFragment
10831082
Timber.e("nav_wallet: ${Session.getAccount()?.hasPin}")
10841083
if (Session.getAccount()?.hasPin == true) {
10851084
lifecycleScope.launch {
1086-
val shouldBlockNavigation: Boolean = shouldShowClassicWalletMissingBtcAddress()
1085+
val shouldBlockNavigation: Boolean = shouldShowWalletMissingBtcAddress()
10871086
if (shouldBlockNavigation) {
1088-
showClassicWalletMissingBtcAddressFragment()
1087+
showWalletMissingBtcAddressFragment()
10891088
isRestoringBottomNavSelection = true
10901089
binding.bottomNav.selectedItemId = lastBottomNavItemId
10911090
return@launch
@@ -1121,32 +1120,34 @@ class MainActivity : BlazeBaseActivity(), ClassicWalletMissingBtcAddressFragment
11211120
findFragmentByTagTyped<ConversationListFragment>(NavigationController.ConversationList.tag)?.hideContainer()
11221121
}
11231122

1124-
private suspend fun shouldShowClassicWalletMissingBtcAddress(): Boolean {
1123+
private suspend fun shouldShowWalletMissingBtcAddress(): Boolean {
11251124
return withContext(Dispatchers.IO) {
1126-
val classicWallets = web3Repository.web3WalletDao.getAllClassicWallets()
1127-
if (classicWallets.isEmpty()) return@withContext false
1128-
val shouldShowBtcAddress: Boolean = classicWallets.any { walletItem ->
1125+
val wallets = web3Repository.getAllWallets().filter { walletItem ->
1126+
walletItem.category == WalletCategory.CLASSIC.value || (walletItem.category == WalletCategory.IMPORTED_MNEMONIC.value && walletItem.hasLocalPrivateKey)
1127+
}
1128+
if (wallets.isEmpty()) return@withContext false
1129+
val shouldShowBtcAddress: Boolean = wallets.any { walletItem ->
11291130
web3Repository.getAddressesByChainId(walletItem.id, Constants.ChainId.BITCOIN_CHAIN_ID) == null
11301131
}
11311132
return@withContext shouldShowBtcAddress
11321133
}
11331134
}
11341135

1135-
private fun showClassicWalletMissingBtcAddressFragment() {
1136-
val fragment = supportFragmentManager.findFragmentByTag(ClassicWalletMissingBtcAddressFragment.TAG)
1136+
private fun showWalletMissingBtcAddressFragment() {
1137+
val fragment = supportFragmentManager.findFragmentByTag(WalletMissingBtcAddressFragment.TAG)
11371138
if (fragment != null) return
1138-
val newFragment = ClassicWalletMissingBtcAddressFragment
1139+
val newFragment = WalletMissingBtcAddressFragment
11391140
.newInstance()
11401141
supportFragmentManager
11411142
.beginTransaction()
11421143
.setReorderingAllowed(true)
1143-
.add(R.id.internal_container, newFragment, ClassicWalletMissingBtcAddressFragment.TAG)
1144-
.addToBackStack(ClassicWalletMissingBtcAddressFragment.TAG)
1144+
.add(R.id.internal_container, newFragment, WalletMissingBtcAddressFragment.TAG)
1145+
.addToBackStack(WalletMissingBtcAddressFragment.TAG)
11451146
.commitAllowingStateLoss()
11461147
}
11471148

1148-
override fun onClassicWalletMissingBtcAddressPinSuccess() {
1149-
supportFragmentManager.popBackStack(ClassicWalletMissingBtcAddressFragment.TAG, androidx.fragment.app.FragmentManager.POP_BACK_STACK_INCLUSIVE)
1149+
override fun onWalletMissingBtcAddressPinSuccess() {
1150+
supportFragmentManager.popBackStack(WalletMissingBtcAddressFragment.TAG, androidx.fragment.app.FragmentManager.POP_BACK_STACK_INCLUSIVE)
11501151
binding.bottomNav.selectedItemId = R.id.nav_wallet
11511152
switchToDestination(NavigationController.Wallet)
11521153
lastBottomNavItemId = R.id.nav_wallet

app/src/main/java/one/mixin/android/ui/wallet/InputFragment.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1241,7 +1241,7 @@ class InputFragment : BaseFragment(R.layout.fragment_input), OnReceiveSelectionC
12411241
val transaction =
12421242
try {
12431243
if (t.chainId == Constants.ChainId.BITCOIN_CHAIN_ID) {
1244-
t.buildTransaction(rpc, fromAddress, toAddress, tokenBalance, web3ViewModel.outputsByAddress(fromAddress), gas)
1244+
t.buildTransaction(rpc, fromAddress, toAddress, "0.00000001", web3ViewModel.outputsByAddress(fromAddress), gas)
12451245
} else {
12461246
t.buildTransaction(rpc, fromAddress, toAddress, tokenBalance)
12471247
}

app/src/main/java/one/mixin/android/ui/wallet/ReImportMnemonicFragment.kt

Lines changed: 75 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,14 @@ import kotlinx.coroutines.launch
1414
import one.mixin.android.Constants
1515
import one.mixin.android.R
1616
import one.mixin.android.RxBus
17+
import one.mixin.android.api.request.web3.WalletRequest
18+
import one.mixin.android.api.request.web3.Web3AddressRequest
1719
import one.mixin.android.crypto.CryptoWalletHelper
1820
import one.mixin.android.databinding.FragmentComposeBinding
1921
import one.mixin.android.db.web3.vo.Web3Address
2022
import one.mixin.android.event.WalletOperationType
2123
import one.mixin.android.event.WalletRefreshedEvent
24+
import one.mixin.android.extension.decodeBase64
2225
import one.mixin.android.extension.openUrl
2326
import one.mixin.android.extension.toast
2427
import one.mixin.android.ui.common.BaseFragment
@@ -28,7 +31,16 @@ import one.mixin.android.ui.qr.CaptureActivity
2831
import one.mixin.android.ui.wallet.viewmodel.FetchWalletViewModel
2932
import one.mixin.android.util.viewBinding
3033
import 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
3140
import timber.log.Timber
41+
import java.math.BigInteger
42+
import java.time.Instant
43+
import javax.inject.Inject
3244

3345
@AndroidEntryPoint
3446
class 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

Comments
 (0)