Skip to content

Commit 4cc810f

Browse files
committed
fix(services/opencode): fix LocalFiat constructor ambiguity and zero-fee currency crashes
Remove typealias Usd = Fiat and ambiguous constructor B, replacing it with an explicit LocalFiat.fromUsd() factory. Add zero-value short-circuits to LocalFiat.minus/plus operators (matching Fiats existing pattern) so that LocalFiat.Zero can safely be used as a fallback for null fees regardless of currency. Revert three broken workarounds (commits 73d2ea2, eaf014b, 8542fe4) back to simple LocalFiat.Zero/Fiat.Zero defaults. Signed-off-by: Brandon McAnsh <git@bmcreations.dev>
1 parent 0d2a600 commit 4cc810f

9 files changed

Lines changed: 112 additions & 26 deletions

File tree

apps/flipcash/features/currency-creator/src/main/kotlin/com/flipcash/app/currencycreator/CurrencyCreatorFlowScreen.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -158,7 +158,7 @@ fun CurrencyCreatorFlowScreen(
158158
if (token != null) {
159159
val bill = Bill.Cash(
160160
token = token,
161-
amount = LocalFiat(usdf = state.purchaseAmount),
161+
amount = LocalFiat.fromUsd(usdf = state.purchaseAmount),
162162
didReceive = true,
163163
)
164164
outerNavigator.hide()

apps/flipcash/features/currency-creator/src/main/kotlin/com/flipcash/app/currencycreator/internal/CurrencyCreatorViewModel.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -523,7 +523,7 @@ internal class CurrencyCreatorViewModel @Inject constructor(
523523
token = Token.usdf,
524524
rate = Rate.oneToOne,
525525
)
526-
val feeAmount = event.context.feeAmount?.let { LocalFiat(usdf = it) }
526+
val feeAmount = event.context.feeAmount?.let { LocalFiat.fromUsd(usdf = it) }
527527
externalWalletController.setAmount(amount = totalAmount, feeAmount = feeAmount)
528528
externalWalletController.setTokenToPurchase(event.context.token)
529529
}
@@ -542,7 +542,7 @@ internal class CurrencyCreatorViewModel @Inject constructor(
542542
token = Token.usdf,
543543
rate = Rate.oneToOne,
544544
)
545-
val feeAmount = context.feeAmount?.let { LocalFiat(usdf = it) }
545+
val feeAmount = context.feeAmount?.let { LocalFiat.fromUsd(usdf = it) }
546546
transactionController.buy(
547547
owner = owner,
548548
amount = totalAmount,

apps/flipcash/shared/bill-customization/src/main/kotlin/com/flipcash/app/bill/customization/internal/InternalBillPlaygroundController.kt

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -125,9 +125,7 @@ class InternalBillPlaygroundController(
125125
context: PlaygroundContext,
126126
) {
127127
// create amount for the bill
128-
val demoAmount = LocalFiat(
129-
usdf = amount,
130-
)
128+
val demoAmount = LocalFiat.fromUsd(usdf = amount)
131129

132130
// provide bill "data" to render the scan code
133131
val payloadInfo = OpenCodePayload(

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -79,13 +79,13 @@ internal class RealVerifiedFiatCalculator @Inject constructor(
7979
if (token.address == Mint.usdf) {
8080
// this doesn't need a calculated value exchange since we are USDC
8181
val localFiat = if (rate.currency != CurrencyCode.USD) {
82-
LocalFiat(
82+
LocalFiat.fromUsd(
8383
usdf = cappedValue,
8484
rate = rate,
8585
mint = token.address,
8686
)
8787
} else {
88-
LocalFiat(usdf = cappedValue)
88+
LocalFiat.fromUsd(usdf = cappedValue)
8989
}
9090
return VerifiedFiat(localFiat, verifiedState)
9191
}

services/opencode/src/main/kotlin/com/getcode/opencode/internal/network/api/intents/IntentWithdraw.kt

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@ import com.getcode.opencode.model.financial.Fee
1010
import com.getcode.opencode.model.financial.Fiat
1111
import com.getcode.opencode.model.financial.LocalFiat
1212
import com.getcode.opencode.model.financial.minus
13-
import com.getcode.opencode.model.financial.toFiat
1413
import com.getcode.opencode.model.transactions.TransactionMetadata
1514
import com.getcode.opencode.solana.intents.ActionGroup
1615
import com.getcode.opencode.solana.intents.IntentType
@@ -39,7 +38,7 @@ internal class IntentWithdraw(
3938
verifiedState: VerifiedState,
4039
): IntentWithdraw {
4140
// transfer the amount less any fee
42-
val feeAmount = fee?.fiat ?: 0.toFiat(amount.nativeAmount.currencyCode)
41+
val feeAmount = fee?.fiat ?: Fiat.Zero
4342
val transferAmount = amount.underlyingTokenAmount - feeAmount
4443

4544
val actionGroup = buildActionGroup {

services/opencode/src/main/kotlin/com/getcode/opencode/internal/network/services/TransactionService.kt

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,6 @@ import com.getcode.opencode.model.financial.Limits
2222
import com.getcode.opencode.model.financial.LocalFiat
2323
import com.getcode.opencode.model.financial.Token
2424
import com.getcode.opencode.model.financial.minus
25-
import com.getcode.opencode.model.financial.toFiat
2625
import com.getcode.opencode.model.transactions.SwapDirection
2726
import com.getcode.opencode.model.transactions.SwapFundingSource
2827
import com.getcode.opencode.model.transactions.SwapRequest
@@ -191,7 +190,7 @@ internal class TransactionService @Inject constructor(
191190
val swapAuthority =
192191
if (isFreshlyLaunchedStub) owner.authority.keyPair else Ed25519.createKeyPair()
193192

194-
val netAmount = amount - (feeAmount ?: LocalFiat(usdf = 0.toFiat(amount.nativeAmount.currencyCode)))
193+
val netAmount = amount - (feeAmount ?: LocalFiat.Zero)
195194
val request = SwapRequest(
196195
owner = owner,
197196
swapAuthority = swapAuthority,

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

Lines changed: 17 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,6 @@ import kotlinx.parcelize.Parcelize
88
import kotlinx.serialization.Serializable
99
import javax.annotation.concurrent.Immutable
1010

11-
typealias Usd = Fiat
12-
1311
/**
1412
* Represents a monetary value bridge between an on-chain token amount and its localized
1513
* fiat representation.
@@ -68,13 +66,6 @@ data class LocalFiat(
6866
)
6967
)
7068

71-
constructor(usdf: Usd, rate: Rate = Rate.oneToOne, mint: Mint = Mint.usdf) : this(
72-
underlyingTokenAmount = usdf,
73-
nativeAmount = usdf.convertingTo(rate),
74-
mint = mint,
75-
rate = rate
76-
)
77-
7869
companion object {
7970
val Zero = LocalFiat(
8071
underlyingTokenAmount = Fiat(0),
@@ -83,8 +74,20 @@ data class LocalFiat(
8374
rate = Rate.oneToOne
8475
)
8576

86-
val MIN_VALUE = LocalFiat(usdf = Fiat(Int.MIN_VALUE, CurrencyCode.USD),)
87-
val MAX_VALUE = LocalFiat(usdf = Fiat(Int.MAX_VALUE, CurrencyCode.USD),)
77+
val MIN_VALUE = fromUsd(usdf = Fiat(Int.MIN_VALUE, CurrencyCode.USD))
78+
val MAX_VALUE = fromUsd(usdf = Fiat(Int.MAX_VALUE, CurrencyCode.USD))
79+
80+
/**
81+
* Creates a [LocalFiat] from a USD-denominated [Fiat] value, converting to the
82+
* native currency via [rate]. Use this when you have a USD amount and an exchange
83+
* rate but not a pre-computed native amount.
84+
*/
85+
fun fromUsd(usdf: Fiat, rate: Rate = Rate.oneToOne, mint: Mint = Mint.usdf): LocalFiat = LocalFiat(
86+
underlyingTokenAmount = usdf,
87+
nativeAmount = usdf.convertingTo(rate),
88+
mint = mint,
89+
rate = rate
90+
)
8891

8992
fun fromNativeAmount(
9093
nativeAmount: Fiat,
@@ -126,6 +129,7 @@ fun Iterable<LocalFiat>.sum(): LocalFiat {
126129
}
127130

128131
operator fun LocalFiat.minus(other: LocalFiat): LocalFiat {
132+
if (other.underlyingTokenAmount.decimalValue == 0.0 && other.nativeAmount.decimalValue == 0.0) return this
129133
if (rate.currency != other.rate.currency) throw IllegalArgumentException("Currency is mismatched")
130134

131135
return copy(
@@ -135,6 +139,8 @@ operator fun LocalFiat.minus(other: LocalFiat): LocalFiat {
135139
}
136140

137141
operator fun LocalFiat.plus(other: LocalFiat): LocalFiat {
142+
if (other.underlyingTokenAmount.decimalValue == 0.0 && other.nativeAmount.decimalValue == 0.0) return this
143+
if (this.underlyingTokenAmount.decimalValue == 0.0 && this.nativeAmount.decimalValue == 0.0) return other
138144
if (rate.currency != other.rate.currency) throw IllegalArgumentException("Currency is mismatched")
139145

140146
return copy(

services/opencode/src/main/kotlin/com/getcode/opencode/model/transactions/SwapRequest.kt

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@ import com.getcode.opencode.internal.solana.model.SwapId
66
import com.getcode.opencode.model.accounts.AccountCluster
77
import com.getcode.opencode.model.financial.LocalFiat
88
import com.getcode.opencode.model.financial.plus
9-
import com.getcode.opencode.model.financial.toFiat
109
import com.getcode.solana.keys.PublicKey
1110

1211
data class SwapRequest(
@@ -25,7 +24,7 @@ data class SwapRequest(
2524

2625
val totalTransferAmount: LocalFiat
2726
get() {
28-
val fee = feeAmount ?: LocalFiat(usdf = 0.toFiat(swapAmount.nativeAmount.currencyCode))
27+
val fee = feeAmount ?: LocalFiat.Zero
2928
return swapAmount + fee
3029
}
3130
}

services/opencode/src/test/kotlin/com/getcode/opencode/model/financial/LocalFiatTest.kt

Lines changed: 86 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ class LocalFiatTest {
2222
val usdf = Fiat(quarks = 1000L, currencyCode = CurrencyCode.USD)
2323
val rate = Rate(fx = 1.5, currency = CurrencyCode.CAD)
2424

25-
val localFiat = LocalFiat(usdf = usdf, rate = rate)
25+
val localFiat = LocalFiat.fromUsd(usdf, rate)
2626

2727
assertEquals(1000L, localFiat.underlyingTokenAmount.quarks)
2828
assertEquals(CurrencyCode.CAD, localFiat.nativeAmount.currencyCode)
@@ -116,6 +116,45 @@ class LocalFiatTest {
116116
}
117117
}
118118

119+
@Test
120+
fun `plus with zero does not throw on currency mismatch`() {
121+
val a = LocalFiat(
122+
underlyingTokenAmount = Fiat(quarks = 100L),
123+
nativeAmount = Fiat(fiat = 1.5, currencyCode = CurrencyCode.CAD),
124+
rate = Rate(fx = 1.5, currency = CurrencyCode.CAD),
125+
mint = Mint.usdf,
126+
)
127+
128+
val result = a + LocalFiat.Zero
129+
assertEquals(a, result)
130+
}
131+
132+
@Test
133+
fun `plus zero on left returns right`() {
134+
val b = LocalFiat(
135+
underlyingTokenAmount = Fiat(quarks = 100L),
136+
nativeAmount = Fiat(fiat = 1.5, currencyCode = CurrencyCode.CAD),
137+
rate = Rate(fx = 1.5, currency = CurrencyCode.CAD),
138+
mint = Mint.usdf,
139+
)
140+
141+
val result = LocalFiat.Zero + b
142+
assertEquals(b, result)
143+
}
144+
145+
@Test
146+
fun `minus zero does not throw on currency mismatch`() {
147+
val a = LocalFiat(
148+
underlyingTokenAmount = Fiat(quarks = 100L),
149+
nativeAmount = Fiat(fiat = 1.5, currencyCode = CurrencyCode.CAD),
150+
rate = Rate(fx = 1.5, currency = CurrencyCode.CAD),
151+
mint = Mint.usdf,
152+
)
153+
154+
val result = a - LocalFiat.Zero
155+
assertEquals(a, result)
156+
}
157+
119158
@Test
120159
fun `minus throws on currency mismatch`() {
121160
val a = LocalFiat(
@@ -138,6 +177,52 @@ class LocalFiatTest {
138177

139178
// endregion
140179

180+
// region zero-fee regressions (commits 73d2ea2, eaf014b, 8542fe4)
181+
182+
@Test
183+
fun `buy net amount - CAD minus null fee uses Zero without crash`() {
184+
val cadRate = Rate(fx = 1.35, currency = CurrencyCode.CAD)
185+
val amount = LocalFiat(
186+
underlyingTokenAmount = Fiat(fiat = 10.0, currencyCode = CurrencyCode.USD),
187+
nativeAmount = Fiat(fiat = 13.5, currencyCode = CurrencyCode.CAD),
188+
rate = cadRate,
189+
mint = Mint.usdf,
190+
)
191+
val feeAmount: LocalFiat? = null
192+
193+
val netAmount = amount - (feeAmount ?: LocalFiat.Zero)
194+
195+
assertEquals(amount, netAmount)
196+
}
197+
198+
@Test
199+
fun `swap total transfer - CAD plus null fee uses Zero without crash`() {
200+
val cadRate = Rate(fx = 1.35, currency = CurrencyCode.CAD)
201+
val swapAmount = LocalFiat(
202+
underlyingTokenAmount = Fiat(fiat = 10.0, currencyCode = CurrencyCode.USD),
203+
nativeAmount = Fiat(fiat = 13.5, currencyCode = CurrencyCode.CAD),
204+
rate = cadRate,
205+
mint = Mint.usdf,
206+
)
207+
val feeAmount: LocalFiat? = null
208+
209+
val total = swapAmount + (feeAmount ?: LocalFiat.Zero)
210+
211+
assertEquals(swapAmount, total)
212+
}
213+
214+
@Test
215+
fun `withdraw fee - Fiat Zero subtracted from non-USD amount does not crash`() {
216+
val underlyingUsd = Fiat(fiat = 10.0, currencyCode = CurrencyCode.USD)
217+
val feeAmount = Fiat.Zero
218+
219+
val transferAmount = underlyingUsd - feeAmount
220+
221+
assertEquals(underlyingUsd, transferAmount)
222+
}
223+
224+
// endregion
225+
141226
// region sum
142227

143228
@Test

0 commit comments

Comments
 (0)