From 955ffd8a1823fc6bf004159bef527ba333e05c89 Mon Sep 17 00:00:00 2001 From: Brandon McAnsh Date: Fri, 24 Jul 2026 19:30:28 -0400 Subject: [PATCH] fix(resolver): drop in-memory resolution cache MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The process-lifetime cache in ResolverController could return a stale on-chain address for a phone/user id. That wrong PublicKey flows into directTransfer's timelock derivation, producing a destination vault the server rejects with "payments to external destinations must be withdrawals" — breaking in-chat contact DM sends. Always resolve fresh against the repository. The cache was only ever a round-trip optimization with no correctness benefit; removing it restores the reliable pre-refactor behavior. --- .../services/controllers/ResolverController.kt | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/services/flipcash/src/main/kotlin/com/flipcash/services/controllers/ResolverController.kt b/services/flipcash/src/main/kotlin/com/flipcash/services/controllers/ResolverController.kt index 702ce7171..c4c9f5462 100644 --- a/services/flipcash/src/main/kotlin/com/flipcash/services/controllers/ResolverController.kt +++ b/services/flipcash/src/main/kotlin/com/flipcash/services/controllers/ResolverController.kt @@ -6,7 +6,6 @@ import com.flipcash.services.repository.ResolverRepository import com.flipcash.services.user.UserManager import com.getcode.opencode.model.core.ID import com.getcode.solana.keys.PublicKey -import java.util.concurrent.ConcurrentHashMap import javax.inject.Inject import javax.inject.Singleton @@ -15,13 +14,6 @@ class ResolverController @Inject constructor( private val repository: ResolverRepository, private val userManager: UserManager, ) { - // Memoizes successful resolutions for the process lifetime. An identity's on-chain address is - // effectively immutable, and callers commonly resolve the same identity twice in quick - // succession (e.g. a chat pre-resolves the recipient to gate the send button, then the payment - // delegate resolves it again at send time) — the cache turns the second into a no-op. Failures - // are never cached, so an unresolvable identity is retried. - private val cache = ConcurrentHashMap() - /** Resolves a phone number to its on-chain address. */ suspend fun resolve(phone: ContactMethod.Phone): Result = resolve(ResolveIdentifier.Phone(phone)) @@ -31,10 +23,8 @@ class ResolverController @Inject constructor( resolve(ResolveIdentifier.UserId(userId)) private suspend fun resolve(identifier: ResolveIdentifier): Result { - cache[identifier]?.let { return Result.success(it) } val owner = userManager.accountCluster?.authority?.keyPair ?: return Result.failure(Throwable("No account cluster in UserManager")) return repository.resolve(owner, identifier) - .onSuccess { cache[identifier] = it } } }