Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -352,7 +352,7 @@ dependencies {
api fileTree(dir: "libs", include: ["*.aar", "*.jar"], exclude: ["trustwalletcore.aar"])

// Fetch Flow Wallet Kit from GitHub Packages
implementation 'com.github.onflow.flow-wallet-kit:flow-wallet-android:0.2.1'
implementation 'com.github.onflow.flow-wallet-kit:flow-wallet-android:0.2.4'
implementation "org.jetbrains.kotlin:kotlin-stdlib:1.9.22"
/** Android Architecture **/
implementation 'androidx.core:core-ktx:1.13.1'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,10 @@ import com.flowfoundation.wallet.utils.storeWalletPassword
import com.flowfoundation.wallet.manager.walletdata.WalletDataManager

import com.flowfoundation.wallet.manager.walletdata.MainWallet
import com.flowfoundation.wallet.manager.key.storage.KeyStorageManager
import com.flowfoundation.wallet.manager.key.storage.KeyStorageMigration
import com.flowfoundation.wallet.page.restore.keystore.model.KeystoreAddress
import com.flowfoundation.wallet.utils.safeRun
import kotlin.text.isNullOrEmpty

object AccountManager {
Expand All @@ -62,7 +65,6 @@ object AccountManager {
private val listeners = CopyOnWriteArrayList<WeakReference<OnAccountUpdate>>()
private val listListeners = CopyOnWriteArrayList<WeakReference<OnAccountListUpdate>>()
private val userPrefixes = mutableListOf<UserPrefix>()
private val switchAccounts = mutableListOf<LocalSwitchAccount>()

private var currentAccount: Account? = null
private var isInitialized = false
Expand All @@ -80,7 +82,6 @@ object AccountManager {
logd(TAG, "Starting AccountManager initialization")
accounts.clear()
userPrefixes.clear()
switchAccounts.clear()
currentAccount = null

ioScope {
Expand Down Expand Up @@ -125,6 +126,9 @@ object AccountManager {
}

logd(TAG, "AccountManager initialization completed successfully")
// Migrate key material to independent storage now that accounts are loaded.
// This must run inside the ioScope block so accounts list is fully populated.
safeRun { KeyStorageMigration.runMigrationIfNeeded() }
// Update Accounts info with WalletDataManager
WalletDataManager.updateWalletData()

Expand All @@ -140,7 +144,6 @@ object AccountManager {
// Clear potentially corrupted state
accounts.clear()
userPrefixes.clear()
switchAccounts.clear()
currentAccount = null

// Initialization failed - user needs to login/restore
Expand All @@ -151,29 +154,50 @@ object AccountManager {
fun getSwitchAccountList(): List<Any> {
logd(TAG, "getSwitchAccountList() called")
logd(TAG, "Current accounts: $accounts")
logd(TAG, "Current switchAccounts: $switchAccounts")

val list = mutableListOf<Any>()
list.addAll(accounts)

// Collect all FlowWallet addresses for the current network from walletNodes
val currentNetwork = chainNetWorkString()
val addressSet = accounts.flatMap { account ->
account.walletNodes.filterIsInstance<FlowWallet>()
.filter { it.chainIdString == currentNetwork }
.map { it.address }
}.toSet()
val keyOnlyAccounts = buildLocalKeyAccounts()
logd(TAG, "Key-only accounts (no Account object): $keyOnlyAccounts")

logd(TAG, "Address set from accounts (current network): $addressSet")

val filteredSwitchAccounts = switchAccounts.filter { it.address !in addressSet }
logd(TAG, "Filtered switch accounts: $filteredSwitchAccounts")

list.addAll(filteredSwitchAccounts)
list.addAll(keyOnlyAccounts)
logd(TAG, "Final list size: ${list.size}")
return list
}

/**
* Builds a list of [LocalSwitchAccount] entries for UIDs that have key material stored
* in [KeyStorageManager] but no matching [Account] in [accounts] (i.e. the account cache
* was lost while the key survived).
*
* The returned list is computed fresh on each call and is not stored persistently.
*/
private fun buildLocalKeyAccounts(): List<LocalSwitchAccount> {
// Collect all UIDs known to the accounts list
val knownUids = accounts.mapNotNull { it.wallet?.id }.toSet()

// Union of all UIDs present in any of the three key stores
val allKeyUids = (
KeyStorageManager.getAllSeedPhraseUids() +
KeyStorageManager.getAllPrivateKeyUids() +
KeyStorageManager.getAllAndroidKeystoreUids()
).toSet()

// Only keep UIDs that have a key but no account
val orphanUids = allKeyUids - knownUids

return orphanUids.map { uid ->
val akPrefix = KeyStorageManager.getAndroidKeystorePrefix(uid)
LocalSwitchAccount(
username = uid,
address = "",
userId = uid,
prefix = akPrefix
)
}
}

fun add(account: Account, uid: String? = null) {
// Clear WalletManager state before setting new account
WalletManager.clear()
Expand All @@ -191,6 +215,8 @@ object AccountManager {
userPrefixes.removeAll { it.userId == uid}
userPrefixes.add(UserPrefix(uid, prefix))
UserPrefixCacheManager.cache(UserPrefixes().apply { addAll(userPrefixes) })
// Write to independent key storage so prefix survives account-cache loss
KeyStorageManager.saveAndroidKeystorePrefix(uid, prefix)
}
AccountEmojiManager.init()

Expand Down Expand Up @@ -454,11 +480,12 @@ object AccountManager {
return@ioScope
}
isSwitching = true
currentAccount = account
logd(TAG, "Account switched. Current account is now: $currentAccount")
logd(TAG, "Starting account switch to: ${account.userInfo.username}")
switchAccount(account) { isSuccess ->
if (isSuccess) {
isSwitching = false
currentAccount = account
logd(TAG, "Account switch successful. Current account updated to: $currentAccount")
accounts.forEach {
it.isActive = it.userInfo.username == account.userInfo.username
}
Expand Down Expand Up @@ -488,7 +515,9 @@ object AccountManager {
switchAccount(switchAccount) { isSuccess ->
if (isSuccess) {
isSwitching = false
switchAccounts.remove(switchAccount)
// localKeyAccounts is computed dynamically from KeyStorageManager; once the
// account is re-added via the login flow the UID will appear in accounts and
// will be excluded from buildLocalKeyAccounts() automatically.
uiScope {
clearUserCache()
MainActivity.relaunch(Env.getApp(), true)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,11 @@ import com.flowfoundation.wallet.utils.readWalletPassword
import com.flow.wallet.storage.StorageProtocol
import com.flowfoundation.wallet.manager.account.HardwareBackedKeyException
import com.flowfoundation.wallet.manager.account.firstFlowWalletAddress
import com.flowfoundation.wallet.manager.key.storage.KeyStorageManager
import com.flowfoundation.wallet.manager.wallet.WalletManager
import com.flowfoundation.wallet.wallet.DERIVATION_PATH
import com.flowfoundation.wallet.wallet.Wallet
import com.flow.wallet.keys.PrivateKey
import org.onflow.flow.models.toHexString

object CryptoProviderManager {
Expand Down Expand Up @@ -101,6 +103,25 @@ object CryptoProviderManager {
logd(TAG, "generateAccountCryptoProvider: Generating for account: ${account.userInfo.username}, isActive: ${account.isActive}, hasKeystore: ${!account.keyStoreInfo.isNullOrBlank()}")

return try {
// --- New independent key storage (checked first) ---
val uid = account.wallet?.id
if (!uid.isNullOrBlank()) {
// SeedPhraseKey object retrieved directly – no intermediate string reconstruction
val seedPhraseKey = KeyStorageManager.getSeedPhraseKey(uid)
if (seedPhraseKey != null) {
logd(TAG, "New storage: found seed phrase key for uid: $uid")
return HDWalletCryptoProvider(seedPhraseKey)
}
// PrivateKey object retrieved directly – no intermediate string reconstruction
val privateKey = KeyStorageManager.getPrivateKeyObject(uid)
if (privateKey != null) {
logd(TAG, "New storage: found private key object for uid: $uid")
val keyWallet = WalletFactory.createKeyWallet(privateKey, setOf(ChainId.Mainnet, ChainId.Testnet), storage) as KeyWallet
return PrivateKeyCryptoProvider(privateKey, keyWallet)
}
// AKP prefix from new storage falls through to prefix-based branch below
}

// Handle keystore-based accounts
if (!account.keyStoreInfo.isNullOrBlank()) {
logd(TAG, " Branch: Keystore-based account. Info (first 100 chars): ${account.keyStoreInfo!!.take(100)}")
Expand All @@ -125,15 +146,24 @@ object CryptoProviderManager {
return HDWalletCryptoProvider(seedPhraseKey)
}
// Handle prefix-based accounts (legacy or hardware-backed)
else if (!account.prefix.isNullOrBlank()) {
logd(TAG, " Branch: Prefix-based account")
// Also check new AKP storage when account.prefix is absent
else if (!account.prefix.isNullOrBlank() || (!uid.isNullOrBlank() && KeyStorageManager.hasAndroidKeystorePrefix(uid))) {
val effectivePrefix = account.prefix?.takeIf { it.isNotBlank() }
?: uid?.let { KeyStorageManager.getAndroidKeystorePrefix(it) }

logd(TAG, " Branch: Prefix-based account (effectivePrefix from ${if (account.prefix.isNullOrBlank()) "new storage" else "account"})")

if (effectivePrefix.isNullOrBlank()) {
loge(TAG, " Prefix-based: resolved prefix is blank, skipping")
return null
}

// Standard prefix-based account handling (for non-multi-restore accounts)
logd(TAG, " Standard prefix-based account handling")

// Try to get the private key, handling hardware-backed keys
val privateKey = try {
KeyCompatibilityManager.getPrivateKeyWithFallback(account.prefix!!, storage)
KeyCompatibilityManager.getPrivateKeyWithFallback(effectivePrefix, storage)
} catch (e: HardwareBackedKeyException) {
loge(TAG, "Hardware-backed key detected")
return AndroidKeystoreCryptoProvider(e.prefix!!)
Expand Down Expand Up @@ -189,7 +219,7 @@ object CryptoProviderManager {
// Create the provider with the correct algorithms
return PrivateKeyCryptoProvider(privateKey, wallet, determinedSigningAlgorithm, matchedKey.hashingAlgorithm)
} else {
logd(TAG, " Prefix-based: Could NOT find matching on-chain key for ${account.prefix}. Using default signing algorithm: $determinedSigningAlgorithm")
logd(TAG, " Prefix-based: Could NOT find matching on-chain key for $effectivePrefix. Using default signing algorithm: $determinedSigningAlgorithm")
}
} else {
logd(TAG, " Prefix-based: No account address available for on-chain key lookup")
Expand Down Expand Up @@ -234,6 +264,22 @@ object CryptoProviderManager {
val storage = getStorage()

return try {
// --- New independent key storage (checked first) ---
val switchUid = account.wallet?.id
if (!switchUid.isNullOrBlank()) {
val seedPhraseKey = KeyStorageManager.getSeedPhraseKey(switchUid)
if (seedPhraseKey != null) {
logd("CryptoProviderManager", "Switch account new storage: seed phrase key for uid: $switchUid")
return HDWalletCryptoProvider(seedPhraseKey)
}
val privateKey = KeyStorageManager.getPrivateKeyObject(switchUid)
if (privateKey != null) {
logd("CryptoProviderManager", "Switch account new storage: private key object for uid: $switchUid")
val keyWallet = WalletFactory.createKeyWallet(privateKey, setOf(ChainId.Mainnet, ChainId.Testnet), storage) as KeyWallet
return PrivateKeyCryptoProvider(privateKey, keyWallet)
}
}

// Handle keystore-based accounts
if (account.keyStoreInfo.isNullOrBlank().not()) {
PrivateKeyStoreCryptoProvider(account.keyStoreInfo!!)
Expand All @@ -256,10 +302,17 @@ object CryptoProviderManager {
}

// Handle prefix-based accounts (legacy or hardware-backed)
else if (account.prefix.isNullOrBlank().not()) {
// Also check new AKP storage when account.prefix is absent
else if (!account.prefix.isNullOrBlank() || (!switchUid.isNullOrBlank() && KeyStorageManager.hasAndroidKeystorePrefix(switchUid))) {
val switchEffectivePrefix = account.prefix?.takeIf { it.isNotBlank() }
?: switchUid?.let { KeyStorageManager.getAndroidKeystorePrefix(it) }
if (switchEffectivePrefix.isNullOrBlank()) {
loge("CryptoProviderManager", "Switch account: resolved prefix is blank")
return null
}
// Load the stored private key using the prefix-based ID with backward compatibility
val privateKey = try {
KeyCompatibilityManager.getPrivateKeyWithFallback(account.prefix!!, storage)
KeyCompatibilityManager.getPrivateKeyWithFallback(switchEffectivePrefix, storage)
} catch (e: HardwareBackedKeyException) {
loge("CryptoProviderManager", "Hardware-backed key detected for switch account")
loge("CryptoProviderManager", "Creating AndroidKeystoreCryptoProvider for hardware-backed key")
Expand Down Expand Up @@ -362,21 +415,40 @@ object CryptoProviderManager {
val storage = getStorage()

return try {
// --- New independent key storage (checked first for LocalSwitchAccount) ---
val localUid = switchAccount.userId
if (!localUid.isNullOrBlank()) {
val seedPhraseKey = KeyStorageManager.getSeedPhraseKey(localUid)
if (seedPhraseKey != null) {
logd("CryptoProviderManager", "LocalSwitchAccount new storage: seed phrase key for uid: $localUid")
return HDWalletCryptoProvider(seedPhraseKey)
}
val privateKey = KeyStorageManager.getPrivateKeyObject(localUid)
if (privateKey != null) {
logd("CryptoProviderManager", "LocalSwitchAccount new storage: private key object for uid: $localUid")
val keyWallet = WalletFactory.createKeyWallet(privateKey, setOf(ChainId.Mainnet, ChainId.Testnet), storage) as KeyWallet
return PrivateKeyCryptoProvider(privateKey, keyWallet)
}
}

// Handle prefix-based accounts
if (switchAccount.prefix.isNullOrBlank().not()) {
// Also check new AKP storage when switchAccount.prefix is absent
val localEffectivePrefix = switchAccount.prefix?.takeIf { it.isNotBlank() }
?: localUid?.let { KeyStorageManager.getAndroidKeystorePrefix(it) }
if (!localEffectivePrefix.isNullOrBlank()) {
// Load the stored private key using the prefix-based ID with backward compatibility
val privateKey = try {
KeyCompatibilityManager.getPrivateKeyWithFallback(switchAccount.prefix, storage)
KeyCompatibilityManager.getPrivateKeyWithFallback(localEffectivePrefix, storage)
} catch (e: HardwareBackedKeyException) {
loge("CryptoProviderManager", "Hardware-backed key detected for local switch account prefix ${switchAccount.prefix}")
loge("CryptoProviderManager", "Hardware-backed key detected for local switch account prefix $localEffectivePrefix")
loge("CryptoProviderManager", "Creating AndroidKeystoreCryptoProvider for hardware-backed key")

// For LocalSwitchAccount, we use defaults since we don't have wallet address
return AndroidKeystoreCryptoProvider(e.prefix!!)
}

if (privateKey == null) {
loge("CryptoProviderManager", "CRITICAL ERROR: Failed to load stored private key for local switch account prefix ${switchAccount.prefix} from both new and old storage")
loge("CryptoProviderManager", "CRITICAL ERROR: Failed to load stored private key for local switch account prefix $localEffectivePrefix from both new and old storage")
loge("CryptoProviderManager", "Cannot proceed without the stored key as it would create a different account")
return null
}
Expand Down
Loading