Skip to content

Commit defccea

Browse files
committed
fix(tokens): poll for USDF balance after USDC->USDF sweep
After a successful sweep, the on-chain swap is asynchronous so the balance is not immediately available. Use BalancePoller to actively poll Mint.usdf until the balance becomes displayable, replacing the passive 20s poll cycle with a 5s x 12 active window.
1 parent 4a0dac0 commit defccea

2 files changed

Lines changed: 61 additions & 6 deletions

File tree

apps/flipcash/shared/tokens/src/main/kotlin/com/flipcash/app/tokens/UsdcDepositSweep.kt

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import com.getcode.opencode.controllers.TransactionOperations
55
import com.getcode.opencode.model.accounts.AccountCluster
66
import com.getcode.opencode.model.accounts.AccountFilter
77
import com.getcode.opencode.model.accounts.AccountType
8+
import com.getcode.opencode.model.financial.Fiat
89
import com.getcode.solana.keys.Mint
910
import com.getcode.solana.keys.base58
1011
import com.getcode.utils.TraceType
@@ -23,21 +24,28 @@ class UsdcDepositSweep(
2324
private val transactionOperations: TransactionOperations,
2425
private val accountController: AccountController,
2526
private val tokenCoordinator: TokenCoordinator,
27+
private val balancePoller: BalancePoller,
2628
private val maxRetries: Int = MAX_RETRIES,
2729
private val initialDelay: Duration = INITIAL_DELAY,
2830
private val backoffFactor: Double = BACKOFF_FACTOR,
31+
private val pollInterval: Duration = POLL_INTERVAL,
32+
private val pollMaxAttempts: Int = POLL_MAX_ATTEMPTS,
2933
) {
3034
@Inject constructor(
3135
transactionOperations: TransactionOperations,
3236
accountController: AccountController,
3337
tokenCoordinator: TokenCoordinator,
38+
balancePoller: BalancePoller,
3439
) : this(
3540
transactionOperations = transactionOperations,
3641
accountController = accountController,
3742
tokenCoordinator = tokenCoordinator,
43+
balancePoller = balancePoller,
3844
maxRetries = MAX_RETRIES,
3945
initialDelay = INITIAL_DELAY,
40-
backoffFactor = BACKOFF_FACTOR
46+
backoffFactor = BACKOFF_FACTOR,
47+
pollInterval = POLL_INTERVAL,
48+
pollMaxAttempts = POLL_MAX_ATTEMPTS,
4149
)
4250

4351
private val scope = CoroutineScope(Dispatchers.IO + SupervisorJob())
@@ -75,7 +83,19 @@ class UsdcDepositSweep(
7583
amount = amount,
7684
).onSuccess {
7785
trace(tag = TAG, message = "USDC→USDF sweep completed")
78-
tokenCoordinator.update()
86+
balancePoller.awaitBalanceChange(
87+
mint = Mint.usdf,
88+
baseline = Fiat.Zero,
89+
predicate = { _, current -> current.hasDisplayableValue },
90+
interval = pollInterval,
91+
maxAttempts = pollMaxAttempts,
92+
).onFailure { error ->
93+
trace(
94+
tag = TAG,
95+
message = "USDF balance poll timed out: ${error.message}",
96+
type = TraceType.Log,
97+
)
98+
}
7999
}.onFailure { error ->
80100
trace(tag = TAG, message = "USDC→USDF sweep failed: ${error.message}", error = error)
81101
}
@@ -92,5 +112,7 @@ class UsdcDepositSweep(
92112
private const val MAX_RETRIES = 5
93113
private val INITIAL_DELAY = 5.seconds
94114
private const val BACKOFF_FACTOR = 2.0
115+
internal val POLL_INTERVAL = 5.seconds
116+
internal const val POLL_MAX_ATTEMPTS = 12
95117
}
96118
}

apps/flipcash/shared/tokens/src/test/kotlin/com/flipcash/app/tokens/UsdcDepositSweepTest.kt

Lines changed: 37 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ import com.getcode.opencode.controllers.TransactionOperations
55
import com.getcode.opencode.model.accounts.AccountCluster
66
import com.getcode.opencode.model.accounts.AccountInfo
77
import com.getcode.opencode.model.accounts.AccountType
8+
import com.getcode.opencode.model.financial.Fiat
9+
import com.getcode.solana.keys.Mint
810
import com.getcode.solana.keys.PublicKey
911
import io.mockk.coEvery
1012
import io.mockk.coVerify
@@ -24,20 +26,28 @@ class UsdcDepositSweepTest {
2426
private val transactionOperations: TransactionOperations = mockk(relaxed = true)
2527
private val accountController: AccountController = mockk(relaxed = true)
2628
private val tokenCoordinator: TokenCoordinator = mockk(relaxed = true)
29+
private val balancePoller: BalancePoller = mockk(relaxed = true)
2730

2831
private val owner: AccountCluster = mockk(relaxed = true)
2932

3033
private lateinit var sweep: UsdcDepositSweep
3134

3235
@Before
3336
fun setUp() {
37+
coEvery {
38+
balancePoller.awaitBalanceChange(any(), any(), any(), any(), any())
39+
} returns Result.success(Fiat.Zero)
40+
3441
sweep = UsdcDepositSweep(
3542
transactionOperations = transactionOperations,
3643
accountController = accountController,
3744
tokenCoordinator = tokenCoordinator,
45+
balancePoller = balancePoller,
3846
maxRetries = 3,
3947
initialDelay = 10.milliseconds,
4048
backoffFactor = 1.0,
49+
pollInterval = 10.milliseconds,
50+
pollMaxAttempts = 2,
4151
)
4252
}
4353

@@ -144,7 +154,7 @@ class UsdcDepositSweepTest {
144154
}
145155

146156
@Test
147-
fun `calls tokenCoordinator update on successful swap`() = runTest {
157+
fun `polls for USDF balance on successful swap`() = runTest {
148158
stubUsdcAccount(balance = 1_000_000L)
149159
coEvery { transactionOperations.swapUsdc(any(), any()) } returns Result.success(Unit)
150160

@@ -153,12 +163,18 @@ class UsdcDepositSweepTest {
153163
Thread.sleep(200)
154164

155165
coVerify {
156-
tokenCoordinator.update()
166+
balancePoller.awaitBalanceChange(
167+
mint = Mint.usdf,
168+
baseline = Fiat.Zero,
169+
predicate = any(),
170+
interval = 10.milliseconds,
171+
maxAttempts = 2,
172+
)
157173
}
158174
}
159175

160176
@Test
161-
fun `does not call tokenCoordinator update on failed swap`() = runTest {
177+
fun `does not poll for USDF balance when swap fails`() = runTest {
162178
stubUsdcAccount(balance = 1_000_000L)
163179
coEvery {
164180
transactionOperations.swapUsdc(any(), any())
@@ -169,7 +185,24 @@ class UsdcDepositSweepTest {
169185
Thread.sleep(200)
170186

171187
coVerify(exactly = 0) {
172-
tokenCoordinator.update()
188+
balancePoller.awaitBalanceChange(any(), any(), any(), any(), any())
189+
}
190+
}
191+
192+
@Test
193+
fun `completes gracefully when USDF balance poll times out`() = runTest {
194+
stubUsdcAccount(balance = 1_000_000L)
195+
coEvery { transactionOperations.swapUsdc(any(), any()) } returns Result.success(Unit)
196+
coEvery {
197+
balancePoller.awaitBalanceChange(any(), any(), any(), any(), any())
198+
} returns Result.failure(BalancePollError.Timeout(Mint.usdf, 2))
199+
200+
sweep.execute(owner)
201+
advanceUntilIdle()
202+
Thread.sleep(200)
203+
204+
coVerify {
205+
balancePoller.awaitBalanceChange(any(), any(), any(), any(), any())
173206
}
174207
}
175208

0 commit comments

Comments
 (0)