Skip to content

Commit 237829a

Browse files
committed
chore(flipcash/pools): handle distributions based on raw quarks
Signed-off-by: Brandon McAnsh <git@bmcreations.dev>
1 parent 2eed9d3 commit 237829a

10 files changed

Lines changed: 74 additions & 39 deletions

File tree

apps/flipcash/features/pools/src/main/kotlin/com/flipcash/app/pools/internal/betting/PoolBettingViewModel.kt

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -446,20 +446,20 @@ internal class PoolBettingViewModel @Inject constructor(
446446
.mapNotNull { resolution ->
447447
val rendezvous = stateFlow.value.rendezvous
448448
if (rendezvous == null) return@mapNotNull null
449-
449+
val pool = stateFlow.value.metadata
450450
PaymentRequest.ResolvePool(
451-
pool = stateFlow.value.metadata,
451+
pool = pool,
452452
bets = stateFlow.value.bets,
453453
rendezvous = rendezvous,
454454
resolution = resolution,
455455
) {
456456
poolsCoordinator.closePool(
457-
pool = stateFlow.value.metadata,
457+
pool = pool,
458458
rendezvous = rendezvous,
459459
).fold(
460460
onSuccess = {
461461
poolsCoordinator.resolvePool(
462-
poolId = stateFlow.value.metadata.id,
462+
pool = pool,
463463
rendezvous = rendezvous,
464464
resolution = resolution,
465465
)

apps/flipcash/features/pools/src/main/kotlin/com/flipcash/app/pools/internal/list/components/PoolSummaryRow.kt

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,16 @@ import androidx.compose.foundation.layout.Arrangement
66
import androidx.compose.foundation.layout.Box
77
import androidx.compose.foundation.layout.Column
88
import androidx.compose.foundation.layout.Row
9+
import androidx.compose.foundation.layout.fillMaxWidth
910
import androidx.compose.foundation.layout.padding
1011
import androidx.compose.material.Icon
1112
import androidx.compose.material.Text
1213
import androidx.compose.runtime.Composable
14+
import androidx.compose.runtime.CompositionLocalProvider
1315
import androidx.compose.runtime.remember
1416
import androidx.compose.ui.Alignment
1517
import androidx.compose.ui.Modifier
18+
import androidx.compose.ui.platform.LocalContext
1619
import androidx.compose.ui.res.painterResource
1720
import androidx.compose.ui.res.stringResource
1821
import androidx.compose.ui.tooling.preview.Preview
@@ -22,7 +25,13 @@ import com.flipcash.app.core.pools.PoolResolution
2225
import com.flipcash.app.core.pools.PoolUserSummary
2326
import com.flipcash.app.theme.FlipcashDesignSystem
2427
import com.flipcash.features.pools.R
28+
import com.getcode.opencode.compose.ExchangeStub
29+
import com.getcode.opencode.compose.LocalExchange
30+
import com.getcode.opencode.model.financial.CurrencyCode
2531
import com.getcode.opencode.model.financial.Fiat
32+
import com.getcode.opencode.model.financial.Rate
33+
import com.getcode.opencode.model.financial.div
34+
import com.getcode.opencode.model.financial.times
2635
import com.getcode.opencode.model.financial.toFiat
2736
import com.getcode.solana.keys.PublicKey
2837
import com.getcode.theme.CodeTheme

apps/flipcash/shared/payments/src/main/kotlin/com/flipcash/app/payments/internal/InternalPoolBidDelegate.kt

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,8 +36,10 @@ internal class InternalPoolBidDelegate @Inject constructor(
3636
return
3737
}
3838

39+
exchange.fetchRatesIfNeeded(force = true)
40+
3941
val localizedAmount = LocalFiat(
40-
usdc = amount.convertingTo(exchange.rateForUsd()),
42+
usdc = amount.convertingTo(exchange.rateToUsd(amount.currencyCode)!!),
4143
converted = amount,
4244
)
4345

apps/flipcash/shared/payments/src/main/kotlin/com/flipcash/app/payments/internal/InternalPoolResolveDelegate.kt

Lines changed: 34 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import com.flipcash.services.models.NetworkPoolResolution
1313
import com.flipcash.services.user.UserManager
1414
import com.getcode.ed25519.Ed25519
1515
import com.getcode.opencode.controllers.TransactionController
16+
import com.getcode.opencode.exchange.Exchange
1617
import com.getcode.opencode.model.core.ID
1718
import com.getcode.opencode.model.core.RandomId
1819
import com.getcode.opencode.model.financial.Distribution
@@ -23,6 +24,7 @@ import javax.inject.Inject
2324
class InternalPoolResolveDelegate @Inject constructor(
2425
private val transactionController: TransactionController,
2526
private val userManager: UserManager,
27+
private val exchange: Exchange,
2628
) : PoolResolveDelegate {
2729
override suspend fun resolvePool(
2830
pool: Pool,
@@ -46,14 +48,20 @@ class InternalPoolResolveDelegate @Inject constructor(
4648
bets = bets,
4749
)
4850

49-
val distributions = poolWithBets.buildDistributionList(resolution)
51+
exchange.fetchRatesIfNeeded()
52+
val distributions = runCatching { poolWithBets.buildDistributionList(resolution) }
53+
54+
distributions.exceptionOrNull()?.let {
55+
onError(it)
56+
return
57+
}
5058

5159
val poolAccount = userManager.poolAccountAt(pool.derivationIndex)
5260

5361
transactionController.distributeFunds(
5462
owner = owner,
5563
from = poolAccount.cluster,
56-
distributions = distributions
64+
distributions = distributions.getOrNull().orEmpty()
5765
).map {
5866
it.id.bytes
5967
}.onSuccess {
@@ -65,23 +73,42 @@ class InternalPoolResolveDelegate @Inject constructor(
6573

6674
private fun PoolWithBets.buildDistributionList(resolution: PoolResolution.DecisionMade): List<Distribution> {
6775
val paidBets = bets.filter { it.hasPaidForBet }
68-
6976
// 1. if the decision was to refund, then all paid bets are returned
7077
if (resolution is PoolResolution.Refund) {
78+
val rate = exchange.rateToUsd(pool.buyIn.currencyCode)
79+
?: throw IllegalArgumentException("No rate found for ${pool.buyIn.currencyCode}")
80+
val usdc = pool.buyIn.convertingTo(rate)
7181
return paidBets.map {
7282
Distribution(
7383
destination = it.payoutDestination,
74-
amount = pool.buyIn,
84+
amount = usdc,
7585
)
7686
}
7787
}
7888

7989
val matchingBets = paidBets.filter { it.selectedOutcome.matchesResolution(resolution) }
90+
val rate = exchange.rateToUsd(pool.totalPoolAmount.currencyCode)
91+
?: throw IllegalArgumentException("No rate found for ${pool.totalPoolAmount.currencyCode}")
92+
93+
val totalPoolInUsdc = pool.totalPoolAmount.convertingTo(rate)
94+
val winnerCount = matchingBets.count()
95+
96+
// Calculate base amount per winner and remainder
97+
val baseAmountPerWinner = totalPoolInUsdc.quarks / winnerCount
98+
val remainderQuarks = totalPoolInUsdc.quarks % winnerCount
99+
80100
// 2. otherwise, pay out all winning (matching bets)
81-
return matchingBets.map {
101+
// unequal remainder is added to the 'remainderQuarks' winners
102+
return matchingBets.mapIndexed { index, bet ->
103+
val amount = if (index < remainderQuarks) {
104+
// Add 1 extra quark to the first 'remainderQuarks' winners
105+
baseAmountPerWinner + 1
106+
} else {
107+
baseAmountPerWinner
108+
}
82109
Distribution(
83-
destination = it.payoutDestination,
84-
amount = winningAmountForResolution(resolution)
110+
destination = bet.payoutDestination,
111+
amount = Fiat(amount)
85112
)
86113
}
87114
}

apps/flipcash/shared/pools/src/main/kotlin/com/flipcash/app/pools/PoolsCoordinator.kt

Lines changed: 18 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -159,35 +159,32 @@ class PoolsCoordinator @Inject constructor(
159159
pool: Pool,
160160
rendezvous: KeyPair,
161161
): Result<Instant> {
162-
val metadata = domainToNetworkMapper.map(pool)
163-
return controller.closePool(metadata, rendezvous)
164-
.onSuccess { closedAt ->
162+
// ensure we are working with an up-to-date instance of a pool
163+
return controller.getPool(pool.id)
164+
.fold(
165+
onSuccess = {
166+
controller.closePool(it.metadata, rendezvous)
167+
},
168+
onFailure = {
169+
Result.failure(it)
170+
}
171+
).onSuccess { closedAt ->
165172
dataSource.closePool(pool.id, closedAt)
166173
}
167174
}
168175

169176
suspend fun resolvePool(
170-
poolId: ID,
177+
pool: Pool,
171178
resolution: PoolResolution.DecisionMade,
172179
rendezvous: KeyPair
173180
): Result<Unit> {
174-
return controller.getPool(poolId)
175-
.fold(
176-
onSuccess = {
177-
controller.resolvePool(
178-
pool = it.metadata,
179-
rendezvous = rendezvous,
180-
resolution = PoolResolutionConverter.toPoolResolution(resolution as PoolResolution),
181-
)
182-
},
183-
onFailure = { Result.failure(it) }
184-
).fold(
185-
onSuccess = {
186-
dataSource.resolvePool(poolId, resolution)
187-
Result.success(Unit)
188-
},
189-
onFailure = { Result.failure(it) }
190-
)
181+
return controller.resolvePool(
182+
pool = domainToNetworkMapper.map(pool),
183+
rendezvous = rendezvous,
184+
resolution = PoolResolutionConverter.toPoolResolution(resolution as PoolResolution),
185+
).onSuccess {
186+
dataSource.resolvePool(pool.id, resolution)
187+
}
191188
}
192189

193190
suspend fun placeBet(

services/opencode-compose/src/main/kotlin/com/getcode/opencode/compose/Exchange.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ private class ExchangeNull(override val staleThreshold: Duration = 1.days) : Exc
6565
return null
6666
}
6767

68-
override suspend fun fetchRatesIfNeeded() = Unit
68+
override suspend fun fetchRatesIfNeeded(force: Boolean) = Unit
6969

7070
override fun rateFor(currencyCode: CurrencyCode): Rate? {
7171
return null

services/opencode-compose/src/main/kotlin/com/getcode/opencode/compose/ExchangeStub.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,7 @@ class ExchangeStub(
7676
).let { if (it == 0) null else it }
7777
}
7878

79-
override suspend fun fetchRatesIfNeeded() = Unit
79+
override suspend fun fetchRatesIfNeeded(force: Boolean) = Unit
8080

8181
override fun rateFor(currencyCode: CurrencyCode): Rate? {
8282
return providedRates[currencyCode]

services/opencode/src/main/kotlin/com/getcode/opencode/exchange/Exchange.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ interface Exchange {
2525
fun getFlagByCurrency(currencyCode: String?): Int?
2626
fun getFlag(countryCode: String): Int?
2727

28-
suspend fun fetchRatesIfNeeded()
28+
suspend fun fetchRatesIfNeeded(force: Boolean = false)
2929

3030
fun rateFor(currencyCode: CurrencyCode): Rate?
3131

services/opencode/src/main/kotlin/com/getcode/opencode/internal/exchange/OpenCodeExchange.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -132,8 +132,8 @@ internal class OpenCodeExchange @Inject constructor(
132132
}
133133
}
134134

135-
override suspend fun fetchRatesIfNeeded() {
136-
if (isStale) {
135+
override suspend fun fetchRatesIfNeeded(force: Boolean) {
136+
if (isStale || force) {
137137
retryable(
138138
call = {
139139
val now = Clock.System.now()

services/opencode/src/main/kotlin/com/getcode/opencode/model/financial/Distribution.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import com.getcode.solana.keys.PublicKey
66
* Represents a distribution of a specific fiat amount to a destination public key.
77
*
88
* @property destination The public key of the recipient of the distribution. This must ALWAYS be a primary account.
9-
* @property amount The amount of funds to distribute to the destination
9+
* @property amount The amount of funds (in quarks) to distribute to the destination
1010
*/
1111
data class Distribution(
1212
val destination: PublicKey,

0 commit comments

Comments
 (0)