From 5e5673a2c9c04f2d7b4aa2020f7543f2d4337301 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 14 Mar 2026 06:39:40 +0000 Subject: [PATCH 1/2] Initial plan From 70ec2f8c273f1bca196f664e201609df487f98cc Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 14 Mar 2026 06:46:43 +0000 Subject: [PATCH 2/2] Fix VaultLockedException in resolveSigner() account-match check before biometric unlock Co-authored-by: mocolicious <6373607+mocolicious@users.noreply.github.com> --- .../com/altude/gasstation/GaslessManager.kt | 51 +++++++++++++++++-- 1 file changed, 46 insertions(+), 5 deletions(-) diff --git a/gasstation/src/main/java/com/altude/gasstation/GaslessManager.kt b/gasstation/src/main/java/com/altude/gasstation/GaslessManager.kt index 6bdb0e2..f507ec4 100644 --- a/gasstation/src/main/java/com/altude/gasstation/GaslessManager.kt +++ b/gasstation/src/main/java/com/altude/gasstation/GaslessManager.kt @@ -19,6 +19,7 @@ import com.altude.core.model.AltudeTransactionBuilder import com.altude.core.model.EmptySignature import com.altude.core.model.MessageAddressTableLookup import com.altude.core.model.TransactionSigner +import com.altude.vault.model.VaultLockedException import com.altude.core.model.TransactionVersion import com.altude.core.network.AltudeRpc import com.altude.gasstation.data.CloseAccountOption @@ -58,6 +59,7 @@ object GaslessManager { return@withContext try { val signerToUse = resolveSigner(option.account, signer) ensureBiometricAuth(signerToUse, "transfer") + verifyAccountMatch(signerToUse, option.account) // After biometric unlock the public key is always available - use it as account val ownerKey = signerToUse.publicKey val pubKeyMint = PublicKey(option.token) @@ -176,6 +178,7 @@ object GaslessManager { return@withContext try { val signerToUse = resolveSigner(option.account, signer) ensureBiometricAuth(signerToUse, "create-account") + verifyAccountMatch(signerToUse, option.account) val ownerKey = signerToUse.publicKey val txInstructions = mutableListOf() @@ -231,6 +234,7 @@ object GaslessManager { return@withContext try { val signerToUse = resolveSigner(option.account, signer) ensureBiometricAuth(signerToUse, "close-account") + verifyAccountMatch(signerToUse, option.account) val ownerKey = signerToUse.publicKey ?: option.account.takeIf { it.isNotBlank() }?.let { PublicKey(it) } ?: throw IllegalArgumentException("Account public key required to close accounts") @@ -299,6 +303,7 @@ object GaslessManager { try { val signerToUse = resolveSigner(option.account, signer) ensureBiometricAuth(signerToUse, "swap") + verifyAccountMatch(signerToUse, option.account) val ownerKey = signerToUse.publicKey val decimals = Utility.getTokenDecimals(option.inputMint) val rawAmount = (option.amount * (10.0.pow(decimals))).toLong() @@ -429,6 +434,7 @@ object GaslessManager { try { val signerToUse = resolveSigner(option.account, signer) ensureBiometricAuth(signerToUse, "swap") + verifyAccountMatch(signerToUse, option.account) val decimals = Utility.getTokenDecimals(option.inputMint) val rawAmount = (option.amount * (10.0.pow(decimals))).toLong() val service = SwapConfig.createService(SwapService::class.java) @@ -530,13 +536,43 @@ object GaslessManager { requireNotNull(signer) { "Vault signer required. Call AltudeGasStation.init() before using SDK methods." } - // If account is blank, use the signer's public key (resolved after biometric unlock) - if (account.isNotBlank() && signer.publicKey.toBase58() != account) { - throw IllegalArgumentException("Signer public key ${signer.publicKey.toBase58()} does not match requested account $account") + if (account.isNotBlank()) { + try { + // Validate account match eagerly if the public key is already available + val signerKey = signer.publicKey.toBase58() + if (signerKey != account) { + throw IllegalArgumentException("Signer public key $signerKey does not match requested account $account") + } + } catch (e: VaultLockedException) { + // Vault is locked; public key is not cached yet. + // Account match will be verified in verifyAccountMatch() after + // ensureBiometricAuth() unlocks the vault and caches the public key. + } } return signer } + /** + * Verifies that the signer's public key matches the requested account after biometric unlock. + * Call this immediately after ensureBiometricAuth() whenever an account was specified, + * to complete any deferred account-match check that was skipped in resolveSigner() due + * to the vault being locked at that point. + */ + private fun verifyAccountMatch(signer: TransactionSigner, account: String) { + if (account.isBlank()) return + val signerKey = try { + signer.publicKey.toBase58() + } catch (e: VaultLockedException) { + // If the vault is still locked after ensureBiometricAuth(), propagate the original + // exception so callers receive a clear VaultLockedException rather than a confusing + // IllegalArgumentException with no public key available. + throw e + } + if (signerKey != account) { + throw IllegalArgumentException("Signer public key $signerKey does not match requested account $account") + } + } + private fun resolveSignerForAccount(signers: List, account: String): TransactionSigner { if (signers.isEmpty()) throw IllegalArgumentException("No signer available") if (account.isBlank()) return signers.first() @@ -548,8 +584,13 @@ object GaslessManager { provided?.let { return it } val defaultSigner = resolveSigner() options.firstOrNull { it.account.isNotBlank() }?.let { option -> - if (defaultSigner.publicKey.toBase58() != option.account) { - throw IllegalArgumentException("Default signer does not match batch account ${option.account}") + try { + if (defaultSigner.publicKey.toBase58() != option.account) { + throw IllegalArgumentException("Default signer does not match batch account ${option.account}") + } + } catch (e: VaultLockedException) { + // Vault is locked; account match will be verified after ensureBiometricAuth() + // unlocks the vault and caches the public key. } } return listOf(defaultSigner)