diff --git a/apps/flipcash/shared/tokens/src/main/kotlin/com/flipcash/app/tokens/UsdcDepositSweep.kt b/apps/flipcash/shared/tokens/src/main/kotlin/com/flipcash/app/tokens/UsdcDepositSweep.kt index 13a2fa6d4..0e63e2ddd 100644 --- a/apps/flipcash/shared/tokens/src/main/kotlin/com/flipcash/app/tokens/UsdcDepositSweep.kt +++ b/apps/flipcash/shared/tokens/src/main/kotlin/com/flipcash/app/tokens/UsdcDepositSweep.kt @@ -5,6 +5,7 @@ import com.getcode.opencode.controllers.TransactionOperations import com.getcode.opencode.model.accounts.AccountCluster import com.getcode.opencode.model.accounts.AccountFilter import com.getcode.opencode.model.accounts.AccountType +import com.getcode.opencode.model.financial.Fiat import com.getcode.solana.keys.Mint import com.getcode.solana.keys.base58 import com.getcode.utils.TraceType @@ -23,21 +24,28 @@ class UsdcDepositSweep( private val transactionOperations: TransactionOperations, private val accountController: AccountController, private val tokenCoordinator: TokenCoordinator, + private val balancePoller: BalancePoller, private val maxRetries: Int = MAX_RETRIES, private val initialDelay: Duration = INITIAL_DELAY, private val backoffFactor: Double = BACKOFF_FACTOR, + private val pollInterval: Duration = POLL_INTERVAL, + private val pollMaxAttempts: Int = POLL_MAX_ATTEMPTS, ) { @Inject constructor( transactionOperations: TransactionOperations, accountController: AccountController, tokenCoordinator: TokenCoordinator, + balancePoller: BalancePoller, ) : this( transactionOperations = transactionOperations, accountController = accountController, tokenCoordinator = tokenCoordinator, + balancePoller = balancePoller, maxRetries = MAX_RETRIES, initialDelay = INITIAL_DELAY, - backoffFactor = BACKOFF_FACTOR + backoffFactor = BACKOFF_FACTOR, + pollInterval = POLL_INTERVAL, + pollMaxAttempts = POLL_MAX_ATTEMPTS, ) private val scope = CoroutineScope(Dispatchers.IO + SupervisorJob()) @@ -75,7 +83,19 @@ class UsdcDepositSweep( amount = amount, ).onSuccess { trace(tag = TAG, message = "USDC→USDF sweep completed") - tokenCoordinator.update() + balancePoller.awaitBalanceChange( + mint = Mint.usdf, + baseline = Fiat.Zero, + predicate = { _, current -> current.hasDisplayableValue }, + interval = pollInterval, + maxAttempts = pollMaxAttempts, + ).onFailure { error -> + trace( + tag = TAG, + message = "USDF balance poll timed out: ${error.message}", + type = TraceType.Log, + ) + } }.onFailure { error -> trace(tag = TAG, message = "USDC→USDF sweep failed: ${error.message}", error = error) } @@ -92,5 +112,7 @@ class UsdcDepositSweep( private const val MAX_RETRIES = 5 private val INITIAL_DELAY = 5.seconds private const val BACKOFF_FACTOR = 2.0 + internal val POLL_INTERVAL = 5.seconds + internal const val POLL_MAX_ATTEMPTS = 12 } } diff --git a/apps/flipcash/shared/tokens/src/test/kotlin/com/flipcash/app/tokens/UsdcDepositSweepTest.kt b/apps/flipcash/shared/tokens/src/test/kotlin/com/flipcash/app/tokens/UsdcDepositSweepTest.kt index d5179e265..0e4b6fcaa 100644 --- a/apps/flipcash/shared/tokens/src/test/kotlin/com/flipcash/app/tokens/UsdcDepositSweepTest.kt +++ b/apps/flipcash/shared/tokens/src/test/kotlin/com/flipcash/app/tokens/UsdcDepositSweepTest.kt @@ -5,6 +5,8 @@ import com.getcode.opencode.controllers.TransactionOperations import com.getcode.opencode.model.accounts.AccountCluster import com.getcode.opencode.model.accounts.AccountInfo import com.getcode.opencode.model.accounts.AccountType +import com.getcode.opencode.model.financial.Fiat +import com.getcode.solana.keys.Mint import com.getcode.solana.keys.PublicKey import io.mockk.coEvery import io.mockk.coVerify @@ -24,6 +26,7 @@ class UsdcDepositSweepTest { private val transactionOperations: TransactionOperations = mockk(relaxed = true) private val accountController: AccountController = mockk(relaxed = true) private val tokenCoordinator: TokenCoordinator = mockk(relaxed = true) + private val balancePoller: BalancePoller = mockk(relaxed = true) private val owner: AccountCluster = mockk(relaxed = true) @@ -31,13 +34,20 @@ class UsdcDepositSweepTest { @Before fun setUp() { + coEvery { + balancePoller.awaitBalanceChange(any(), any(), any(), any(), any()) + } returns Result.success(Fiat.Zero) + sweep = UsdcDepositSweep( transactionOperations = transactionOperations, accountController = accountController, tokenCoordinator = tokenCoordinator, + balancePoller = balancePoller, maxRetries = 3, initialDelay = 10.milliseconds, backoffFactor = 1.0, + pollInterval = 10.milliseconds, + pollMaxAttempts = 2, ) } @@ -144,7 +154,7 @@ class UsdcDepositSweepTest { } @Test - fun `calls tokenCoordinator update on successful swap`() = runTest { + fun `polls for USDF balance on successful swap`() = runTest { stubUsdcAccount(balance = 1_000_000L) coEvery { transactionOperations.swapUsdc(any(), any()) } returns Result.success(Unit) @@ -153,12 +163,18 @@ class UsdcDepositSweepTest { Thread.sleep(200) coVerify { - tokenCoordinator.update() + balancePoller.awaitBalanceChange( + mint = Mint.usdf, + baseline = Fiat.Zero, + predicate = any(), + interval = 10.milliseconds, + maxAttempts = 2, + ) } } @Test - fun `does not call tokenCoordinator update on failed swap`() = runTest { + fun `does not poll for USDF balance when swap fails`() = runTest { stubUsdcAccount(balance = 1_000_000L) coEvery { transactionOperations.swapUsdc(any(), any()) @@ -169,7 +185,24 @@ class UsdcDepositSweepTest { Thread.sleep(200) coVerify(exactly = 0) { - tokenCoordinator.update() + balancePoller.awaitBalanceChange(any(), any(), any(), any(), any()) + } + } + + @Test + fun `completes gracefully when USDF balance poll times out`() = runTest { + stubUsdcAccount(balance = 1_000_000L) + coEvery { transactionOperations.swapUsdc(any(), any()) } returns Result.success(Unit) + coEvery { + balancePoller.awaitBalanceChange(any(), any(), any(), any(), any()) + } returns Result.failure(BalancePollError.Timeout(Mint.usdf, 2)) + + sweep.execute(owner) + advanceUntilIdle() + Thread.sleep(200) + + coVerify { + balancePoller.awaitBalanceChange(any(), any(), any(), any(), any()) } }