Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ import com.getcode.opencode.model.core.errors.SwapError
import com.getcode.opencode.model.financial.Currency
import com.getcode.opencode.model.financial.CurrencyCode
import com.getcode.opencode.model.financial.Fiat
import com.getcode.opencode.model.financial.HOUSE_SELL_FEE_BPS
import com.getcode.opencode.model.financial.Limits
import com.getcode.opencode.model.financial.LocalFiat
import com.getcode.opencode.model.financial.Rate
Expand All @@ -63,6 +64,7 @@ import com.getcode.opencode.model.financial.max
import com.getcode.opencode.model.financial.min
import com.getcode.opencode.model.financial.minus
import com.getcode.opencode.model.financial.plus
import com.getcode.opencode.model.financial.sellFeeBpsOrHouseRate
import com.getcode.opencode.model.financial.toFiat
import com.getcode.opencode.model.financial.usdf
import com.getcode.opencode.model.transactions.SwapState
Expand Down Expand Up @@ -483,8 +485,8 @@ class SwapViewModel @Inject constructor(
get() {
val purpose = stateFlow.value.purpose as? SwapPurpose.Convert ?: return 0
if (purpose.mint == Mint.usdf) return DEFAULT_CONVERT_FEE_BPS
return stateFlow.value.tokenWithBalance?.token?.launchpadMetadata?.sellFeeBps
?: DEFAULT_CONVERT_FEE_BPS
return stateFlow.value.tokenWithBalance?.token?.launchpadMetadata
.sellFeeBpsOrHouseRate
}

private val feeAmount: Fiat
Expand Down Expand Up @@ -559,12 +561,13 @@ class SwapViewModel @Inject constructor(
/**
* The fee a buy funded by [fundingToken] charges, as bps and whether it rides on top of the
* entered amount. A v2 Get from Dollars pays the flat house rate on top (no pool to skim);
* every other currency has its pool's sell fee grossed up into the debit; v1's reserves buy is
* every other currency has its pool's sell fee grossed up into the debit (falling back to the
* house rate when the pool doesn't declare one, as [convertFeeBps] does); v1's reserves buy is
* free.
*/
private fun buyFeeFor(fundingToken: Token): Pair<Int, Boolean> = when {
fundingToken.address != Mint.usdf ->
(fundingToken.launchpadMetadata?.sellFeeBps ?: 0) to false
fundingToken.launchpadMetadata.sellFeeBpsOrHouseRate to false
stateFlow.value.isGet -> DEFAULT_CONVERT_FEE_BPS to true
else -> 0 to false
}
Expand Down Expand Up @@ -1315,7 +1318,7 @@ class SwapViewModel @Inject constructor(
}
} else {
enteredFiat.grossingUpLaunchpadSellFee(
bps = fundingToken.launchpadMetadata?.sellFeeBps ?: 0,
bps = fundingToken.launchpadMetadata.sellFeeBpsOrHouseRate,
)
},
token = fundingToken,
Expand Down Expand Up @@ -1891,7 +1894,7 @@ class SwapViewModel @Inject constructor(
* House rate for a conversion when the source pool doesn't charge its own sell fee — 1%,
* matching the launchpad default.
*/
private const val DEFAULT_CONVERT_FEE_BPS = 100
private const val DEFAULT_CONVERT_FEE_BPS = HOUSE_SELL_FEE_BPS

val updateStateForEvent: (Event) -> ((State) -> State) = { event ->
when (event) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,3 +58,18 @@ fun Fiat.spendableUnderSellFeeOnTop(bps: Int): Fiat {
val cappedBps = bps.coerceIn(0, MAX_FEE_BPS)
return this / (1.0 + cappedBps / MAX_FEE_BPS.toDouble())
}

/**
* The flat house sell fee, in basis points, charged wherever a pool doesn't declare one of its own.
*/
const val HOUSE_SELL_FEE_BPS = 100

/**
* This pool's declared sell fee, or the house rate when there is no pool to declare one.
*
* Erring high is the safe direction: the fee is grossed up into the debit, so falling back to zero
* prices a debit the funding balance can't actually cover. iOS assumes the house rate on every leg
* for the same reason.
*/
val LaunchpadMetadata?.sellFeeBpsOrHouseRate: Int
get() = this?.sellFeeBps ?: HOUSE_SELL_FEE_BPS
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package com.getcode.opencode.model.financial

import kotlin.test.Test
import kotlin.test.assertEquals
import com.getcode.opencode.tests.generateRandomPublicKeyForTest
import kotlin.test.assertFalse

/**
Expand Down Expand Up @@ -110,4 +111,37 @@ class LaunchpadSellFeeTest {
// Clamped to 10_000 (100%): half the balance covers a fee equal to the entry.
assertEquals("$10.00", balance.spendableUnderSellFeeOnTop(bps = 12_000).formatted())
}

// region House-rate fallback

@Test
fun `a pool that declares its own sell fee uses it`() {
assertEquals(250, pool(sellFeeBps = 250).sellFeeBpsOrHouseRate)
}

@Test
fun `a token with no pool falls back to the house rate`() {
// Erring high is the safe direction — the fee is grossed up into the debit, so a zero
// fallback prices a debit the funding balance can't actually cover. iOS assumes the house
// rate on every leg for the same reason.
assertEquals(HOUSE_SELL_FEE_BPS, null.sellFeeBpsOrHouseRate)
}

private fun pool(sellFeeBps: Int): LaunchpadMetadata {
val key = generateRandomPublicKeyForTest()
return LaunchpadMetadata(
currencyConfig = key,
liquidityPool = key,
seed = key,
authority = key,
mintVault = key,
coreMintVault = key,
currentCirculatingSupplyQuarks = 1_000_000L,
sellFeeBps = sellFeeBps,
price = Fiat(fiat = 1.0),
marketCap = Fiat(fiat = 1000.0),
)
}

// endregion
}
Loading