Skip to content

Commit f8e5c7c

Browse files
committed
chore: add local fiat exchange tests
Signed-off-by: Brandon McAnsh <[email protected]>
1 parent 3833730 commit f8e5c7c

File tree

6 files changed

+226
-10
lines changed

6 files changed

+226
-10
lines changed

services/opencode/build.gradle.kts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,4 +113,6 @@ dependencies {
113113
implementation(Libs.bugsnag)
114114

115115
implementation(Libs.eventBus)
116+
117+
testImplementation(kotlin("test"))
116118
}

services/opencode/src/main/kotlin/com/getcode/opencode/internal/extensions/CurrencyCode.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
package com.getcode.opencode.internal.extensions
22

3-
import android.icu.util.Currency
43
import com.getcode.opencode.model.financial.CurrencyCode
54
import com.getcode.utils.trace
5+
import java.util.Currency
66
import java.util.Locale
77

88
internal fun CurrencyCode.getClosestLocale(): Locale {
@@ -19,7 +19,7 @@ internal fun CurrencyCode.getClosestLocale(): Locale {
1919
if (locale.country.equals(getRegion()?.name, ignoreCase = true)) {
2020
try {
2121
val localeCurrency = Currency.getInstance(locale)
22-
if (localeCurrency.currencyCode.lowercase() == currency.currencyCode.lowercase()) {
22+
if (localeCurrency.currencyCode.equals(currency.currencyCode, ignoreCase = true)) {
2323
matchedLocale = locale
2424
break
2525
}

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

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,7 @@ data class Fiat(
7878
false
7979
}
8080

81-
val formatter = android.icu.text.DecimalFormat.getInstance(ULocale.US).apply {
81+
val formatter = DecimalFormat.getInstance(Locale.US).apply {
8282
val decimalDigits =
8383
java.util.Currency.getInstance(currencyCode.name).defaultFractionDigits
8484
val preferredDigits = when (formatting) {
@@ -91,7 +91,7 @@ data class Fiat(
9191
minimumFractionDigits = preferredDigits
9292
maximumFractionDigits = preferredDigits
9393
roundingMode = ROUNDING_MODE
94-
(this as android.icu.text.DecimalFormat).decimalFormatSymbols =
94+
(this as DecimalFormat).decimalFormatSymbols =
9595
decimalFormatSymbols.apply {
9696
currencySymbol = ""
9797
}
@@ -149,7 +149,7 @@ data class Fiat(
149149
mintDecimals = 6,
150150
).getOrNull() ?: BigDecimal.ZERO)
151151

152-
val formatter = android.icu.text.DecimalFormat.getInstance(ULocale.US).apply {
152+
val formatter = DecimalFormat.getInstance(Locale.US).apply {
153153
if (fractionDigits != null) {
154154
maximumFractionDigits = fractionDigits
155155
minimumFractionDigits = fractionDigits
@@ -161,7 +161,7 @@ data class Fiat(
161161
}
162162

163163
companion object {
164-
private val ROUNDING_MODE = RoundingMode.HALF_UP.ordinal
164+
private val ROUNDING_MODE = RoundingMode.HALF_UP
165165
const val MULTIPLIER: Double = 1_000_000.0
166166

167167
val Zero = Fiat(0, CurrencyCode.USD)

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

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ data class LocalFiat(
6464
fun valueExchangeIn(
6565
amount: Fiat,
6666
token: Token,
67-
balance: Fiat = Fiat.Zero,
67+
balance: Fiat? = null,
6868
rate: Rate,
6969
debug: Boolean = BuildConfig.DEBUG,
7070
): LocalFiat {
@@ -84,7 +84,7 @@ data class LocalFiat(
8484

8585
val circulatingSupply = token.launchpadMetadata?.currentCirculatingSupplyQuarks ?: 0
8686

87-
val cappedValue = min(balance, usdValue)
87+
val cappedValue = balance?.let { min(it, usdValue) } ?: usdValue
8888

8989
// determine quarks to exchange for the desired amount
9090
val quarks = Estimator.valueExchangeAsQuarks(
@@ -105,7 +105,7 @@ data class LocalFiat(
105105
if (debug) {
106106
println("############## EXCHANGE REPORT ###################")
107107
println("requested quarks: ${usdValue.quarks * 1_000_000}")
108-
println("balance quarks: ${balance.quarks * 1_000_000}")
108+
println("balance quarks: ${balance?.quarks?.times(1_000_000)}")
109109
println("capped quarks: ${cappedValue.quarks * 1_000_000}")
110110
println("circulating supply: $circulatingSupply")
111111
println("calculated quarks: $quarks")

services/opencode/src/main/kotlin/com/getcode/opencode/utils/Double.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,6 @@ fun Double.toByteArray(): ByteArray {
2222
* @param mode The rounding mode to apply (e.g., RoundingMode.HALF_UP).
2323
* @return The rounded Double value.
2424
*/
25-
fun Double.roundTo(decimals: Int, mode: Int = RoundingMode.HALF_UP.ordinal): Double {
25+
fun Double.roundTo(decimals: Int, mode: RoundingMode = RoundingMode.HALF_UP): Double {
2626
return BigDecimal.valueOf(this).setScale(decimals, mode).toDouble()
2727
}
Lines changed: 214 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,214 @@
1+
package com.getcode.opencode.model.financial
2+
3+
import com.getcode.opencode.internal.solana.vmAuthority
4+
import com.getcode.opencode.solana.keys.TimelockDerivedAccounts
5+
import com.getcode.opencode.utils.padded
6+
import com.getcode.solana.keys.PublicKey
7+
import java.security.KeyPairGenerator
8+
import java.security.SecureRandom
9+
import kotlin.test.Test
10+
import kotlin.test.assertEquals
11+
12+
import kotlin.test.assertTrue
13+
class LocalFiatTests {
14+
15+
private val launchpadMetadata = LaunchpadMetadata(
16+
currencyConfig = generateRandomPublicKeyForTest(),
17+
liquidityPool = generateRandomPublicKeyForTest(),
18+
seed = generateRandomPublicKeyForTest(),
19+
authority = vmAuthority,
20+
mintVault = generateRandomPublicKeyForTest(),
21+
coreMintVault = generateRandomPublicKeyForTest(),
22+
coreMintFees = generateRandomPublicKeyForTest(),
23+
currentCirculatingSupplyQuarks = 0,
24+
coreMintLockedQuarks = 0,
25+
sellFeeBps = 0
26+
)
27+
28+
private val token = Token(
29+
address = vmAuthority,
30+
decimals = 6,
31+
name = "USDC",
32+
symbol = "USDC",
33+
description = "",
34+
imageUrl = "",
35+
vmMetadata = VmMetadata(
36+
authority = vmAuthority,
37+
vm = vmAuthority,
38+
lockDurationInDays = TimelockDerivedAccounts.lockoutInDays.toInt()
39+
),
40+
launchpadMetadata = launchpadMetadata,
41+
billCustomizations = null,
42+
)
43+
44+
@Test
45+
fun `test sending amounts`() {
46+
val startSupply = 1_00_00_000_000
47+
val endSupply = 21_000_000_00_00_000_000
48+
49+
val fiatToTest = listOf(
50+
5.00.toFiat(),
51+
10.00.toFiat(),
52+
100.00.toFiat(),
53+
500.00.toFiat(),
54+
1_000.00.toFiat(),
55+
)
56+
57+
val output = buildString {
58+
var supply = startSupply
59+
while (supply <= endSupply) {
60+
val updatedTokenOnChain = token.copy(
61+
launchpadMetadata = launchpadMetadata.copy(
62+
currentCirculatingSupplyQuarks = supply
63+
)
64+
)
65+
fiatToTest.forEach { amount ->
66+
val exchanged = LocalFiat.valueExchangeIn(
67+
amount = amount,
68+
token = updatedTokenOnChain,
69+
rate = Rate.oneToOne,
70+
debug = false,
71+
)
72+
73+
val formattedSupply = supply.toString().padded(20)
74+
val underlying = exchanged.underlyingTokenAmount.quarks.toString().padded(20)
75+
val converted = exchanged.nativeAmount.formatted().padded(20)
76+
77+
appendLine("$formattedSupply $underlying $converted")
78+
}
79+
supply *= 10
80+
}
81+
}.trim()
82+
83+
println(output)
84+
85+
val expectedOutput = """
86+
10000000000 5001092401997 $5.00
87+
10000000000 10004379663394 $10.00
88+
10000000000 100441080923772 $100.00
89+
10000000000 511295760602584 $500.00
90+
10000000000 1046604099690267 $1,000.00
91+
100000000000 5001052911980 $5.00
92+
100000000000 10004300648691 $10.00
93+
100000000000 100440284483693 $100.00
94+
100000000000 511291632270217 $500.00
95+
100000000000 1046595446081380 $1,000.00
96+
1000000000000 5000658028967 $5.00
97+
1000000000000 10003510535997 $10.00
98+
1000000000000 100432320431767 $100.00
99+
1000000000000 511250350821238 $500.00
100+
1000000000000 1046508914111068 $1,000.00
101+
10000000000000 4996710913709 $5.00
102+
10000000000000 9995612841801 $10.00
103+
10000000000000 100352714788750 $100.00
104+
10000000000000 510837723741344 $500.00
105+
10000000000000 1045644006120180 $1,000.00
106+
100000000000000 4957410747532 $5.00
107+
100000000000000 9916978171984 $10.00
108+
100000000000000 99560135637918 $100.00
109+
100000000000000 506730134317242 $500.00
110+
100000000000000 1037035954468485 $1,000.00
111+
1000000000000000 4581018238122 $5.00
112+
1000000000000000 9163878032451 $10.00
113+
1000000000000000 91971957711638 $100.00
114+
1000000000000000 467464270426932 $500.00
115+
1000000000000000 954919469210810 $1,000.00
116+
10000000000000000 2079970815228 $5.00
117+
10000000000000000 4160321190168 $10.00
118+
10000000000000000 41671690967489 $100.00
119+
10000000000000000 209898607695880 $500.00
120+
10000000000000000 423734428251854 $1,000.00
121+
100000000000000000 775257916 $5.00
122+
100000000000000000 1550515885 $10.00
123+
100000000000000000 15505168342 $100.00
124+
100000000000000000 77526052595 $500.00
125+
100000000000000000 155052632401 $1,000.00
126+
""".trimIndent()
127+
128+
val actualLines = output.lines()
129+
val expectedLines = expectedOutput.lines()
130+
131+
assertEquals(expectedLines.size, actualLines.size)
132+
133+
for (i in actualLines.indices) {
134+
val actualParts = actualLines[i].split("\\s+".toRegex()).filter { it.isNotEmpty() }
135+
val expectedParts = expectedLines[i].split("\\s+".toRegex()).filter { it.isNotEmpty() }
136+
137+
assertEquals(expectedParts[0], actualParts[0], "Column 1 mismatch on line $i")
138+
val diff = (expectedParts[1].toLong() - actualParts[1].toLong()).let { if (it < 0) -it else it }
139+
assertTrue(diff <= 1, "Column 2 is not within 1 on line $i")
140+
assertEquals(expectedParts[2], actualParts[2], "Column 3 mismatch on line $i")
141+
}
142+
}
143+
144+
@Test
145+
fun `test quarks to balance conversion`() {
146+
val startVol = 1_000_000L
147+
val endVol = 100_000_000_000_000L
148+
149+
val quarks = 1_000_000_000_000L
150+
151+
val output = buildString {
152+
var index = 0
153+
var volume = startVol
154+
while (volume <= endVol) {
155+
val updatedTokenOnChain = token.copy(
156+
launchpadMetadata = launchpadMetadata.copy(
157+
coreMintLockedQuarks = volume
158+
)
159+
)
160+
val tokenBalance = Fiat.tokenBalance(
161+
quarks = quarks,
162+
token = updatedTokenOnChain,
163+
)
164+
165+
val exchanged = LocalFiat.valueExchangeIn(
166+
amount = tokenBalance,
167+
token = updatedTokenOnChain,
168+
rate = Rate.oneToOne,
169+
debug = false,
170+
)
171+
172+
val nativeAmountsForExchangedQuarks = exchanged.nativeAmount.formatted()
173+
174+
appendLine(nativeAmountsForExchangedQuarks)
175+
176+
volume *= 10
177+
index += 1
178+
}
179+
}.trim()
180+
181+
println(output)
182+
183+
val expectedOutput = listOf(
184+
"$1.00",
185+
"$1.00",
186+
"$1.01",
187+
"$1.09",
188+
"$1.88",
189+
"$9.77",
190+
"$88.71",
191+
"$878.14",
192+
"$8,772.37"
193+
)
194+
195+
val generatedOutput = output.lines()
196+
197+
assertEquals(expectedOutput, generatedOutput)
198+
}
199+
}
200+
201+
/**
202+
* Generates a random Public Key for testing purposes.
203+
*/
204+
private fun generateRandomPublicKeyForTest(): PublicKey {
205+
// 1. Generate a KeyPair
206+
val keyGen = KeyPairGenerator.getInstance("RSA")
207+
keyGen.initialize(2048, SecureRandom()) // Use SecureRandom for strong keys
208+
val keyPair = keyGen.generateKeyPair()
209+
210+
// 2. Extract the public key bytes
211+
val publicKeyBytes = keyPair.public.encoded.toList()
212+
213+
return PublicKey(publicKeyBytes)
214+
}

0 commit comments

Comments
 (0)